From 70c57bca08aa8d8331a26d461a79be26b19b66aa Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Mon, 18 Oct 2021 13:58:07 -0700 Subject: [PATCH 01/53] Update doc links to Fleet/Agent docs (#115289) --- docs/getting-started/quick-start-guide.asciidoc | 2 +- docs/osquery/osquery.asciidoc | 2 +- docs/setup/connect-to-elasticsearch.asciidoc | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/getting-started/quick-start-guide.asciidoc b/docs/getting-started/quick-start-guide.asciidoc index d614aece5b42..2bddd9bf6145 100644 --- a/docs/getting-started/quick-start-guide.asciidoc +++ b/docs/getting-started/quick-start-guide.asciidoc @@ -138,7 +138,7 @@ image::images/dashboard_sampleDataAddFilter_7.15.0.png[The [eCommerce] Revenue D [[quick-start-whats-next]] == What's next? -*Add your own data.* Ready to add your own data? Go to {fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack] to learn how to ingest your data, or go to <> and learn about all the other ways you can add data. +*Add your own data.* Ready to add your own data? Go to {observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}], or go to <> and learn about all the other ways you can add data. *Explore your own data in Discover.* Ready to learn more about exploring your data in *Discover*? Go to <>. diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 1e4e6604a7c7..a4f3c8046314 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -365,7 +365,7 @@ The following is an example of an **error response** for an undefined action que == System requirements * {fleet-guide}/fleet-overview.html[Fleet] is enabled on your cluster, and -one or more {fleet-guide}/elastic-agent-installation-configuration.html[Elastic Agents] is enrolled. +one or more {fleet-guide}/elastic-agent-installation.html[Elastic Agents] is enrolled. * The https://docs.elastic.co/en/integrations/osquery_manager[*Osquery Manager*] integration has been added and configured for an agent policy through Fleet. diff --git a/docs/setup/connect-to-elasticsearch.asciidoc b/docs/setup/connect-to-elasticsearch.asciidoc index c7f745648cbe..ad38ac1710fd 100644 --- a/docs/setup/connect-to-elasticsearch.asciidoc +++ b/docs/setup/connect-to-elasticsearch.asciidoc @@ -47,7 +47,7 @@ so you can quickly get insights into your data, and {fleet} mode offers several image::images/addData_fleet_7.15.0.png[Add data using Fleet] To get started, refer to -{fleet-guide}/fleet-quick-start.html[Quick start: Get logs and metrics into the Elastic Stack]. +{observability-guide}/ingest-logs-metrics-uptime.html[Ingest logs, metrics, and uptime data with {agent}]. [discrete] [[upload-data-kibana]] From 7e1acab9dd3c3504fe0ef58be29547919d7b3a26 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 18 Oct 2021 23:09:04 +0200 Subject: [PATCH 02/53] [Exploratory View] Added step level filtering/breakdowns (#115182) Co-authored-by: Dominique Clarke Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../configurations/constants/constants.ts | 3 + .../constants/field_names/synthetics.ts | 2 + .../configurations/constants/labels.ts | 14 ++++ .../configurations/lens_attributes.ts | 16 ++++- .../synthetics/data_distribution_config.ts | 6 +- .../synthetics/field_formats.ts | 14 ++++ .../synthetics/kpi_over_time_config.ts | 54 +++++++++++++- .../test_data/sample_attribute_cwv.ts | 3 +- .../series_editor/breakdown/breakdowns.tsx | 49 +++++++++++-- .../columns/incomplete_badge.tsx | 9 ++- .../columns/report_definition_col.tsx | 70 ++++++++++++++++--- .../columns/report_definition_field.tsx | 47 +++++++++---- .../series_editor/expanded_series_row.tsx | 6 +- .../shared/exploratory_view/types.ts | 10 ++- .../field_value_combobox.tsx | 2 + .../shared/field_value_suggestions/index.tsx | 5 +- .../shared/field_value_suggestions/types.ts | 1 + .../common/header/action_menu_content.tsx | 5 +- 18 files changed, 268 insertions(+), 48 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e4473b183d72..c12e67bc9b1a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -49,7 +49,9 @@ import { MONITORS_DURATION_LABEL, PAGE_LOAD_TIME_LABEL, LABELS_FIELD, + STEP_NAME_LABEL, } from './labels'; +import { SYNTHETICS_STEP_NAME } from './field_names/synthetics'; export const DEFAULT_TIME = { from: 'now-1h', to: 'now' }; @@ -77,6 +79,7 @@ export const FieldLabels: Record = { 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, 'monitor.duration.us': MONITORS_DURATION_LABEL, + [SYNTHETICS_STEP_NAME]: STEP_NAME_LABEL, 'agent.hostname': AGENT_HOST_LABEL, 'host.hostname': HOST_NAME_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts index eff73d242de7..0f2864855272 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/field_names/synthetics.ts @@ -11,3 +11,5 @@ export const SYNTHETICS_LCP = 'browser.experience.lcp.us'; export const SYNTHETICS_FCP = 'browser.experience.fcp.us'; export const SYNTHETICS_DOCUMENT_ONLOAD = 'browser.experience.load.us'; export const SYNTHETICS_DCL = 'browser.experience.dcl.us'; +export const SYNTHETICS_STEP_NAME = 'synthetics.step.name.keyword'; +export const SYNTHETICS_STEP_DURATION = 'synthetics.step.duration.us'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts index cdaa89fc7138..599f846af2ff 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/labels.ts @@ -231,6 +231,20 @@ export const MONITORS_DURATION_LABEL = i18n.translate( } ); +export const STEP_DURATION_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepDurationLabel', + { + defaultMessage: 'Step duration', + } +); + +export const STEP_NAME_LABEL = i18n.translate( + 'xpack.observability.expView.fieldLabels.stepNameLabel', + { + defaultMessage: 'Step name', + } +); + export const WEB_APPLICATION_LABEL = i18n.translate( 'xpack.observability.expView.fieldLabels.webApplication', { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 38c9ecc06491..a31bef7c9c21 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -194,10 +194,12 @@ export class LensAttributes { label, sourceField, columnType, + columnFilter, operationType, }: { sourceField: string; columnType?: string; + columnFilter?: ColumnFilter; operationType?: string; label?: string; seriesConfig: SeriesConfig; @@ -214,6 +216,7 @@ export class LensAttributes { operationType, label, seriesConfig, + columnFilter, }); } if (operationType?.includes('th')) { @@ -228,11 +231,13 @@ export class LensAttributes { label, seriesConfig, operationType, + columnFilter, }: { sourceField: string; operationType: 'average' | 'median' | 'sum' | 'unique_count'; label?: string; seriesConfig: SeriesConfig; + columnFilter?: ColumnFilter; }): | AvgIndexPatternColumn | MedianIndexPatternColumn @@ -247,6 +252,7 @@ export class LensAttributes { operationType: capitalize(operationType), }, }), + filter: columnFilter, operationType, }; } @@ -391,6 +397,7 @@ export class LensAttributes { return this.getNumberColumn({ sourceField: fieldName, columnType, + columnFilter: columnFilters?.[0], operationType, label: columnLabel || label, seriesConfig: layerConfig.seriesConfig, @@ -447,10 +454,10 @@ export class LensAttributes { return this.getColumnBasedOnType({ sourceField, - operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, label, layerConfig, colIndex: 0, + operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, }); } @@ -629,7 +636,12 @@ export class LensAttributes { [`y-axis-column-${layerId}`]: { ...mainYAxis, label, - filter: { query: columnFilter, language: 'kuery' }, + filter: { + query: mainYAxis.filter + ? `${columnFilter} and ${mainYAxis.filter.query}` + : columnFilter, + language: 'kuery', + }, ...(timeShift ? { timeShift } : {}), }, ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index da90f45d1520..fb44da8e4327 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -29,6 +29,7 @@ import { SYNTHETICS_FCP, SYNTHETICS_LCP, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; export function getSyntheticsDistributionConfig({ series, @@ -58,7 +59,10 @@ export function getSyntheticsDistributionConfig({ 'url.port', ], baseFilters: [], - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: 'synthetics.step.name.keyword', singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts index 5f8a6a28ca81..f3e3fe081784 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/field_formats.ts @@ -11,6 +11,7 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, } from '../constants/field_names/synthetics'; export const syntheticsFieldFormats: FieldFormat[] = [ @@ -27,6 +28,19 @@ export const syntheticsFieldFormats: FieldFormat[] = [ }, }, }, + { + field: SYNTHETICS_STEP_DURATION, + format: { + id: 'duration', + params: { + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + showSuffix: true, + useShortSuffix: true, + }, + }, + }, { field: SYNTHETICS_LCP, format: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 6df9cdcd0503..8951ffcda63d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConfigProps, SeriesConfig } from '../../types'; +import { ColumnFilter, ConfigProps, SeriesConfig } from '../../types'; import { FieldLabels, OPERATION_COLUMN, @@ -21,6 +21,7 @@ import { FCP_LABEL, LCP_LABEL, MONITORS_DURATION_LABEL, + STEP_DURATION_LABEL, UP_LABEL, } from '../constants/labels'; import { @@ -30,10 +31,26 @@ import { SYNTHETICS_DOCUMENT_ONLOAD, SYNTHETICS_FCP, SYNTHETICS_LCP, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_STEP_NAME, } from '../constants/field_names/synthetics'; +import { buildExistsFilter } from '../utils'; const SUMMARY_UP = 'summary.up'; const SUMMARY_DOWN = 'summary.down'; +export const isStepLevelMetric = (metric?: string) => { + if (!metric) { + return false; + } + return [ + SYNTHETICS_LCP, + SYNTHETICS_FCP, + SYNTHETICS_CLS, + SYNTHETICS_DCL, + SYNTHETICS_STEP_DURATION, + SYNTHETICS_DOCUMENT_ONLOAD, + ].includes(metric); +}; export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesConfig { return { reportType: ReportTypes.KPI, @@ -50,10 +67,19 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon ], hasOperationType: false, filterFields: ['observer.geo.name', 'monitor.type', 'tags'], - breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name', PERCENTILE], + breakdownFields: [ + 'observer.geo.name', + 'monitor.type', + 'monitor.name', + SYNTHETICS_STEP_NAME, + PERCENTILE, + ], baseFilters: [], palette: { type: 'palette', name: 'status' }, - definitionFields: ['monitor.name', 'url.full'], + definitionFields: [ + { field: 'monitor.name', nested: SYNTHETICS_STEP_NAME, singleSelection: true }, + { field: 'url.full', filters: buildExistsFilter('summary.up', indexPattern) }, + ], metricOptions: [ { label: MONITORS_DURATION_LABEL, @@ -73,37 +99,59 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon label: DOWN_LABEL, columnType: OPERATION_COLUMN, }, + { + label: STEP_DURATION_LABEL, + field: SYNTHETICS_STEP_DURATION, + id: SYNTHETICS_STEP_DURATION, + columnType: OPERATION_COLUMN, + columnFilters: [STEP_END_FILTER], + }, { label: LCP_LABEL, field: SYNTHETICS_LCP, id: SYNTHETICS_LCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: FCP_LABEL, field: SYNTHETICS_FCP, id: SYNTHETICS_FCP, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DCL_LABEL, field: SYNTHETICS_DCL, id: SYNTHETICS_DCL, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: DOCUMENT_ONLOAD_LABEL, field: SYNTHETICS_DOCUMENT_ONLOAD, id: SYNTHETICS_DOCUMENT_ONLOAD, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, { label: CLS_LABEL, field: SYNTHETICS_CLS, id: SYNTHETICS_CLS, columnType: OPERATION_COLUMN, + columnFilters: [STEP_METRIC_FILTER], }, ], labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL }, }; } + +const STEP_METRIC_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/metrics`, +}; + +const STEP_END_FILTER: ColumnFilter = { + language: 'kuery', + query: `synthetics.type: step/end`, +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index adc6d4bb1446..4563509eeb19 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -77,7 +77,8 @@ export const sampleAttributeCoreWebVital = { dataType: 'number', filter: { language: 'kuery', - query: 'transaction.type: page-load and processor.event: transaction', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.marks.agent.largestContentfulPaint < 2500', }, isBucketed: false, label: 'Good', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx index a235cbd8852a..f30a80f87ebb 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect } from 'react'; import styled from 'styled-components'; import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -17,6 +17,8 @@ import { PERCENTILE, } from '../../configurations/constants'; import { SeriesConfig, SeriesUrl } from '../../types'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; interface Props { seriesId: number; @@ -51,6 +53,18 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } }; + useEffect(() => { + if ( + !isStepLevelMetric(series.selectedMetricField) && + selectedBreakdown === SYNTHETICS_STEP_NAME + ) { + setSeries(seriesId, { + ...series, + breakdown: undefined, + }); + } + }); + if (!seriesConfig) { return null; } @@ -71,11 +85,26 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } const options = items - .map(({ id, label }) => ({ - inputDisplay: label, - value: id, - dropdownDisplay: label, - })) + .map(({ id, label }) => { + if (id === SYNTHETICS_STEP_NAME && !isStepLevelMetric(series.selectedMetricField)) { + return { + inputDisplay: label, + value: id, + dropdownDisplay: ( + + <>{label} + + ), + disabled: true, + }; + } else { + return { + inputDisplay: label, + value: id, + dropdownDisplay: label, + }; + } + }) .filter(({ value }) => !(value === PERCENTILE && isRecordsMetric)); let valueOfSelected = @@ -121,6 +150,14 @@ export const BREAKDOWN_WARNING = i18n.translate('xpack.observability.exp.breakDo defaultMessage: 'Breakdowns can be applied to only one series at a time.', }); +export const BREAKDOWN_UNAVAILABLE = i18n.translate( + 'xpack.observability.exp.breakDownFilter.unavailable', + { + defaultMessage: + 'Step name breakdown is not available for monitor duration metric. Use step duration metric to breakdown by step name.', + } +); + const Wrapper = styled.span` .euiToolTipAnchor { width: 100%; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx index 4e1c38592190..8e64f4bcea68 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/incomplete_badge.tsx @@ -31,7 +31,14 @@ export function IncompleteBadge({ seriesConfig, series }: Props) { const incompleteDefinition = isEmpty(reportDefinitions) ? i18n.translate('xpack.observability.overview.exploratoryView.missingReportDefinition', { defaultMessage: 'Missing {reportDefinition}', - values: { reportDefinition: labels?.[definitionFields[0]] }, + values: { + reportDefinition: + labels?.[ + typeof definitionFields[0] === 'string' + ? definitionFields[0] + : definitionFields[0].field + ], + }, }) : ''; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx index fbd7c34303d9..a665ec199913 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_col.tsx @@ -6,10 +6,13 @@ */ import React from 'react'; +import { isEmpty } from 'lodash'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { SeriesConfig, SeriesUrl } from '../../types'; import { ReportDefinitionField } from './report_definition_field'; +import { isStepLevelMetric } from '../../configurations/synthetics/kpi_over_time_config'; +import { SYNTHETICS_STEP_NAME } from '../../configurations/constants/field_names/synthetics'; export function ReportDefinitionCol({ seriesId, @@ -41,19 +44,64 @@ export function ReportDefinitionCol({ } }; + const hasFieldDataSelected = (field: string) => { + return !isEmpty(series.reportDefinitions?.[field]); + }; + return ( - {definitionFields.map((field) => ( - - - - ))} + {definitionFields.map((field) => { + const fieldStr = typeof field === 'string' ? field : field.field; + const singleSelection = typeof field !== 'string' && field.singleSelection; + const nestedField = typeof field !== 'string' && field.nested; + const filters = typeof field !== 'string' ? field.filters : undefined; + + const isNonStepMetric = !isStepLevelMetric(series.selectedMetricField); + + const hideNestedStep = nestedField === SYNTHETICS_STEP_NAME && isNonStepMetric; + + if (hideNestedStep && nestedField && selectedReportDefinitions[nestedField]?.length > 0) { + setSeries(seriesId, { + ...series, + reportDefinitions: { ...selectedReportDefinitions, [nestedField]: [] }, + }); + } + + let nestedFieldElement; + + if (nestedField && hasFieldDataSelected(fieldStr) && !hideNestedStep) { + nestedFieldElement = ( + + + + ); + } + + return ( + <> + + + + {nestedFieldElement} + + ); + })} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx index 01f36e85c03a..f3e0eb767d33 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/report_definition_field.tsx @@ -7,7 +7,7 @@ import React, { useMemo } from 'react'; import { isEmpty } from 'lodash'; -import { ExistsFilter } from '@kbn/es-query'; +import { ExistsFilter, PhraseFilter } from '@kbn/es-query'; import FieldValueSuggestions from '../../../field_value_suggestions'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { ESFilter } from '../../../../../../../../../src/core/types/elasticsearch'; @@ -19,32 +19,49 @@ import { ALL_VALUES_SELECTED } from '../../../field_value_suggestions/field_valu interface Props { seriesId: number; series: SeriesUrl; - field: string; + singleSelection?: boolean; + keepHistory?: boolean; + field: string | { field: string; nested: string }; seriesConfig: SeriesConfig; onChange: (field: string, value?: string[]) => void; + filters?: Array; } -export function ReportDefinitionField({ series, field, seriesConfig, onChange }: Props) { +export function ReportDefinitionField({ + singleSelection, + keepHistory, + series, + field: fieldProp, + seriesConfig, + onChange, + filters, +}: Props) { const { indexPattern } = useAppIndexPatternContext(series.dataType); + const field = typeof fieldProp === 'string' ? fieldProp : fieldProp.field; + const { reportDefinitions: selectedReportDefinitions = {} } = series; const { labels, baseFilters, definitionFields } = seriesConfig; const queryFilters = useMemo(() => { const filtersN: ESFilter[] = []; - (baseFilters ?? []).forEach((qFilter: PersistableFilter | ExistsFilter) => { - if (qFilter.query) { - filtersN.push(qFilter.query); - } - const existFilter = qFilter as ExistsFilter; - if (existFilter.query.exists) { - filtersN.push({ exists: existFilter.query.exists }); - } - }); + (baseFilters ?? []) + .concat(filters ?? []) + .forEach((qFilter: PersistableFilter | ExistsFilter) => { + if (qFilter.query) { + filtersN.push(qFilter.query); + } + const existFilter = qFilter as ExistsFilter; + if (existFilter.query.exists) { + filtersN.push({ exists: existFilter.query.exists }); + } + }); if (!isEmpty(selectedReportDefinitions)) { - definitionFields.forEach((fieldT) => { + definitionFields.forEach((fieldObj) => { + const fieldT = typeof fieldObj === 'string' ? fieldObj : fieldObj.field; + if (indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; if (!values.includes(ALL_VALUES_SELECTED)) { @@ -65,7 +82,7 @@ export function ReportDefinitionField({ series, field, seriesConfig, onChange }: return ( ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx index 180be1ac0414..12e0ceca2064 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -48,13 +48,13 @@ export function ExpandedSeriesRow(seriesProps: Props) { return (
- - + + - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts index 001664cf1278..acd49fc25588 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts @@ -56,7 +56,15 @@ export interface SeriesConfig { filterFields: Array; seriesTypes: SeriesType[]; baseFilters?: Array; - definitionFields: string[]; + definitionFields: Array< + | string + | { + field: string; + nested?: string; + singleSelection?: boolean; + filters?: Array; + } + >; metricOptions?: MetricOption[]; labels: Record; hasOperationType: boolean; diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx index 0735df53888a..e04d5463d549 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/field_value_combobox.tsx @@ -42,6 +42,7 @@ export function FieldValueCombobox({ usePrependLabel = true, compressed = true, required = true, + singleSelection = false, allowAllValuesSelection, onChange: onSelectionChange, }: FieldValueSelectionProps) { @@ -68,6 +69,7 @@ export function FieldValueCombobox({ const comboBox = ( ); diff --git a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts index 6f6d520a8315..95b24aa69b1e 100644 --- a/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts +++ b/x-pack/plugins/observability/public/components/shared/field_value_suggestions/types.ts @@ -29,6 +29,7 @@ interface CommonProps { allowAllValuesSelection?: boolean; cardinalityField?: string; required?: boolean; + keepHistory?: boolean; } export type FieldValueSuggestionsProps = CommonProps & { diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index 21ef3428696e..bcfbf18c93cf 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -56,9 +56,8 @@ export function ActionMenuContent(): React.ReactElement { time: { from: dateRangeStart, to: dateRangeEnd }, breakdown: monitorId ? 'observer.geo.name' : 'monitor.type', reportDefinitions: { - 'monitor.name': selectedMonitor?.monitor?.name - ? [selectedMonitor?.monitor?.name] - : ['ALL_VALUES'], + 'monitor.name': selectedMonitor?.monitor?.name ? [selectedMonitor?.monitor?.name] : [], + 'url.full': ['ALL_VALUES'], }, name: monitorId ? `${monitorId}-response-duration` : 'All monitors response duration', }, From 85d7115d4a3051846655e46575b3558530491fd0 Mon Sep 17 00:00:00 2001 From: Rich Kuzsma <62522248+richkuz@users.noreply.github.com> Date: Mon, 18 Oct 2021 17:19:00 -0400 Subject: [PATCH 03/53] Document edge cases for enterpriseSearch.host (#115446) Fixes https://github.com/elastic/enterprise-search-team/issues/517 --- docs/setup/settings.asciidoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 16fa8eb73420..4802a4da8182 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -275,7 +275,7 @@ that the {kib} server uses to perform maintenance on the {kib} index at startup. is an alternative to `elasticsearch.username` and `elasticsearch.password`. | `enterpriseSearch.host` - | The URL of your Enterprise Search instance + | The http(s) URL of your Enterprise Search instance. For example, in a local self-managed setup, set this to `http://localhost:3002`. Authentication between Kibana and the Enterprise Search host URL, such as via OAuth, is not supported. You can also {enterprise-search-ref}/configure-ssl-tls.html#configure-ssl-tls-in-kibana[configure Kibana to trust your Enterprise Search TLS certificate authority]. | `interpreter.enableInVisualize` | Enables use of interpreter in Visualize. *Default: `true`* From eb5ffff7d09f53880d24f5d6d43289afe1eee2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Mon, 18 Oct 2021 23:25:01 +0200 Subject: [PATCH 04/53] Clean angular from moved code, global state and legacy shims (#115420) --- .../public/alerts/alert_form.test.tsx | 18 +- .../public/angular/angular_i18n/directive.ts | 103 ------ .../public/angular/angular_i18n/filter.ts | 19 - .../public/angular/angular_i18n/index.ts | 15 - .../public/angular/angular_i18n/provider.ts | 25 -- .../helpers/format_angular_http_error.ts | 43 --- .../public/angular/top_nav/angular_config.tsx | 349 ------------------ .../public/angular/top_nav/index.ts | 10 - .../public/angular/top_nav/kbn_top_nav.d.ts | 16 - .../public/angular/top_nav/kbn_top_nav.js | 119 ------ .../contexts/global_state_context.tsx | 25 +- .../plugins/monitoring/public/legacy_shims.ts | 25 +- x-pack/plugins/monitoring/public/url_state.ts | 34 -- 13 files changed, 18 insertions(+), 783 deletions(-) delete mode 100644 x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx delete mode 100644 x-pack/plugins/monitoring/public/angular/top_nav/index.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts delete mode 100644 x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js diff --git a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx index 8752a4118fb6..1a44beab260c 100644 --- a/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alert_form.test.tsx @@ -50,17 +50,13 @@ const initLegacyShims = () => { ruleTypeRegistry: ruleTypeRegistryMock.create(), }; const data = { query: { timefilter: { timefilter: {} } } } as any; - const ngInjector = {} as angular.auto.IInjectorService; - Legacy.init( - { - core: coreMock.createStart(), - data, - isCloud: false, - triggersActionsUi, - usageCollection: {}, - } as any, - ngInjector - ); + Legacy.init({ + core: coreMock.createStart(), + data, + isCloud: false, + triggersActionsUi, + usageCollection: {}, + } as any); }; const ALERTS_FEATURE_ID = 'alerts'; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts deleted file mode 100644 index 1aaff99a6a5c..000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/directive.ts +++ /dev/null @@ -1,103 +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 { IDirective, IRootElementService, IScope } from 'angular'; - -import { I18nServiceType } from './provider'; - -interface I18nScope extends IScope { - values?: Record; - defaultMessage: string; - id: string; -} - -const HTML_KEY_PREFIX = 'html_'; -const PLACEHOLDER_SEPARATOR = '@I18N@'; - -export const i18nDirective: [string, string, typeof i18nDirectiveFn] = [ - 'i18n', - '$sanitize', - i18nDirectiveFn, -]; - -function i18nDirectiveFn( - i18n: I18nServiceType, - $sanitize: (html: string) => string -): IDirective { - return { - restrict: 'A', - scope: { - id: '@i18nId', - defaultMessage: '@i18nDefaultMessage', - values: ' { - setContent($element, $scope, $sanitize, i18n); - }); - } else { - setContent($element, $scope, $sanitize, i18n); - } - }, - }; -} - -function setContent( - $element: IRootElementService, - $scope: I18nScope, - $sanitize: (html: string) => string, - i18n: I18nServiceType -) { - const originalValues = $scope.values; - const valuesWithPlaceholders = {} as Record; - let hasValuesWithPlaceholders = false; - - // If we have values with the keys that start with HTML_KEY_PREFIX we should replace - // them with special placeholders that later on will be inserted as HTML - // into the DOM, the rest of the content will be treated as text. We don't - // sanitize values at this stage as some of the values can be excluded from - // the translated string (e.g. not used by ICU conditional statements). - if (originalValues) { - for (const [key, value] of Object.entries(originalValues)) { - if (key.startsWith(HTML_KEY_PREFIX)) { - valuesWithPlaceholders[ - key.slice(HTML_KEY_PREFIX.length) - ] = `${PLACEHOLDER_SEPARATOR}${key}${PLACEHOLDER_SEPARATOR}`; - - hasValuesWithPlaceholders = true; - } else { - valuesWithPlaceholders[key] = value; - } - } - } - - const label = i18n($scope.id, { - values: valuesWithPlaceholders, - defaultMessage: $scope.defaultMessage, - }); - - // If there are no placeholders to replace treat everything as text, otherwise - // insert label piece by piece replacing every placeholder with corresponding - // sanitized HTML content. - if (!hasValuesWithPlaceholders) { - $element.text(label); - } else { - $element.empty(); - for (const contentOrPlaceholder of label.split(PLACEHOLDER_SEPARATOR)) { - if (!contentOrPlaceholder) { - continue; - } - - $element.append( - originalValues!.hasOwnProperty(contentOrPlaceholder) - ? $sanitize(originalValues![contentOrPlaceholder]) - : document.createTextNode(contentOrPlaceholder) - ); - } - } -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.ts deleted file mode 100644 index e4e553fa47b6..000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/filter.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 { I18nServiceType } from './provider'; - -export const i18nFilter: [string, typeof i18nFilterFn] = ['i18n', i18nFilterFn]; - -function i18nFilterFn(i18n: I18nServiceType) { - return (id: string, { defaultMessage = '', values = {} } = {}) => { - return i18n(id, { - values, - defaultMessage, - }); - }; -} diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts deleted file mode 100644 index 8915c96e59be..000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/index.ts +++ /dev/null @@ -1,15 +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. - */ - -export { I18nProvider } from './provider'; - -export { i18nFilter } from './filter'; -export { i18nDirective } from './directive'; - -// re-export types: https://github.com/babel/babel-loader/issues/603 -import { I18nServiceType as _I18nServiceType } from './provider'; -export type I18nServiceType = _I18nServiceType; diff --git a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts b/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts deleted file mode 100644 index b1da1bad6e39..000000000000 --- a/x-pack/plugins/monitoring/public/angular/angular_i18n/provider.ts +++ /dev/null @@ -1,25 +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 { i18n } from '@kbn/i18n'; - -export type I18nServiceType = ReturnType; - -export class I18nProvider implements angular.IServiceProvider { - public addTranslation = i18n.addTranslation; - public getTranslation = i18n.getTranslation; - public setLocale = i18n.setLocale; - public getLocale = i18n.getLocale; - public setDefaultLocale = i18n.setDefaultLocale; - public getDefaultLocale = i18n.getDefaultLocale; - public setFormats = i18n.setFormats; - public getFormats = i18n.getFormats; - public getRegisteredLocales = i18n.getRegisteredLocales; - public init = i18n.init; - public load = i18n.load; - public $get = () => i18n.translate; -} diff --git a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts b/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts deleted file mode 100644 index abdcf157a3c8..000000000000 --- a/x-pack/plugins/monitoring/public/angular/helpers/format_angular_http_error.ts +++ /dev/null @@ -1,43 +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 { i18n } from '@kbn/i18n'; -import type { IHttpResponse } from 'angular'; - -type AngularHttpError = IHttpResponse<{ message: string }>; - -export function isAngularHttpError(error: any): error is AngularHttpError { - return ( - error && - typeof error.status === 'number' && - typeof error.statusText === 'string' && - error.data && - typeof error.data.message === 'string' - ); -} - -export function formatAngularHttpError(error: AngularHttpError) { - // is an Angular $http "error object" - if (error.status === -1) { - // status = -1 indicates that the request was failed to reach the server - return i18n.translate('xpack.monitoring.notify.fatalError.unavailableServerErrorMessage', { - defaultMessage: - 'An HTTP request has failed to connect. ' + - 'Please check if the Kibana server is running and that your browser has a working connection, ' + - 'or contact your system administrator.', - }); - } - - return i18n.translate('xpack.monitoring.notify.fatalError.errorStatusMessage', { - defaultMessage: 'Error {errStatus} {errStatusText}: {errMessage}', - values: { - errStatus: error.status, - errStatusText: error.statusText, - errMessage: error.data.message, - }, - }); -} diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx b/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx deleted file mode 100644 index 9c2e931d24a9..000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/angular_config.tsx +++ /dev/null @@ -1,349 +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 { - ICompileProvider, - IHttpProvider, - IHttpService, - ILocationProvider, - IModule, - IRootScopeService, - IRequestConfig, -} from 'angular'; -import $ from 'jquery'; -import { set } from '@elastic/safer-lodash-set'; -import { get } from 'lodash'; -import * as Rx from 'rxjs'; -import { ChromeBreadcrumb, EnvironmentMode, PackageInfo } from 'kibana/public'; -import { History } from 'history'; - -import { CoreStart } from 'kibana/public'; -import { formatAngularHttpError, isAngularHttpError } from '../helpers/format_angular_http_error'; - -export interface RouteConfiguration { - controller?: string | ((...args: any[]) => void); - redirectTo?: string; - resolveRedirectTo?: (...args: any[]) => void; - reloadOnSearch?: boolean; - reloadOnUrl?: boolean; - outerAngularWrapperRoute?: boolean; - resolve?: object; - template?: string; - k7Breadcrumbs?: (...args: any[]) => ChromeBreadcrumb[]; - requireUICapability?: string; -} - -function isSystemApiRequest(request: IRequestConfig) { - const { headers } = request; - return headers && !!headers['kbn-system-request']; -} - -/** - * Detects whether a given angular route is a dummy route that doesn't - * require any action. There are two ways this can happen: - * If `outerAngularWrapperRoute` is set on the route config object, - * it means the local application service set up this route on the outer angular - * and the internal routes will handle the hooks. - * - * If angular did not detect a route and it is the local angular, we are currently - * navigating away from a URL controlled by a local angular router and the - * application will get unmounted. In this case the outer router will handle - * the hooks. - * @param $route Injected $route dependency - * @param isLocalAngular Flag whether this is the local angular router - */ -function isDummyRoute($route: any, isLocalAngular: boolean) { - return ( - ($route.current && $route.current.$$route && $route.current.$$route.outerAngularWrapperRoute) || - (!$route.current && isLocalAngular) - ); -} - -export const configureAppAngularModule = ( - angularModule: IModule, - newPlatform: { - core: CoreStart; - readonly env: { - mode: Readonly; - packageInfo: Readonly; - }; - }, - isLocalAngular: boolean, - getHistory?: () => History -) => { - const core = 'core' in newPlatform ? newPlatform.core : newPlatform; - const packageInfo = newPlatform.env.packageInfo; - - angularModule - .value('kbnVersion', packageInfo.version) - .value('buildNum', packageInfo.buildNum) - .value('buildSha', packageInfo.buildSha) - .value('esUrl', getEsUrl(core)) - .value('uiCapabilities', core.application.capabilities) - .config(setupCompileProvider(newPlatform.env.mode.dev)) - .config(setupLocationProvider()) - .config($setupXsrfRequestInterceptor(packageInfo.version)) - .run(capture$httpLoadingCount(core)) - .run(digestOnHashChange(getHistory)) - .run($setupBreadcrumbsAutoClear(core, isLocalAngular)) - .run($setupBadgeAutoClear(core, isLocalAngular)) - .run($setupHelpExtensionAutoClear(core, isLocalAngular)) - .run($setupUICapabilityRedirect(core)); -}; - -const getEsUrl = (newPlatform: CoreStart) => { - const a = document.createElement('a'); - a.href = newPlatform.http.basePath.prepend('/elasticsearch'); - const protocolPort = /https/.test(a.protocol) ? 443 : 80; - const port = a.port || protocolPort; - return { - host: a.hostname, - port, - protocol: a.protocol, - pathname: a.pathname, - }; -}; - -const digestOnHashChange = (getHistory?: () => History) => ($rootScope: IRootScopeService) => { - if (!getHistory) return; - const unlisten = getHistory().listen(() => { - // dispatch synthetic hash change event to update hash history objects and angular routing - // this is necessary because hash updates triggered by using popState won't trigger this event naturally. - // this has to happen in the next tick to not change the existing timing of angular digest cycles. - setTimeout(() => { - window.dispatchEvent(new HashChangeEvent('hashchange')); - }, 0); - }); - $rootScope.$on('$destroy', unlisten); -}; - -const setupCompileProvider = (devMode: boolean) => ($compileProvider: ICompileProvider) => { - if (!devMode) { - $compileProvider.debugInfoEnabled(false); - } -}; - -const setupLocationProvider = () => ($locationProvider: ILocationProvider) => { - $locationProvider.html5Mode({ - enabled: false, - requireBase: false, - rewriteLinks: false, - }); - - $locationProvider.hashPrefix(''); -}; - -export const $setupXsrfRequestInterceptor = (version: string) => { - // Configure jQuery prefilter - $.ajaxPrefilter(({ kbnXsrfToken = true }: any, originalOptions, jqXHR) => { - if (kbnXsrfToken) { - jqXHR.setRequestHeader('kbn-version', version); - } - }); - - return ($httpProvider: IHttpProvider) => { - // Configure $httpProvider interceptor - $httpProvider.interceptors.push(() => { - return { - request(opts) { - const { kbnXsrfToken = true } = opts as any; - if (kbnXsrfToken) { - set(opts, ['headers', 'kbn-version'], version); - } - return opts; - }, - }; - }); - }; -}; - -/** - * Injected into angular module by ui/chrome angular integration - * and adds a root-level watcher that will capture the count of - * active $http requests on each digest loop and expose the count to - * the core.loadingCount api - */ -const capture$httpLoadingCount = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $http: IHttpService) => { - newPlatform.http.addLoadingCountSource( - new Rx.Observable((observer) => { - const unwatch = $rootScope.$watch(() => { - const reqs = $http.pendingRequests || []; - observer.next(reqs.filter((req) => !isSystemApiRequest(req)).length); - }); - - return unwatch; - }) - ); - }; - -/** - * integrates with angular to automatically redirect to home if required - * capability is not met - */ -const $setupUICapabilityRedirect = - (newPlatform: CoreStart) => ($rootScope: IRootScopeService, $injector: any) => { - const isKibanaAppRoute = window.location.pathname.endsWith('/app/kibana'); - // this feature only works within kibana app for now after everything is - // switched to the application service, this can be changed to handle all - // apps. - if (!isKibanaAppRoute) { - return; - } - $rootScope.$on( - '$routeChangeStart', - (event, { $$route: route }: { $$route?: RouteConfiguration } = {}) => { - if (!route || !route.requireUICapability) { - return; - } - - if (!get(newPlatform.application.capabilities, route.requireUICapability)) { - $injector.get('$location').url('/home'); - event.preventDefault(); - } - } - ); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the breadcrumbs if we switch to a Kibana app that does not use breadcrumbs correctly - */ -const $setupBreadcrumbsAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the breadcrumbs between angular route changes. - let breadcrumbSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - // reset breadcrumbSetSinceRouteChange any time the breadcrumbs change, even - // if it was done directly through the new platform - newPlatform.chrome.getBreadcrumbs$().subscribe({ - next() { - breadcrumbSetSinceRouteChange = true; - }, - }); - - $rootScope.$on('$routeChangeStart', () => { - breadcrumbSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (breadcrumbSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const k7BreadcrumbsProvider = current.k7Breadcrumbs; - if (!k7BreadcrumbsProvider) { - newPlatform.chrome.setBreadcrumbs([]); - return; - } - - try { - newPlatform.chrome.setBreadcrumbs($injector.invoke(k7BreadcrumbsProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the badge if we switch to a Kibana app that does not use the badge correctly - */ -const $setupBadgeAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - // A flag used to determine if we should automatically - // clear the badge between angular route changes. - let badgeSetSinceRouteChange = false; - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - badgeSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (badgeSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - const badgeProvider = current.badge; - if (!badgeProvider) { - newPlatform.chrome.setBadge(undefined); - return; - } - - try { - newPlatform.chrome.setBadge($injector.invoke(badgeProvider)); - } catch (error) { - if (isAngularHttpError(error)) { - error = formatAngularHttpError(error); - } - newPlatform.fatalErrors.add(error, 'location'); - } - }); - }; - -/** - * internal angular run function that will be called when angular bootstraps and - * lets us integrate with the angular router so that we can automatically clear - * the helpExtension if we switch to a Kibana app that does not set its own - * helpExtension - */ -const $setupHelpExtensionAutoClear = - (newPlatform: CoreStart, isLocalAngular: boolean) => - ($rootScope: IRootScopeService, $injector: any) => { - /** - * reset helpExtensionSetSinceRouteChange any time the helpExtension changes, even - * if it was done directly through the new platform - */ - let helpExtensionSetSinceRouteChange = false; - newPlatform.chrome.getHelpExtension$().subscribe({ - next() { - helpExtensionSetSinceRouteChange = true; - }, - }); - - const $route = $injector.has('$route') ? $injector.get('$route') : {}; - - $rootScope.$on('$routeChangeStart', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - helpExtensionSetSinceRouteChange = false; - }); - - $rootScope.$on('$routeChangeSuccess', () => { - if (isDummyRoute($route, isLocalAngular)) { - return; - } - const current = $route.current || {}; - - if (helpExtensionSetSinceRouteChange || (current.$$route && current.$$route.redirectTo)) { - return; - } - - newPlatform.chrome.setHelpExtension(current.helpExtension); - }); - }; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts b/x-pack/plugins/monitoring/public/angular/top_nav/index.ts deleted file mode 100644 index b3501e4cbad1..000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/index.ts +++ /dev/null @@ -1,10 +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. - */ - -export * from './angular_config'; -// @ts-ignore -export { createTopNavDirective, createTopNavHelper, loadKbnTopNavDirectives } from './kbn_top_nav'; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts deleted file mode 100644 index 0cff77241bb9..000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.d.ts +++ /dev/null @@ -1,16 +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 { Injectable, IDirectiveFactory, IScope, IAttributes, IController } from 'angular'; - -export const createTopNavDirective: Injectable< - IDirectiveFactory ->; -export const createTopNavHelper: ( - options: unknown -) => Injectable>; -export function loadKbnTopNavDirectives(navUi: unknown): void; diff --git a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js b/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js deleted file mode 100644 index 6edcca6aa714..000000000000 --- a/x-pack/plugins/monitoring/public/angular/top_nav/kbn_top_nav.js +++ /dev/null @@ -1,119 +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 angular from 'angular'; -import 'ngreact'; - -export function createTopNavDirective() { - return { - restrict: 'E', - template: '', - compile: (elem) => { - const child = document.createElement('kbn-top-nav-helper'); - - // Copy attributes to the child directive - for (const attr of elem[0].attributes) { - child.setAttribute(attr.name, attr.value); - } - - // Add a special attribute that will change every time that one - // of the config array's disableButton function return value changes. - child.setAttribute('disabled-buttons', 'disabledButtons'); - - // Append helper directive - elem.append(child); - - const linkFn = ($scope, _, $attr) => { - // Watch config changes - $scope.$watch( - () => { - const config = $scope.$eval($attr.config) || []; - return config.map((item) => { - // Copy key into id, as it's a reserved react propery. - // This is done for Angular directive backward compatibility. - // In React only id is recognized. - if (item.key && !item.id) { - item.id = item.key; - } - - // Watch the disableButton functions - if (typeof item.disableButton === 'function') { - return item.disableButton(); - } - return item.disableButton; - }); - }, - (newVal) => { - $scope.disabledButtons = newVal; - }, - true - ); - }; - - return linkFn; - }, - }; -} - -export const createTopNavHelper = - ({ TopNavMenu }) => - (reactDirective) => { - return reactDirective(TopNavMenu, [ - ['config', { watchDepth: 'value' }], - ['setMenuMountPoint', { watchDepth: 'reference' }], - ['disabledButtons', { watchDepth: 'reference' }], - - ['query', { watchDepth: 'reference' }], - ['savedQuery', { watchDepth: 'reference' }], - ['intl', { watchDepth: 'reference' }], - - ['onQuerySubmit', { watchDepth: 'reference' }], - ['onFiltersUpdated', { watchDepth: 'reference' }], - ['onRefreshChange', { watchDepth: 'reference' }], - ['onClearSavedQuery', { watchDepth: 'reference' }], - ['onSaved', { watchDepth: 'reference' }], - ['onSavedQueryUpdated', { watchDepth: 'reference' }], - ['onSavedQueryIdChange', { watchDepth: 'reference' }], - - ['indexPatterns', { watchDepth: 'collection' }], - ['filters', { watchDepth: 'collection' }], - - // All modifiers default to true. - // Set to false to hide subcomponents. - 'showSearchBar', - 'showQueryBar', - 'showQueryInput', - 'showSaveQuery', - 'showDatePicker', - 'showFilterBar', - - 'appName', - 'screenTitle', - 'dateRangeFrom', - 'dateRangeTo', - 'savedQueryId', - 'isRefreshPaused', - 'refreshInterval', - 'disableAutoFocus', - 'showAutoRefreshOnly', - - // temporary flag to use the stateful components - 'useDefaultBehaviors', - ]); - }; - -let isLoaded = false; - -export function loadKbnTopNavDirectives(navUi) { - if (!isLoaded) { - isLoaded = true; - angular - .module('kibana') - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navUi)); - } -} diff --git a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx index e6638b4c4fed..cc8619dbc7ad 100644 --- a/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx @@ -32,31 +32,8 @@ export const GlobalStateProvider: React.FC = ({ toasts, children, }) => { - // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed - const fakeAngularRootScope: Partial = { - $on: - (name: string, listener: (event: ng.IAngularEvent, ...args: any[]) => any): (() => void) => - () => {}, - $applyAsync: () => {}, - }; - - const fakeAngularLocation: Partial = { - search: () => { - return {} as any; - }, - replace: () => { - return {} as any; - }, - }; - const localState: State = {}; - const state = new GlobalState( - query, - toasts, - fakeAngularRootScope, - fakeAngularLocation, - localState as { [key: string]: unknown } - ); + const state = new GlobalState(query, toasts, localState as { [key: string]: unknown }); const initialState: any = state.getState(); for (const key in initialState) { diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 72d50aac1dbb..48484421839b 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -44,7 +44,7 @@ const angularNoop = () => { export interface IShims { toastNotifications: CoreStart['notifications']['toasts']; capabilities: CoreStart['application']['capabilities']; - getAngularInjector: typeof angularNoop | (() => angular.auto.IInjectorService); + getAngularInjector: typeof angularNoop; getBasePath: () => string; getInjected: (name: string, defaultValue?: unknown) => unknown; breadcrumbs: { @@ -73,23 +73,18 @@ export interface IShims { export class Legacy { private static _shims: IShims; - public static init( - { - core, - data, - isCloud, - triggersActionsUi, - usageCollection, - appMountParameters, - }: MonitoringStartPluginDependencies, - ngInjector?: angular.auto.IInjectorService - ) { + public static init({ + core, + data, + isCloud, + triggersActionsUi, + usageCollection, + appMountParameters, + }: MonitoringStartPluginDependencies) { this._shims = { toastNotifications: core.notifications.toasts, capabilities: core.application.capabilities, - getAngularInjector: ngInjector - ? (): angular.auto.IInjectorService => ngInjector - : angularNoop, + getAngularInjector: angularNoop, getBasePath: (): string => core.http.basePath.get(), getInjected: (name: string, defaultValue?: unknown): string | unknown => core.injectedMetadata.getInjectedVar(name, defaultValue), diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index 25086411c65a..8f89df732b80 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { Subscription } from 'rxjs'; import { History, createHashHistory } from 'history'; import { MonitoringStartPluginDependencies } from './types'; @@ -27,10 +26,6 @@ import { withNotifyOnErrors, } from '../../../../src/plugins/kibana_utils/public'; -interface Route { - params: { _g: unknown }; -} - interface RawObject { [key: string]: unknown; } @@ -57,7 +52,6 @@ export interface MonitoringAppStateTransitions { const GLOBAL_STATE_KEY = '_g'; const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); -// TODO: clean all angular references after angular is removed export class GlobalState { private readonly stateSyncRef: ISyncStateRef; private readonly stateContainer: StateContainer< @@ -70,13 +64,10 @@ export class GlobalState { private readonly timefilterRef: MonitoringStartPluginDependencies['data']['query']['timefilter']['timefilter']; private lastAssignedState: MonitoringAppState = {}; - private lastKnownGlobalState?: string; constructor( queryService: MonitoringStartPluginDependencies['data']['query'], toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], - rootScope: Partial, - ngLocation: Partial, externalState: RawObject ) { this.timefilterRef = queryService.timefilter.timefilter; @@ -102,9 +93,6 @@ export class GlobalState { this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { this.lastAssignedState = this.getState(); - if (!this.stateContainer.get() && this.lastKnownGlobalState) { - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); - } // TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged if (Legacy.isInitializated()) { @@ -112,15 +100,11 @@ export class GlobalState { } this.syncExternalState(externalState); - rootScope.$applyAsync?.(); }); this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); this.stateSyncRef.start(); - this.startHashSync(rootScope, ngLocation); this.lastAssignedState = this.getState(); - - rootScope.$on?.('$destroy', () => this.destroy()); } private syncExternalState(externalState: { [key: string]: unknown }) { @@ -137,24 +121,6 @@ export class GlobalState { } } - private startHashSync( - rootScope: Partial, - ngLocation: Partial - ) { - rootScope.$on?.( - '$routeChangeStart', - (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { - const currentGlobalState = oldState?.params?._g; - const nextGlobalState = newState?.params?._g; - if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { - newState.params._g = currentGlobalState; - ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); - } - this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; - } - ); - } - public setState(state?: { [key: string]: unknown }) { const currentAppState = this.getState(); const newAppState = { ...currentAppState, ...state }; From 70bd56a04fd30e0ac8fb0c55ddfec7e3c11d9349 Mon Sep 17 00:00:00 2001 From: Vadim Yakhin Date: Mon, 18 Oct 2021 14:50:59 -0700 Subject: [PATCH 05/53] [Fleet, App search] Add App Search ingestion methods to the unified integrations view (#115433) * Add a new category for Web crawler * Add App Search integrations * Fix isBeta flag for Web Crawler It's already GA --- .../custom_integrations/common/index.ts | 1 + .../enterprise_search/server/integrations.ts | 63 +++++++++++++++++++ 2 files changed, 64 insertions(+) diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 944ac6ba3e6e..98148bb22c81 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -49,6 +49,7 @@ export const INTEGRATION_CATEGORY_DISPLAY = { project_management: 'Project Management', software_development: 'Software Development', upload_file: 'Upload a file', + website_search: 'Website Search', }; /** diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts index 48909261243e..eee5cdc3aaec 100644 --- a/x-pack/plugins/enterprise_search/server/integrations.ts +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -301,4 +301,67 @@ export const registerEnterpriseSearchIntegrations = ( ...integration, }); }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_web_crawler', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.webCrawlerName', { + defaultMessage: 'Web Crawler', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.appSearch.integrations.webCrawlerDescription', + { + defaultMessage: "Add search to your website with App Search's web crawler.", + } + ), + categories: ['website_search'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=crawler', + icons: [ + { + type: 'eui', + src: 'globe', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_json', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonName', { + defaultMessage: 'JSON', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.jsonDescription', { + defaultMessage: 'Search over your JSON data with App Search.', + }), + categories: ['upload_file'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=json', + icons: [ + { + type: 'eui', + src: 'exportAction', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); + + customIntegrations.registerCustomIntegration({ + id: 'app_search_api', + title: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiName', { + defaultMessage: 'API', + }), + description: i18n.translate('xpack.enterpriseSearch.appSearch.integrations.apiDescription', { + defaultMessage: "Add search to your application with App Search's robust APIs.", + }), + categories: ['custom'], + uiInternalPath: '/app/enterprise_search/app_search/engines/new?method=api', + icons: [ + { + type: 'eui', + src: 'editorCodeBlock', + }, + ], + shipper: 'enterprise_search', + isBeta: false, + }); }; From 62f057dee12a9f2f2693c249ddbf3f5e93ee6ca8 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 19 Oct 2021 00:12:07 +0200 Subject: [PATCH 06/53] [ML] APM Correlations: Get trace samples tab overall distribution via APM endpoint. (#114615) This creates an APM API endpoint that fetches data for the latency distribution chart in the trace samples tab on the transactions page. Previously, this data was fetched via the custom Kibana search strategies used for APM Correlations which causes issues in load balancing setups. --- .../failed_transactions_correlations/types.ts | 13 +- .../latency_correlations/types.ts | 12 +- .../apm/common/search_strategies/types.ts | 12 +- .../latency_correlations.test.tsx | 5 +- .../distribution/index.test.tsx | 59 +++------ .../distribution/index.tsx | 79 +++++++++--- .../apm/public/hooks/use_search_strategy.ts | 10 +- .../get_overall_latency_distribution.ts | 121 ++++++++++++++++++ .../latency/get_percentile_threshold_value.ts | 53 ++++++++ .../plugins/apm/server/lib/latency/types.ts | 23 ++++ ...ransactions_correlations_search_service.ts | 31 ++--- .../failed_transactions_correlations/index.ts | 6 +- .../latency_correlations/index.ts | 6 +- .../latency_correlations_search_service.ts | 31 ++--- .../field_stats/get_field_stats.test.ts | 4 +- .../queries/get_query_with_params.test.ts | 12 +- .../queries/get_query_with_params.ts | 17 +-- .../queries/get_request_base.test.ts | 4 + .../queries/query_correlation.test.ts | 4 +- .../queries/query_field_candidates.test.ts | 4 +- .../queries/query_field_value_pairs.test.ts | 4 +- .../queries/query_fractions.test.ts | 4 +- .../queries/query_histogram.test.ts | 4 +- .../query_histogram_range_steps.test.ts | 4 +- .../queries/query_histogram_range_steps.ts | 6 +- .../query_histograms_generator.test.ts | 4 +- .../queries/query_percentiles.test.ts | 4 +- .../queries/query_ranges.test.ts | 4 +- .../search_strategy_provider.test.ts | 4 +- .../search_strategy_provider.ts | 104 +++++++++++---- .../get_global_apm_server_route_repository.ts | 2 + .../apm/server/routes/latency_distribution.ts | 63 +++++++++ .../tests/correlations/failed_transactions.ts | 4 +- .../tests/correlations/latency.ts | 23 ++-- .../test/apm_api_integration/tests/index.ts | 4 + .../latency_overall_distribution.ts | 65 ++++++++++ 36 files changed, 590 insertions(+), 219 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts create mode 100644 x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts create mode 100644 x-pack/plugins/apm/server/lib/latency/types.ts create mode 100644 x-pack/plugins/apm/server/routes/latency_distribution.ts create mode 100644 x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts diff --git a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts index 266d7246c35d..28ce2ff24b96 100644 --- a/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/failed_transactions_correlations/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - FieldValuePair, - HistogramItem, - RawResponseBase, - SearchStrategyClientParams, -} from '../types'; +import { FieldValuePair, HistogramItem } from '../types'; import { FAILED_TRANSACTIONS_IMPACT_THRESHOLD } from './constants'; import { FieldStats } from '../field_stats_types'; @@ -33,11 +28,7 @@ export interface FailedTransactionsCorrelationsParams { percentileThreshold: number; } -export type FailedTransactionsCorrelationsRequestParams = - FailedTransactionsCorrelationsParams & SearchStrategyClientParams; - -export interface FailedTransactionsCorrelationsRawResponse - extends RawResponseBase { +export interface FailedTransactionsCorrelationsRawResponse { log: string[]; failedTransactionsCorrelations?: FailedTransactionsCorrelation[]; percentileThresholdValue?: number; diff --git a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts index 2eb2b3715945..ea74175a3dac 100644 --- a/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/latency_correlations/types.ts @@ -5,12 +5,7 @@ * 2.0. */ -import { - FieldValuePair, - HistogramItem, - RawResponseBase, - SearchStrategyClientParams, -} from '../types'; +import { FieldValuePair, HistogramItem } from '../types'; import { FieldStats } from '../field_stats_types'; export interface LatencyCorrelation extends FieldValuePair { @@ -33,10 +28,7 @@ export interface LatencyCorrelationsParams { analyzeCorrelations: boolean; } -export type LatencyCorrelationsRequestParams = LatencyCorrelationsParams & - SearchStrategyClientParams; - -export interface LatencyCorrelationsRawResponse extends RawResponseBase { +export interface LatencyCorrelationsRawResponse { log: string[]; overallHistogram?: HistogramItem[]; percentileThresholdValue?: number; diff --git a/x-pack/plugins/apm/common/search_strategies/types.ts b/x-pack/plugins/apm/common/search_strategies/types.ts index d7c6eab1f07c..ff925f70fc9b 100644 --- a/x-pack/plugins/apm/common/search_strategies/types.ts +++ b/x-pack/plugins/apm/common/search_strategies/types.ts @@ -31,16 +31,26 @@ export interface RawResponseBase { took: number; } -export interface SearchStrategyClientParams { +export interface SearchStrategyClientParamsBase { environment: string; kuery: string; serviceName?: string; transactionName?: string; transactionType?: string; +} + +export interface RawSearchStrategyClientParams + extends SearchStrategyClientParamsBase { start?: string; end?: string; } +export interface SearchStrategyClientParams + extends SearchStrategyClientParamsBase { + start: number; + end: number; +} + export interface SearchStrategyServerParams { index: string; includeFrozen?: boolean; diff --git a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx index 9956452c565b..918f94e64ef0 100644 --- a/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx +++ b/x-pack/plugins/apm/public/components/app/correlations/latency_correlations.test.tsx @@ -19,6 +19,7 @@ import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import type { LatencyCorrelationsRawResponse } from '../../../../common/search_strategies/latency_correlations/types'; +import type { RawResponseBase } from '../../../../common/search_strategies/types'; import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; import { @@ -34,7 +35,9 @@ function Wrapper({ dataSearchResponse, }: { children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; + dataSearchResponse: IKibanaSearchResponse< + LatencyCorrelationsRawResponse & RawResponseBase + >; }) { const mockDataSearch = jest.fn(() => of(dataSearchResponse)); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx index bd0ff4c87c3b..0e9639de4aa7 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.test.tsx @@ -8,43 +8,24 @@ import { render, screen, waitFor } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React, { ReactNode } from 'react'; -import { of } from 'rxjs'; import { CoreStart } from 'kibana/public'; import { merge } from 'lodash'; -import { dataPluginMock } from 'src/plugins/data/public/mocks'; -import type { IKibanaSearchResponse } from 'src/plugins/data/public'; import { EuiThemeProvider } from 'src/plugins/kibana_react/common'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; -import type { LatencyCorrelationsRawResponse } from '../../../../../common/search_strategies/latency_correlations/types'; import { MockUrlParamsContextProvider } from '../../../../context/url_params_context/mock_url_params_context_provider'; import { ApmPluginContextValue } from '../../../../context/apm_plugin/apm_plugin_context'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../../context/apm_plugin/mock_apm_plugin_context'; +import * as useFetcherModule from '../../../../hooks/use_fetcher'; import { fromQuery } from '../../../shared/Links/url_helpers'; import { getFormattedSelection, TransactionDistribution } from './index'; -function Wrapper({ - children, - dataSearchResponse, -}: { - children?: ReactNode; - dataSearchResponse: IKibanaSearchResponse; -}) { - const mockDataSearch = jest.fn(() => of(dataSearchResponse)); - - const dataPluginMockStart = dataPluginMock.createStartContract(); +function Wrapper({ children }: { children?: ReactNode }) { const KibanaReactContext = createKibanaReactContext({ - data: { - ...dataPluginMockStart, - search: { - ...dataPluginMockStart.search, - search: mockDataSearch, - }, - }, usageCollection: { reportUiCounter: () => {} }, } as Partial); @@ -105,18 +86,14 @@ describe('transaction_details/distribution', () => { describe('TransactionDistribution', () => { it('shows loading indicator when the service is running and returned no results yet', async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: {}, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.LOADING, + })); + render( - + { }); it("doesn't show loading indicator when the service isn't running", async () => { + jest.spyOn(useFetcherModule, 'useFetcher').mockImplementation(() => ({ + data: { percentileThresholdValue: 1234, overallHistogram: [] }, + refetch: () => {}, + status: useFetcherModule.FETCH_STATUS.SUCCESS, + })); + render( - + { + if (serviceName && environment && start && end) { + return callApmApi({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: { + query: { + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + percentileThreshold: DEFAULT_PERCENTILE_THRESHOLD, + }, + }, + }); + } + }, + [ + serviceName, + transactionName, + transactionType, + kuery, + environment, + start, + end, + ] ); + const overallHistogram = + data.overallHistogram === undefined && status !== FETCH_STATUS.LOADING + ? [] + : data.overallHistogram; + const hasData = + Array.isArray(overallHistogram) && overallHistogram.length > 0; + useEffect(() => { - if (isErrorMessage(progress.error)) { + if (isErrorMessage(error)) { notifications.toasts.addDanger({ title: i18n.translate( 'xpack.apm.transactionDetails.distribution.errorTitle', @@ -119,10 +156,10 @@ export function TransactionDistribution({ defaultMessage: 'An error occurred fetching the distribution', } ), - text: progress.error.toString(), + text: error.toString(), }); } - }, [progress.error, notifications.toasts]); + }, [error, notifications.toasts]); const trackApmEvent = useUiTracker({ app: 'apm' }); @@ -213,7 +250,7 @@ export function TransactionDistribution({ data={transactionDistributionChartData} markerCurrentTransaction={markerCurrentTransaction} markerPercentile={DEFAULT_PERCENTILE_THRESHOLD} - markerValue={response.percentileThresholdValue ?? 0} + markerValue={data.percentileThresholdValue ?? 0} onChartSelection={onTrackedChartSelection as BrushEndListener} hasData={hasData} selection={selection} diff --git a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts index ca8d28b106f8..275eddb68ae0 100644 --- a/x-pack/plugins/apm/public/hooks/use_search_strategy.ts +++ b/x-pack/plugins/apm/public/hooks/use_search_strategy.ts @@ -16,7 +16,7 @@ import { } from '../../../../../src/plugins/data/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import type { SearchStrategyClientParams } from '../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../common/search_strategies/types'; import type { RawResponseBase } from '../../common/search_strategies/types'; import type { LatencyCorrelationsParams, @@ -77,13 +77,15 @@ interface SearchStrategyReturnBase { export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_LATENCY_CORRELATIONS, searchStrategyParams: LatencyCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase; // Function overload for Failed Transactions Correlations export function useSearchStrategy( searchStrategyName: typeof APM_SEARCH_STRATEGIES.APM_FAILED_TRANSACTIONS_CORRELATIONS, searchStrategyParams: FailedTransactionsCorrelationsParams -): SearchStrategyReturnBase; +): SearchStrategyReturnBase< + FailedTransactionsCorrelationsRawResponse & RawResponseBase +>; export function useSearchStrategy< TRawResponse extends RawResponseBase, @@ -145,7 +147,7 @@ export function useSearchStrategy< // Submit the search request using the `data.search` service. searchSubscription$.current = data.search .search< - IKibanaSearchRequest, + IKibanaSearchRequest, IKibanaSearchResponse >(request, { strategy: searchStrategyName, diff --git a/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts new file mode 100644 index 000000000000..39470869488c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_overall_latency_distribution.ts @@ -0,0 +1,121 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { withApmSpan } from '../../utils/with_apm_span'; + +import { + getHistogramIntervalRequest, + getHistogramRangeSteps, +} from '../search_strategies/queries/query_histogram_range_steps'; +import { getTransactionDurationRangesRequest } from '../search_strategies/queries/query_ranges'; + +import { getPercentileThresholdValue } from './get_percentile_threshold_value'; +import type { + OverallLatencyDistributionOptions, + OverallLatencyDistributionResponse, +} from './types'; + +export async function getOverallLatencyDistribution( + options: OverallLatencyDistributionOptions +) { + return withApmSpan('get_overall_latency_distribution', async () => { + const overallLatencyDistribution: OverallLatencyDistributionResponse = { + log: [], + }; + + const { setup, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + // #1: get 95th percentile to be displayed as a marker in the log log chart + overallLatencyDistribution.percentileThresholdValue = + await getPercentileThresholdValue(options); + + // finish early if we weren't able to identify the percentileThresholdValue. + if (!overallLatencyDistribution.percentileThresholdValue) { + return overallLatencyDistribution; + } + + // #2: get histogram range steps + const steps = 100; + + const { body: histogramIntervalRequestBody } = + getHistogramIntervalRequest(params); + + const histogramIntervalResponse = (await apmEventClient.search( + 'get_histogram_interval', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: histogramIntervalRequestBody, + } + )) as { + aggregations?: { + transaction_duration_min: estypes.AggregationsValueAggregate; + transaction_duration_max: estypes.AggregationsValueAggregate; + }; + hits: { total: estypes.SearchTotalHits }; + }; + + if ( + !histogramIntervalResponse.aggregations || + histogramIntervalResponse.hits.total.value === 0 + ) { + return overallLatencyDistribution; + } + + const min = + histogramIntervalResponse.aggregations.transaction_duration_min.value; + const max = + histogramIntervalResponse.aggregations.transaction_duration_max.value * 2; + + const histogramRangeSteps = getHistogramRangeSteps(min, max, steps); + + // #3: get histogram chart data + const { body: transactionDurationRangesRequestBody } = + getTransactionDurationRangesRequest(params, histogramRangeSteps); + + const transactionDurationRangesResponse = (await apmEventClient.search( + 'get_transaction_duration_ranges', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationRangesRequestBody, + } + )) as { + aggregations?: { + logspace_ranges: estypes.AggregationsMultiBucketAggregate<{ + from: number; + doc_count: number; + }>; + }; + }; + + if (!transactionDurationRangesResponse.aggregations) { + return overallLatencyDistribution; + } + + overallLatencyDistribution.overallHistogram = + transactionDurationRangesResponse.aggregations.logspace_ranges.buckets + .map((d) => ({ + key: d.from, + doc_count: d.doc_count, + })) + .filter((d) => d.key !== undefined); + + return overallLatencyDistribution; + }); +} diff --git a/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts new file mode 100644 index 000000000000..0d417a370e0b --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/get_percentile_threshold_value.ts @@ -0,0 +1,53 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; + +import { ProcessorEvent } from '../../../common/processor_event'; + +import { getTransactionDurationPercentilesRequest } from '../search_strategies/queries/query_percentiles'; + +import type { OverallLatencyDistributionOptions } from './types'; + +export async function getPercentileThresholdValue( + options: OverallLatencyDistributionOptions +) { + const { setup, percentileThreshold, ...rawParams } = options; + const { apmEventClient } = setup; + const params = { + // pass on an empty index because we're using only the body attribute + // of the request body getters we're reusing from search strategies. + index: '', + ...rawParams, + }; + + const { body: transactionDurationPercentilesRequestBody } = + getTransactionDurationPercentilesRequest(params, [percentileThreshold]); + + const transactionDurationPercentilesResponse = (await apmEventClient.search( + 'get_transaction_duration_percentiles', + { + // TODO: add support for metrics + apm: { events: [ProcessorEvent.transaction] }, + body: transactionDurationPercentilesRequestBody, + } + )) as { + aggregations?: { + transaction_duration_percentiles: estypes.AggregationsTDigestPercentilesAggregate; + }; + }; + + if (!transactionDurationPercentilesResponse.aggregations) { + return; + } + + const percentilesResponseThresholds = + transactionDurationPercentilesResponse.aggregations + .transaction_duration_percentiles?.values ?? {}; + + return percentilesResponseThresholds[`${percentileThreshold}.0`]; +} diff --git a/x-pack/plugins/apm/server/lib/latency/types.ts b/x-pack/plugins/apm/server/lib/latency/types.ts new file mode 100644 index 000000000000..8dad1a39bd15 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/latency/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 { Setup } from '../helpers/setup_request'; +import { CorrelationsOptions } from '../search_strategies/queries/get_filters'; + +export interface OverallLatencyDistributionOptions extends CorrelationsOptions { + percentileThreshold: number; + setup: Setup; +} + +export interface OverallLatencyDistributionResponse { + log: string[]; + percentileThresholdValue?: number; + overallHistogram?: Array<{ + key: number; + doc_count: number; + }>; +} diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts index af5e535abdc3..efc28ce98e5e 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/failed_transactions_correlations_search_service.ts @@ -9,17 +9,15 @@ import { chunk } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - import { EVENT_OUTCOME } from '../../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../../common/event_outcome'; -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - FailedTransactionsCorrelationsRequestParams, + SearchStrategyClientParams, + SearchStrategyServerParams, + RawResponseBase, +} from '../../../../common/search_strategies/types'; +import type { + FailedTransactionsCorrelationsParams, FailedTransactionsCorrelationsRawResponse, } from '../../../../common/search_strategies/failed_transactions_correlations/types'; import type { ApmIndicesConfig } from '../../settings/apm_indices/get_apm_indices'; @@ -38,22 +36,18 @@ import { failedTransactionsCorrelationsSearchServiceStateProvider } from './fail import { ERROR_CORRELATION_THRESHOLD } from '../constants'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type FailedTransactionsCorrelationsSearchServiceProvider = +type FailedTransactionsCorrelationsSearchServiceProvider = SearchServiceProvider< - FailedTransactionsCorrelationsRequestParams, - FailedTransactionsCorrelationsRawResponse + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase >; -export type FailedTransactionsCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse ->; - export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: FailedTransactionsCorrelationsRequestParams, + searchServiceParams: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -63,7 +57,8 @@ export const failedTransactionsCorrelationsSearchServiceProvider: FailedTransact async function fetchErrorCorrelations() { try { const indices = await getApmIndices(); - const params: FailedTransactionsCorrelationsRequestParams & + const params: FailedTransactionsCorrelationsParams & + SearchStrategyClientParams & SearchStrategyServerParams = { ...searchServiceParams, index: indices.transaction, diff --git a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts index ec91165cb481..4763cd994d30 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/failed_transactions_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - failedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations_search_service'; +export { failedTransactionsCorrelationsSearchServiceProvider } from './failed_transactions_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts index 073bb122896f..040aa5a7e424 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/index.ts @@ -5,8 +5,4 @@ * 2.0. */ -export { - latencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations_search_service'; +export { latencyCorrelationsSearchServiceProvider } from './latency_correlations_search_service'; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts index 4862f7dd1de1..f170818d018d 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/latency_correlations/latency_correlations_search_service.ts @@ -8,15 +8,13 @@ import { range } from 'lodash'; import type { ElasticsearchClient } from 'src/core/server'; -import type { ISearchStrategy } from '../../../../../../../src/plugins/data/server'; -import { - IKibanaSearchRequest, - IKibanaSearchResponse, -} from '../../../../../../../src/plugins/data/common'; - -import type { SearchStrategyServerParams } from '../../../../common/search_strategies/types'; import type { - LatencyCorrelationsRequestParams, + RawResponseBase, + SearchStrategyClientParams, + SearchStrategyServerParams, +} from '../../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, LatencyCorrelationsRawResponse, } from '../../../../common/search_strategies/latency_correlations/types'; @@ -38,21 +36,16 @@ import type { SearchServiceProvider } from '../search_strategy_provider'; import { latencyCorrelationsSearchServiceStateProvider } from './latency_correlations_search_service_state'; import { fetchFieldsStats } from '../queries/field_stats/get_fields_stats'; -export type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< - LatencyCorrelationsRequestParams, - LatencyCorrelationsRawResponse ->; - -export type LatencyCorrelationsSearchStrategy = ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse +type LatencyCorrelationsSearchServiceProvider = SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase >; export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearchServiceProvider = ( esClient: ElasticsearchClient, getApmIndices: () => Promise, - searchServiceParams: LatencyCorrelationsRequestParams, + searchServiceParams: LatencyCorrelationsParams & SearchStrategyClientParams, includeFrozen: boolean ) => { const { addLogMessage, getLogMessages } = searchServiceLogProvider(); @@ -61,7 +54,9 @@ export const latencyCorrelationsSearchServiceProvider: LatencyCorrelationsSearch async function fetchCorrelations() { let params: - | (LatencyCorrelationsRequestParams & SearchStrategyServerParams) + | (LatencyCorrelationsParams & + SearchStrategyClientParams & + SearchStrategyServerParams) | undefined; try { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts index deb89ace47c5..d3cee1c4ca59 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/field_stats/get_field_stats.test.ts @@ -15,8 +15,8 @@ import { fetchFieldsStats } from './get_fields_stats'; const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts index c77b4df78f86..9d0441e51319 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.test.ts @@ -14,8 +14,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', @@ -45,8 +45,8 @@ describe('correlations', () => { index: 'apm-*', serviceName: 'actualServiceName', transactionName: 'actualTransactionName', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, environment: 'dev', kuery: '', includeFrozen: false, @@ -93,8 +93,8 @@ describe('correlations', () => { const query = getQueryWithParams({ params: { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts index f00c89503f10..31a98b0a6bb1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_query_with_params.ts @@ -6,15 +6,10 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import { getOrElse } from 'fp-ts/lib/Either'; -import { pipe } from 'fp-ts/lib/pipeable'; -import * as t from 'io-ts'; -import { failure } from 'io-ts/lib/PathReporter'; import type { FieldValuePair, SearchStrategyParams, } from '../../../../common/search_strategies/types'; -import { rangeRt } from '../../../routes/default_api_types'; import { getCorrelationsFilters } from './get_filters'; export const getTermsQuery = ({ fieldName, fieldValue }: FieldValuePair) => { @@ -36,22 +31,14 @@ export const getQueryWithParams = ({ params, termFilters }: QueryParams) => { transactionName, } = params; - // converts string based start/end to epochmillis - const decodedRange = pipe( - rangeRt.decode({ start, end }), - getOrElse((errors) => { - throw new Error(failure(errors).join('\n')); - }) - ); - const correlationFilters = getCorrelationsFilters({ environment, kuery, serviceName, transactionType, transactionName, - start: decodedRange.start, - end: decodedRange.end, + start, + end, }); return { diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts index fd5f52207d4c..eb771e1e1aaf 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/get_request_base.test.ts @@ -16,6 +16,8 @@ describe('correlations', () => { includeFrozen: true, environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', @@ -29,6 +31,8 @@ describe('correlations', () => { index: 'apm-*', environment: ENVIRONMENT_ALL.value, kuery: '', + start: 1577836800000, + end: 1609459200000, }); expect(requestBase).toEqual({ index: 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts index fc2dacce61a7..40fcc1744449 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_correlation.test.ts @@ -18,8 +18,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts index 6e0521ac1a00..bae42666e6db 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_candidates.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts index 9ffbf6b2ce18..ab7a0b4e0207 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_field_value_pairs.test.ts @@ -20,8 +20,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts index daf6b368c78b..9c704ef7b489 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_fractions.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts index 7ecb1d2d8a33..7cc6106f671a 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts index ffc86c7ef6c3..41a2fa9a5039 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts index 790919d19302..439bb9e4b9cd 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histogram_range_steps.ts @@ -17,7 +17,11 @@ import type { SearchStrategyParams } from '../../../../common/search_strategies/ import { getQueryWithParams } from './get_query_with_params'; import { getRequestBase } from './get_request_base'; -const getHistogramRangeSteps = (min: number, max: number, steps: number) => { +export const getHistogramRangeSteps = ( + min: number, + max: number, + steps: number +) => { // A d3 based scale function as a helper to get equally distributed bins on a log scale. // We round the final values because the ES range agg we use won't accept numbers with decimals for `transaction.duration.us`. const logFn = scaleLog().domain([min, max]).range([1, steps]); diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts index 375e32b1472c..00e8c26497eb 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_histograms_generator.test.ts @@ -17,8 +17,8 @@ import { fetchTransactionDurationHistograms } from './query_histograms_generator const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts index ce86ffd9654e..57e3e6cadb9b 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_percentiles.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts index e210eb7d41e7..7d67e80ae339 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/queries/query_ranges.test.ts @@ -17,8 +17,8 @@ import { const params = { index: 'apm-*', - start: '2020', - end: '2021', + start: 1577836800000, + end: 1609459200000, includeFrozen: false, environment: ENVIRONMENT_ALL.value, kuery: '', diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts index 8a9d04df3203..034bd2a60ad1 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.test.ts @@ -13,7 +13,7 @@ import { IKibanaSearchRequest } from '../../../../../../src/plugins/data/common' import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import type { LatencyCorrelationsParams } from '../../../common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../common/search_strategies/types'; import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; @@ -112,7 +112,7 @@ describe('APM Correlations search strategy', () => { let mockDeps: SearchStrategyDependencies; let params: Required< IKibanaSearchRequest< - LatencyCorrelationsParams & SearchStrategyClientParams + LatencyCorrelationsParams & RawSearchStrategyClientParams > >['params']; diff --git a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts index cec10294460b..8035e9e4d97c 100644 --- a/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts +++ b/x-pack/plugins/apm/server/lib/search_strategies/search_strategy_provider.ts @@ -7,6 +7,10 @@ import uuid from 'uuid'; import { of } from 'rxjs'; +import { getOrElse } from 'fp-ts/lib/Either'; +import { pipe } from 'fp-ts/lib/pipeable'; +import * as t from 'io-ts'; +import { failure } from 'io-ts/lib/PathReporter'; import type { ElasticsearchClient } from 'src/core/server'; @@ -16,18 +20,21 @@ import { IKibanaSearchResponse, } from '../../../../../../src/plugins/data/common'; -import type { SearchStrategyClientParams } from '../../../common/search_strategies/types'; -import type { RawResponseBase } from '../../../common/search_strategies/types'; -import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; - import type { - LatencyCorrelationsSearchServiceProvider, - LatencyCorrelationsSearchStrategy, -} from './latency_correlations'; + RawResponseBase, + RawSearchStrategyClientParams, + SearchStrategyClientParams, +} from '../../../common/search_strategies/types'; +import type { + LatencyCorrelationsParams, + LatencyCorrelationsRawResponse, +} from '../../../common/search_strategies/latency_correlations/types'; import type { - FailedTransactionsCorrelationsSearchServiceProvider, - FailedTransactionsCorrelationsSearchStrategy, -} from './failed_transactions_correlations'; + FailedTransactionsCorrelationsParams, + FailedTransactionsCorrelationsRawResponse, +} from '../../../common/search_strategies/failed_transactions_correlations/types'; +import { rangeRt } from '../../routes/default_api_types'; +import type { ApmIndicesConfig } from '../settings/apm_indices/get_apm_indices'; interface SearchServiceState { cancel: () => void; @@ -56,35 +63,50 @@ export type SearchServiceProvider< // Failed Transactions Correlations function overload export function searchStrategyProvider( - searchServiceProvider: FailedTransactionsCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + FailedTransactionsCorrelationsParams & SearchStrategyClientParams, + FailedTransactionsCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): FailedTransactionsCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse< + FailedTransactionsCorrelationsRawResponse & RawResponseBase + > +>; // Latency Correlations function overload export function searchStrategyProvider( - searchServiceProvider: LatencyCorrelationsSearchServiceProvider, + searchServiceProvider: SearchServiceProvider< + LatencyCorrelationsParams & SearchStrategyClientParams, + LatencyCorrelationsRawResponse & RawResponseBase + >, getApmIndices: () => Promise, includeFrozen: boolean -): LatencyCorrelationsSearchStrategy; +): ISearchStrategy< + IKibanaSearchRequest< + LatencyCorrelationsParams & RawSearchStrategyClientParams + >, + IKibanaSearchResponse +>; -export function searchStrategyProvider< - TSearchStrategyClientParams extends SearchStrategyClientParams, - TRawResponse extends RawResponseBase ->( +export function searchStrategyProvider( searchServiceProvider: SearchServiceProvider< - TSearchStrategyClientParams, - TRawResponse + TRequestParams & SearchStrategyClientParams, + TResponseParams & RawResponseBase >, getApmIndices: () => Promise, includeFrozen: boolean ): ISearchStrategy< - IKibanaSearchRequest, - IKibanaSearchResponse + IKibanaSearchRequest, + IKibanaSearchResponse > { const searchServiceMap = new Map< string, - GetSearchServiceState + GetSearchServiceState >(); return { @@ -93,9 +115,21 @@ export function searchStrategyProvider< throw new Error('Invalid request parameters.'); } + const { start: startString, end: endString } = request.params; + + // converts string based start/end to epochmillis + const decodedRange = pipe( + rangeRt.decode({ start: startString, end: endString }), + getOrElse((errors) => { + throw new Error(failure(errors).join('\n')); + }) + ); + // The function to fetch the current state of the search service. // This will be either an existing service for a follow up fetch or a new one for new requests. - let getSearchServiceState: GetSearchServiceState; + let getSearchServiceState: GetSearchServiceState< + TResponseParams & RawResponseBase + >; // If the request includes an ID, we require that the search service already exists // otherwise we throw an error. The client should never poll a service that's been cancelled or finished. @@ -111,10 +145,30 @@ export function searchStrategyProvider< getSearchServiceState = existingGetSearchServiceState; } else { + const { + start, + end, + environment, + kuery, + serviceName, + transactionName, + transactionType, + ...requestParams + } = request.params; + getSearchServiceState = searchServiceProvider( deps.esClient.asCurrentUser, getApmIndices, - request.params as TSearchStrategyClientParams, + { + environment, + kuery, + serviceName, + transactionName, + transactionType, + start: decodedRange.start, + end: decodedRange.end, + ...(requestParams as unknown as TRequestParams), + }, includeFrozen ); } diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 472e46fecfa1..3fa6152d953f 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -17,6 +17,7 @@ import { environmentsRouteRepository } from './environments'; import { errorsRouteRepository } from './errors'; import { apmFleetRouteRepository } from './fleet'; import { indexPatternRouteRepository } from './index_pattern'; +import { latencyDistributionRouteRepository } from './latency_distribution'; import { metricsRouteRepository } from './metrics'; import { observabilityOverviewRouteRepository } from './observability_overview'; import { rumRouteRepository } from './rum_client'; @@ -41,6 +42,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(indexPatternRouteRepository) .merge(environmentsRouteRepository) .merge(errorsRouteRepository) + .merge(latencyDistributionRouteRepository) .merge(metricsRouteRepository) .merge(observabilityOverviewRouteRepository) .merge(rumRouteRepository) diff --git a/x-pack/plugins/apm/server/routes/latency_distribution.ts b/x-pack/plugins/apm/server/routes/latency_distribution.ts new file mode 100644 index 000000000000..ea921a7f4838 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/latency_distribution.ts @@ -0,0 +1,63 @@ +/* + * 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 t from 'io-ts'; +import { toNumberRt } from '@kbn/io-ts-utils'; +import { getOverallLatencyDistribution } from '../lib/latency/get_overall_latency_distribution'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { environmentRt, kueryRt, rangeRt } from './default_api_types'; + +const latencyOverallDistributionRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/latency/overall_distribution', + params: t.type({ + query: t.intersection([ + t.partial({ + serviceName: t.string, + transactionName: t.string, + transactionType: t.string, + }), + environmentRt, + kueryRt, + rangeRt, + t.type({ + percentileThreshold: toNumberRt, + }), + ]), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + } = resources.params.query; + + return getOverallLatencyDistribution({ + environment, + kuery, + serviceName, + transactionType, + transactionName, + start, + end, + percentileThreshold, + setup, + }); + }, +}); + +export const latencyDistributionRouteRepository = + createApmServerRouteRepository().add(latencyOverallDistributionRoute); diff --git a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts index 6e2025a7fa2c..3388d5b4aa37 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/failed_transactions.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { FailedTransactionsCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/failed_transactions_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -23,7 +23,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const getRequestBody = () => { const request: IKibanaSearchRequest< - FailedTransactionsCorrelationsParams & SearchStrategyClientParams + FailedTransactionsCorrelationsParams & RawSearchStrategyClientParams > = { params: { environment: 'ENVIRONMENT_ALL', diff --git a/x-pack/test/apm_api_integration/tests/correlations/latency.ts b/x-pack/test/apm_api_integration/tests/correlations/latency.ts index 99aee770c625..75a4edd447c7 100644 --- a/x-pack/test/apm_api_integration/tests/correlations/latency.ts +++ b/x-pack/test/apm_api_integration/tests/correlations/latency.ts @@ -10,7 +10,7 @@ import expect from '@kbn/expect'; import { IKibanaSearchRequest } from '../../../../../src/plugins/data/common'; import type { LatencyCorrelationsParams } from '../../../../plugins/apm/common/search_strategies/latency_correlations/types'; -import type { SearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; +import type { RawSearchStrategyClientParams } from '../../../../plugins/apm/common/search_strategies/types'; import { APM_SEARCH_STRATEGIES } from '../../../../plugins/apm/common/search_strategies/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -22,16 +22,17 @@ export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('legacySupertestAsApmReadUser'); const getRequestBody = () => { - const request: IKibanaSearchRequest = { - params: { - environment: 'ENVIRONMENT_ALL', - start: '2020', - end: '2021', - kuery: '', - percentileThreshold: 95, - analyzeCorrelations: true, - }, - }; + const request: IKibanaSearchRequest = + { + params: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: 95, + analyzeCorrelations: true, + }, + }; return { batch: [ diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 09f4e2596ea4..f68a49658f2e 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -175,6 +175,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./transactions/error_rate')); }); + describe('transactions/latency_overall_distribution', function () { + loadTestFile(require.resolve('./transactions/latency_overall_distribution')); + }); + describe('transactions/latency', function () { loadTestFile(require.resolve('./transactions/latency')); }); diff --git a/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts new file mode 100644 index 000000000000..c915ac8911e3 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/transactions/latency_overall_distribution.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + + const endpoint = 'GET /internal/apm/latency/overall_distribution'; + + // This matches the parameters used for the other tab's search strategy approach in `../correlations/*`. + const getOptions = () => ({ + params: { + query: { + environment: 'ENVIRONMENT_ALL', + start: '2020', + end: '2021', + kuery: '', + percentileThreshold: '95', + }, + }, + }); + + registry.when( + 'latency overall distribution without data', + { config: 'trial', archives: [] }, + () => { + it('handles the empty state', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.be(200); + expect(response.body?.percentileThresholdValue).to.be(undefined); + expect(response.body?.overallHistogram?.length).to.be(undefined); + }); + } + ); + + registry.when( + 'latency overall distribution with data and default args', + // This uses the same archive used for the other tab's search strategy approach in `../correlations/*`. + { config: 'trial', archives: ['8.0.0'] }, + () => { + it('returns percentileThresholdValue and overall histogram', async () => { + const response = await apmApiClient.readUser({ + endpoint, + ...getOptions(), + }); + + expect(response.status).to.eql(200); + // This matches the values returned for the other tab's search strategy approach in `../correlations/*`. + expect(response.body?.percentileThresholdValue).to.be(1309695.875); + expect(response.body?.overallHistogram?.length).to.be(101); + }); + } + ); +} From 512d59467bcdbbf52d1b9038f1d6a1d1a2e0b964 Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Mon, 18 Oct 2021 18:16:20 -0400 Subject: [PATCH 07/53] [Uptime] redirect Uptime tutorials to the Elastic Synthetics Integration (#115229) * redirect uptime tutorials * adjust tests and aria labels --- .../shared/add_data_buttons/synthetics_add_data.tsx | 4 ++-- .../observability/public/pages/overview/empty_section.ts | 2 +- x-pack/plugins/uptime/public/apps/use_no_data_config.ts | 4 ++-- .../components/common/header/action_menu_content.test.tsx | 6 ++++-- .../public/components/common/header/action_menu_content.tsx | 6 ++++-- x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx | 2 +- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx index af91624769e6..d852d6fdb9a3 100644 --- a/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx +++ b/x-pack/plugins/observability/public/components/shared/add_data_buttons/synthetics_add_data.tsx @@ -16,9 +16,9 @@ export function SyntheticsAddData() { return ( diff --git a/x-pack/plugins/observability/public/pages/overview/empty_section.ts b/x-pack/plugins/observability/public/pages/overview/empty_section.ts index 2747b2ecdebc..f249a820a60a 100644 --- a/x-pack/plugins/observability/public/pages/overview/empty_section.ts +++ b/x-pack/plugins/observability/public/pages/overview/empty_section.ts @@ -69,7 +69,7 @@ export const getEmptySections = ({ core }: { core: CoreStart }): ISection[] => { linkTitle: i18n.translate('xpack.observability.emptySection.apps.uptime.link', { defaultMessage: 'Install Heartbeat', }), - href: core.http.basePath.prepend('/app/home#/tutorial/uptimeMonitors'), + href: core.http.basePath.prepend('/app/integrations/detail/synthetics/overview'), }, { id: 'ux', diff --git a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts index dc00a25e3a11..6e73a6d5e826 100644 --- a/x-pack/plugins/uptime/public/apps/use_no_data_config.ts +++ b/x-pack/plugins/uptime/public/apps/use_no_data_config.ts @@ -31,13 +31,13 @@ export function useNoDataConfig(): KibanaPageTemplateProps['noDataConfig'] { actions: { beats: { title: i18n.translate('xpack.uptime.noDataConfig.beatsCard.title', { - defaultMessage: 'Add monitors with Heartbeat', + defaultMessage: 'Add monitors with the Elastic Synthetics integration', }), description: i18n.translate('xpack.uptime.noDataConfig.beatsCard.description', { defaultMessage: 'Proactively monitor the availability of your sites and services. Receive alerts and resolve issues faster to optimize your users experience.', }), - href: basePath + `/app/home#/tutorial/uptimeMonitors`, + href: basePath + `/app/integrations/detail/synthetics/overview`, }, }, docsLink: docLinks!.links.observability.guide, diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx index 0265588c3fde..76b9378ca4ff 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.test.tsx @@ -45,11 +45,13 @@ describe('ActionMenuContent', () => { it('renders Add Data link', () => { const { getByLabelText, getByText } = render(); - const addDataAnchor = getByLabelText('Navigate to a tutorial about adding Uptime data'); + const addDataAnchor = getByLabelText( + 'Navigate to the Elastic Synthetics integration to add Uptime data' + ); // this href value is mocked, so it doesn't correspond to the real link // that Kibana core services will provide - expect(addDataAnchor.getAttribute('href')).toBe('/home#/tutorial/uptimeMonitors'); + expect(addDataAnchor.getAttribute('href')).toBe('/integrations/detail/synthetics/overview'); expect(getByText('Add data')); }); }); diff --git a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx index bcfbf18c93cf..789953258750 100644 --- a/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx +++ b/x-pack/plugins/uptime/public/components/common/header/action_menu_content.tsx @@ -99,9 +99,11 @@ export function ActionMenuContent(): React.ReactElement { diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index ac129bdb327d..60ccec84c3bb 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -83,7 +83,7 @@ const createMockStore = () => { const mockAppUrls: Record = { uptime: '/app/uptime', observability: '/app/observability', - '/home#/tutorial/uptimeMonitors': '/home#/tutorial/uptimeMonitors', + '/integrations/detail/synthetics/overview': '/integrations/detail/synthetics/overview', }; /* default mock core */ From a4f6988fc67e1bef37b3c153377ddbde40ddd704 Mon Sep 17 00:00:00 2001 From: Kevin Logan <56395104+kevinlog@users.noreply.github.com> Date: Mon, 18 Oct 2021 19:27:28 -0400 Subject: [PATCH 08/53] [Security Solution] Skip flakey test Configures a new connector.Cases connectors Configures a new connector (#115440) --- .../cypress/integration/cases/connectors.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index 287d86c6fba9..69b623de0b43 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -20,7 +20,8 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { CASES_URL } from '../../urls/navigation'; -describe('Cases connectors', () => { +// Skipping flakey test: https://github.com/elastic/kibana/issues/115438 +describe.skip('Cases connectors', () => { const configureResult = { connector: { id: 'e271c3b8-f702-4fbc-98e0-db942b573bbd', From 75048dc13b00d1ab8cde9e6e7d9db9829636cd05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Mon, 18 Oct 2021 19:53:20 -0400 Subject: [PATCH 09/53] [APM] Ensure APM deprecation documentationUrl point to correct doc branch (#115401) * using branch in the url * fixing TS --- x-pack/plugins/apm/server/deprecations/deprecations.test.ts | 5 ++++- x-pack/plugins/apm/server/deprecations/index.ts | 5 +++-- x-pack/plugins/apm/server/plugin.ts | 2 ++ 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts index d706146faf21..43e8140fb9b3 100644 --- a/x-pack/plugins/apm/server/deprecations/deprecations.test.ts +++ b/x-pack/plugins/apm/server/deprecations/deprecations.test.ts @@ -19,7 +19,7 @@ const deprecationContext = { describe('getDeprecations', () => { describe('when fleet is disabled', () => { it('returns no deprecations', async () => { - const deprecationsCallback = getDeprecations({}); + const deprecationsCallback = getDeprecations({ branch: 'master' }); const deprecations = await deprecationsCallback(deprecationContext); expect(deprecations).toEqual([]); }); @@ -28,6 +28,7 @@ describe('getDeprecations', () => { describe('when running on cloud with legacy apm-server', () => { it('returns deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, fleet: { start: () => ({ @@ -43,6 +44,7 @@ describe('getDeprecations', () => { describe('when running on cloud with fleet', () => { it('returns no deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: true } as unknown as CloudSetup, fleet: { start: () => ({ @@ -58,6 +60,7 @@ describe('getDeprecations', () => { describe('when running on prem', () => { it('returns no deprecations', async () => { const deprecationsCallback = getDeprecations({ + branch: 'master', cloudSetup: { isCloudEnabled: false } as unknown as CloudSetup, fleet: { start: () => ({ agentPolicyService: { get: () => undefined } }), diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts index b592a2bf1326..76c90270abb8 100644 --- a/x-pack/plugins/apm/server/deprecations/index.ts +++ b/x-pack/plugins/apm/server/deprecations/index.ts @@ -15,9 +15,11 @@ import { APMRouteHandlerResources } from '../'; export function getDeprecations({ cloudSetup, fleet, + branch, }: { cloudSetup?: CloudSetup; fleet?: APMRouteHandlerResources['plugins']['fleet']; + branch: string; }) { return async ({ savedObjectsClient, @@ -46,8 +48,7 @@ export function getDeprecations({ defaultMessage: 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.', }), - documentationUrl: - 'https://www.elastic.co/guide/en/apm/server/current/apm-integration.html', + documentationUrl: `https://www.elastic.co/guide/en/apm/server/${branch}/apm-integration.html`, level: 'warning', correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index d2d8dbf60236..72a1bc483015 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -215,10 +215,12 @@ export class APMPlugin ); })(); }); + core.deprecations.registerDeprecations({ getDeprecations: getDeprecations({ cloudSetup: plugins.cloud, fleet: resourcePlugins.fleet, + branch: this.initContext.env.packageInfo.branch, }), }); From e673383584c44f8fcb3bca767f9c38f0f7f79393 Mon Sep 17 00:00:00 2001 From: Oliver Gupte Date: Mon, 18 Oct 2021 19:58:35 -0400 Subject: [PATCH 10/53] [APM] APM-Fleet integration version check & upgrade message (#115297) --- x-pack/plugins/apm/common/fleet.ts | 2 + .../components/app/Settings/schema/index.tsx | 4 +- .../schema/migrated/card_footer_content.tsx | 47 +++++++++++++ .../migrated/successful_migration_card.tsx | 30 ++++++++ .../migrated/upgrade_available_card.tsx | 51 ++++++++++++++ .../app/Settings/schema/schema_overview.tsx | 68 +++++-------------- .../public/components/shared/Links/kibana.ts | 11 +++ .../get_apm_package_policy_definition.ts | 7 +- x-pack/plugins/apm/server/routes/fleet.ts | 6 +- 9 files changed, 170 insertions(+), 56 deletions(-) create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/card_footer_content.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx create mode 100644 x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx diff --git a/x-pack/plugins/apm/common/fleet.ts b/x-pack/plugins/apm/common/fleet.ts index 618cd20d6615..97551cc16b4b 100644 --- a/x-pack/plugins/apm/common/fleet.ts +++ b/x-pack/plugins/apm/common/fleet.ts @@ -6,3 +6,5 @@ */ export const POLICY_ELASTIC_AGENT_ON_CLOUD = 'policy-elastic-agent-on-cloud'; + +export const SUPPORTED_APM_PACKAGE_VERSION = '7.16.0'; diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx index ac32e22fa3de..b13046d34be9 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/index.tsx @@ -54,7 +54,8 @@ export function Schema() { const isLoading = status !== FETCH_STATUS.SUCCESS; const cloudApmMigrationEnabled = !!data.cloud_apm_migration_enabled; const hasCloudAgentPolicy = !!data.has_cloud_agent_policy; - const hasCloudApmPackagePolicy = !!data.has_cloud_apm_package_policy; + const cloudApmPackagePolicy = data.cloud_apm_package_policy; + const hasCloudApmPackagePolicy = !!cloudApmPackagePolicy; const hasRequiredRole = !!data.has_required_role; function updateLocalStorage(newStatus: FETCH_STATUS) { @@ -90,6 +91,7 @@ export function Schema() { cloudApmMigrationEnabled={cloudApmMigrationEnabled} hasCloudAgentPolicy={hasCloudAgentPolicy} hasRequiredRole={hasRequiredRole} + cloudApmPackagePolicy={cloudApmPackagePolicy} /> {isSwitchActive && ( + + {i18n.translate( + 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText', + { defaultMessage: 'View the APM integration in Fleet' } + )} + + + +

+ + {i18n.translate( + 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink', + { defaultMessage: 'Service inventory' } + )} + + ), + }} + /> +

+
+
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx new file mode 100644 index 000000000000..839479fbbf65 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/successful_migration_card.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiCard, EuiIcon } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { CardFooterContent } from './card_footer_content'; + +export function SuccessfulMigrationCard() { + return ( + } + title={i18n.translate('xpack.apm.settings.schema.success.title', { + defaultMessage: 'Elastic Agent successfully setup!', + })} + description={i18n.translate( + 'xpack.apm.settings.schema.success.description', + { + defaultMessage: + 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.', + } + )} + footer={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx new file mode 100644 index 000000000000..8c1023633596 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/migrated/upgrade_available_card.tsx @@ -0,0 +1,51 @@ +/* + * 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 { EuiCard, EuiIcon, EuiLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { useUpgradeApmPackagePolicyHref } from '../../../../shared/Links/kibana'; +import { CardFooterContent } from './card_footer_content'; + +export function UpgradeAvailableCard({ + apmPackagePolicyId, +}: { + apmPackagePolicyId: string | undefined; +}) { + const upgradeApmPackagePolicyHref = + useUpgradeApmPackagePolicyHref(apmPackagePolicyId); + + return ( + } + title={i18n.translate( + 'xpack.apm.settings.schema.upgradeAvailable.title', + { + defaultMessage: 'APM integration upgrade available!', + } + )} + description={ + + {i18n.translate( + 'xpack.apm.settings.schema.upgradeAvailable.upgradePackagePolicyLink', + { defaultMessage: 'Upgrade your APM integration' } + )} + + ), + }} + /> + } + footer={} + /> + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx index 0031c102e8ae..cead6cd8a6fb 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/schema/schema_overview.tsx @@ -19,11 +19,14 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { APMLink } from '../../../shared/Links/apm/APMLink'; +import semverLt from 'semver/functions/lt'; +import { SUPPORTED_APM_PACKAGE_VERSION } from '../../../../../common/fleet'; +import { PackagePolicy } from '../../../../../../fleet/common/types'; import { ElasticDocsLink } from '../../../shared/Links/ElasticDocsLink'; -import { useFleetCloudAgentPolicyHref } from '../../../shared/Links/kibana'; import rocketLaunchGraphic from './blog-rocket-720x420.png'; import { MigrationInProgressPanel } from './migration_in_progress_panel'; +import { UpgradeAvailableCard } from './migrated/upgrade_available_card'; +import { SuccessfulMigrationCard } from './migrated/successful_migration_card'; interface Props { onSwitch: () => void; @@ -34,6 +37,7 @@ interface Props { cloudApmMigrationEnabled: boolean; hasCloudAgentPolicy: boolean; hasRequiredRole: boolean; + cloudApmPackagePolicy: PackagePolicy | undefined; } export function SchemaOverview({ onSwitch, @@ -44,10 +48,13 @@ export function SchemaOverview({ cloudApmMigrationEnabled, hasCloudAgentPolicy, hasRequiredRole, + cloudApmPackagePolicy, }: Props) { - const fleetCloudAgentPolicyHref = useFleetCloudAgentPolicyHref(); const isDisabled = !cloudApmMigrationEnabled || !hasCloudAgentPolicy || !hasRequiredRole; + const packageVersion = cloudApmPackagePolicy?.package?.version; + const isUpgradeAvailable = + packageVersion && semverLt(packageVersion, SUPPORTED_APM_PACKAGE_VERSION); if (isLoading) { return ( @@ -76,54 +83,13 @@ export function SchemaOverview({ - - } - title={i18n.translate('xpack.apm.settings.schema.success.title', { - defaultMessage: 'Elastic Agent successfully setup!', - })} - description={i18n.translate( - 'xpack.apm.settings.schema.success.description', - { - defaultMessage: - 'Your APM integration is now setup and ready to receive data from your currently instrumented agents. Feel free to review the policies applied to your integtration.', - } - )} - footer={ -
- - {i18n.translate( - 'xpack.apm.settings.schema.success.viewIntegrationInFleet.buttonText', - { defaultMessage: 'View the APM integration in Fleet' } - )} - - - -

- - {i18n.translate( - 'xpack.apm.settings.schema.success.returnText.serviceInventoryLink', - { defaultMessage: 'Service inventory' } - )} - - ), - }} - /> -

-
-
- } - /> + {isUpgradeAvailable ? ( + + ) : ( + + )}
diff --git a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts index bfb7cf849f56..c0bdf3a98aa3 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/kibana.ts +++ b/x-pack/plugins/apm/public/components/shared/Links/kibana.ts @@ -26,3 +26,14 @@ export function useFleetCloudAgentPolicyHref() { } = useApmPluginContext(); return basePath.prepend('/app/fleet#/policies/policy-elastic-agent-on-cloud'); } + +export function useUpgradeApmPackagePolicyHref(packagePolicyId = '') { + const { + core: { + http: { basePath }, + }, + } = useApmPluginContext(); + return basePath.prepend( + `/app/fleet/policies/policy-elastic-agent-on-cloud/upgrade-package-policy/${packagePolicyId}?from=integrations-policy-list` + ); +} diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index 64b071b67d2b..98b6a6489c47 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { POLICY_ELASTIC_AGENT_ON_CLOUD } from '../../../common/fleet'; +import { + POLICY_ELASTIC_AGENT_ON_CLOUD, + SUPPORTED_APM_PACKAGE_VERSION, +} from '../../../common/fleet'; import { APMPluginSetupDependencies } from '../../types'; import { APM_PACKAGE_NAME } from './get_cloud_apm_package_policy'; @@ -36,7 +39,7 @@ export function getApmPackagePolicyDefinition( ], package: { name: APM_PACKAGE_NAME, - version: '0.4.0', + version: SUPPORTED_APM_PACKAGE_VERSION, title: 'Elastic APM', }, }; diff --git a/x-pack/plugins/apm/server/routes/fleet.ts b/x-pack/plugins/apm/server/routes/fleet.ts index 2884c08ceb9a..e18aefcd6e0d 100644 --- a/x-pack/plugins/apm/server/routes/fleet.ts +++ b/x-pack/plugins/apm/server/routes/fleet.ts @@ -92,7 +92,7 @@ const fleetAgentsRoute = createApmServerRoute({ }); const saveApmServerSchemaRoute = createApmServerRoute({ - endpoint: 'POST /internal/apm/fleet/apm_server_schema', + endpoint: 'POST /api/apm/fleet/apm_server_schema', options: { tags: ['access:apm', 'access:apm_write'] }, params: t.type({ body: t.type({ @@ -143,11 +143,13 @@ const getMigrationCheckRoute = createApmServerRoute({ fleetPluginStart, }) : undefined; + const apmPackagePolicy = getApmPackagePolicy(cloudAgentPolicy); return { has_cloud_agent_policy: !!cloudAgentPolicy, - has_cloud_apm_package_policy: !!getApmPackagePolicy(cloudAgentPolicy), + has_cloud_apm_package_policy: !!apmPackagePolicy, cloud_apm_migration_enabled: cloudApmMigrationEnabled, has_required_role: hasRequiredRole, + cloud_apm_package_policy: apmPackagePolicy, }; }, }); From 5e58fbded0592b1006c289341f376d89f1f88793 Mon Sep 17 00:00:00 2001 From: Georgii Gorbachev Date: Tue, 19 Oct 2021 02:29:14 +0200 Subject: [PATCH 11/53] [Security Solution][Detections] Fix a bug in siem-detection-engine-rule-status Saved Object migration to SO references (#115355) **Ticket:** https://github.com/elastic/kibana/issues/107068 **Follow-up after:** https://github.com/elastic/kibana/pull/114585 ## Summary The existing migration function `legacyMigrateRuleAlertIdSOReferences` that migrates `alertId` fields to SO references array did not include all the other attributes of a `siem-detection-engine-rule-status` doc being migrated to the resulting doc. This PR includes a fix and an integration test for that. ## Run the test To run the test, in one terminal execute: ``` cd ${KIBANA_HOME} && node scripts/functional_tests_server --config x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts ``` In another terminal execute: ``` cd ${KIBANA_HOME} && node scripts/functional_test_runner --config x-pack/test/detection_engine_api_integration/security_and_spaces/config.ts --include=x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts ``` ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../legacy_rule_status/legacy_migrations.ts | 63 ++++++++----------- .../security_and_spaces/tests/migrations.ts | 25 ++++++++ 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts index 92d7487be0cd..72ab4a2237ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_migrations.ts @@ -18,34 +18,19 @@ import { IRuleSavedAttributesSavedObjectAttributes } from '../types'; import { legacyGetRuleReference } from './legacy_utils'; export const truncateMessageFields: SavedObjectMigrationFn> = (doc) => { - const { lastFailureMessage, lastSuccessMessage, ...restAttributes } = doc.attributes; + const { lastFailureMessage, lastSuccessMessage, ...otherAttributes } = doc.attributes; return { ...doc, attributes: { + ...otherAttributes, lastFailureMessage: truncateMessage(lastFailureMessage), lastSuccessMessage: truncateMessage(lastSuccessMessage), - ...restAttributes, }, references: doc.references ?? [], }; }; -/** - * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and - * additional fields on the Alerting Framework Rule SO. - * - * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) - */ -export const legacyRuleStatusSavedObjectMigration = { - '7.15.2': truncateMessageFields, - '7.16.0': ( - doc: SavedObjectUnsanitizedDoc - ): SavedObjectSanitizedDoc => { - return legacyMigrateRuleAlertIdSOReferences(doc); - }, -}; - /** * This migrates alertId within legacy `siem-detection-engine-rule-status` to saved object references on an upgrade. * We only migrate alertId if we find these conditions: @@ -62,29 +47,24 @@ export const legacyRuleStatusSavedObjectMigration = { export const legacyMigrateRuleAlertIdSOReferences = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectSanitizedDoc => { - const { references } = doc; - - // Isolate alertId from the doc - const { alertId, ...attributesWithoutAlertId } = doc.attributes; - const existingReferences = references ?? []; + const { alertId, ...otherAttributes } = doc.attributes; + const existingReferences = doc.references ?? []; + // early return if alertId is not a string as expected if (!isString(alertId)) { - // early return if alertId is not a string as expected return { ...doc, references: existingReferences }; - } else { - const alertReferences = legacyMigrateAlertId({ - alertId, - existingReferences, - }); - - return { - ...doc, - attributes: { - ...attributesWithoutAlertId.attributes, - }, - references: [...existingReferences, ...alertReferences], - }; } + + const alertReferences = legacyMigrateAlertId({ + alertId, + existingReferences, + }); + + return { + ...doc, + attributes: otherAttributes, + references: [...existingReferences, ...alertReferences], + }; }; /** @@ -113,3 +93,14 @@ export const legacyMigrateAlertId = ({ return [legacyGetRuleReference(alertId)]; } }; + +/** + * This side-car rule status SO is deprecated and is to be replaced by the RuleExecutionLog on Event-Log and + * additional fields on the Alerting Framework Rule SO. + * + * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) + */ +export const legacyRuleStatusSavedObjectMigration = { + '7.15.2': truncateMessageFields, + '7.16.0': legacyMigrateRuleAlertIdSOReferences, +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index 6d1d64a04cd9..cfae7532ba49 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -6,6 +6,7 @@ */ import expect from '@kbn/expect'; +import { IRuleStatusSOAttributes } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export @@ -113,6 +114,30 @@ export default ({ getService }: FtrProviderContext): void => { undefined ); }); + + it('migrates legacy siem-detection-engine-rule-status and retains other attributes as the same attributes as before', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-status': IRuleStatusSOAttributes; + }>({ + index: '.kibana', + id: 'siem-detection-engine-rule-status:d62d2980-27c4-11ec-92b0-f7b47106bb35', + }); + expect(response.statusCode).to.eql(200); + + expect(response.body._source?.['siem-detection-engine-rule-status']).to.eql({ + statusDate: '2021-10-11T20:51:26.622Z', + status: 'succeeded', + lastFailureAt: '2021-10-11T18:10:08.982Z', + lastSuccessAt: '2021-10-11T20:51:26.622Z', + lastFailureMessage: + '4 days (323690920ms) were not queried between this rule execution and the last execution, so signals may have been missed. Consider increasing your look behind time or adding more Kibana instances. name: "Threshy" id: "fb1046a0-0452-11ec-9b15-d13d79d162f3" rule id: "b789c80f-f6d8-41f1-8b4f-b4a23342cde2" signals index: ".siem-signals-spong-default"', + lastSuccessMessage: 'succeeded', + gap: '4 days', + bulkCreateTimeDurations: ['34.49'], + searchAfterTimeDurations: ['62.58'], + lastLookBackDate: null, + }); + }); }); }); }; From a8b616aaa29e52863526284c1b038eda1a3a5944 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Mon, 18 Oct 2021 20:31:07 -0400 Subject: [PATCH 12/53] [Fleet] Add beta flag for custom integrations (#115447) --- .../integrations/sections/epm/screens/home/index.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 2d1077f586a1..4270d360b929 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -66,6 +66,13 @@ export const mapToCard = ( uiInternalPathUrl = url; } + let release: 'ga' | 'beta' | 'experimental' | undefined; + if ('release' in item) { + release = item.release; + } else if (item.isBeta === true) { + release = 'beta'; + } + return { id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}-${item.id}`, description: item.description, @@ -75,7 +82,7 @@ export const mapToCard = ( integration: 'integration' in item ? item.integration || '' : '', name: 'name' in item ? item.name || '' : '', version: 'version' in item ? item.version || '' : '', - release: 'release' in item ? item.release : undefined, + release, categories: ((item.categories || []) as string[]).filter((c: string) => !!c), }; }; From 083a2b9523d10fa8891563274abbca360a4216b7 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Mon, 18 Oct 2021 21:08:50 -0400 Subject: [PATCH 13/53] skip flaky suite (#115488) --- .../test/security_solution_endpoint_api_int/apis/metadata.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 35fe0cdd6da2..2dcf36cc42ae 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -24,7 +24,8 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); - describe('test metadata api', () => { + // Failing: See https://github.com/elastic/kibana/issues/115488 + describe.skip('test metadata api', () => { // TODO add this after endpoint package changes are merged and in snapshot // describe('with .metrics-endpoint.metadata_united_default index', () => { // }); From b103a544cc71d0998074b94c6a00a5da5d6e78ae Mon Sep 17 00:00:00 2001 From: Justin Kambic Date: Mon, 18 Oct 2021 22:43:00 -0400 Subject: [PATCH 14/53] [Uptime] Fix unhandled promise rejection failure (#114883) * Fix unhandled promise rejection failure. * Mock monaco to avoid editor-related errors failing test. * Update assertion. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/fleet_package/custom_fields.test.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index 26ee26cc8ed7..62c6f5598adb 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -313,11 +313,11 @@ describe.skip('', () => { // resolve errors fireEvent.click(monitorType); - waitFor(() => { - expect(getByText('http')).toBeInTheDocument(); - expect(getByText('tcp')).toBeInTheDocument(); - expect(getByText('icmp')).toBeInTheDocument(); - expect(queryByText('browser')).not.toBeInTheDocument(); + await waitFor(() => { + expect(getByText('HTTP')).toBeInTheDocument(); + expect(getByText('TCP')).toBeInTheDocument(); + expect(getByText('ICMP')).toBeInTheDocument(); + expect(queryByText('Browser')).not.toBeInTheDocument(); }); }); }); From d0bc10f896a30c4b08c5fc302cd98c661d6847e3 Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Tue, 19 Oct 2021 05:27:12 +0200 Subject: [PATCH 15/53] [Security Solution][Endpoint]Activity Log API/UX changes (#114905) * rename legacy actions/responses fixes elastic/security-team/issues/1702 * use correct name for responses index refs elastic/kibana/pull/113621 * extract helper method to utils * append endpoint responses docs to activity log * Show completed responses on activity log fixes elastic/security-team/issues/1703 * remove width restriction on date picker * add a simple test to verify endpoint responses fixes elastic/security-team/issues/1702 * find unique action_ids from `.fleet-actions` and `.logs-endpoint.actions-default` indices fixes elastic/security-team/issues/1702 * do not filter out endpoint only actions/responses that did not make it to Fleet review comments * use a constant to manage various doc types review comments * refactor `getActivityLog` Simplify `getActivityLog` so it is easier to reason with. review comments * skip this for now will mock this better in a new PR * improve types * display endpoint actions similar to fleet actions, but with success icon color * Correctly do mocks for tests * Include only errored endpoint actions, remove successful duplicates fixes elastic/security-team/issues/1703 * Update tests to use non duplicate action_ids review comments fixes elastic/security-team/issues/1703 * show correct action title review fixes * statusCode constant review change * rename review changes * Update translations.ts refs https://github.com/elastic/kibana/pull/114905/commits/74a8340b5eb2e31faba67a4fbe656f74fe52d0a2 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/endpoint/constants.ts | 4 +- .../common/endpoint/types/actions.ts | 33 ++- .../activity_log_date_range_picker/index.tsx | 1 - .../view/details/components/log_entry.tsx | 101 ++++++- .../components/log_entry_timeline_icon.tsx | 21 +- .../view/details/endpoints.stories.tsx | 14 +- .../pages/endpoint_hosts/view/index.test.tsx | 95 +++++-- .../pages/endpoint_hosts/view/translations.ts | 36 +++ .../endpoint/routes/actions/audit_log.test.ts | 213 ++++++++++++-- .../endpoint/routes/actions/isolation.ts | 29 +- .../server/endpoint/routes/actions/mocks.ts | 32 +++ .../server/endpoint/services/actions.ts | 146 ++++------ .../endpoint/utils/audit_log_helpers.ts | 266 ++++++++++++++++++ .../server/endpoint/utils/index.ts | 2 + .../endpoint/utils/yes_no_data_stream.test.ts | 100 +++++++ .../endpoint/utils/yes_no_data_stream.ts | 59 ++++ 16 files changed, 949 insertions(+), 203 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts create mode 100644 x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 6e9123da2dd9..178a2b68a4aa 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -10,7 +10,7 @@ export const ENDPOINT_ACTIONS_DS = '.logs-endpoint.actions'; export const ENDPOINT_ACTIONS_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; export const ENDPOINT_ACTION_RESPONSES_DS = '.logs-endpoint.action.responses'; -export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTIONS_DS}-default`; +export const ENDPOINT_ACTION_RESPONSES_INDEX = `${ENDPOINT_ACTION_RESPONSES_DS}-default`; export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; @@ -60,3 +60,5 @@ export const UNISOLATE_HOST_ROUTE = `${BASE_ENDPOINT_ROUTE}/unisolate`; /** Endpoint Actions Log Routes */ export const ENDPOINT_ACTION_LOG_ROUTE = `/api/endpoint/action_log/{agent_id}`; export const ACTION_STATUS_ROUTE = `/api/endpoint/action_status`; + +export const failedFleetActionErrorCode = '424'; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts index bc46ca2f5b45..fb29297eb592 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/actions.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/actions.ts @@ -10,6 +10,13 @@ import { ActionStatusRequestSchema, HostIsolationRequestSchema } from '../schema export type ISOLATION_ACTIONS = 'isolate' | 'unisolate'; +export const ActivityLogItemTypes = { + ACTION: 'action' as const, + RESPONSE: 'response' as const, + FLEET_ACTION: 'fleetAction' as const, + FLEET_RESPONSE: 'fleetResponse' as const, +}; + interface EcsError { code?: string; id?: string; @@ -87,8 +94,24 @@ export interface EndpointActionResponse { action_data: EndpointActionData; } +export interface EndpointActivityLogAction { + type: typeof ActivityLogItemTypes.ACTION; + item: { + id: string; + data: LogsEndpointAction; + }; +} + +export interface EndpointActivityLogActionResponse { + type: typeof ActivityLogItemTypes.RESPONSE; + item: { + id: string; + data: LogsEndpointActionResponse; + }; +} + export interface ActivityLogAction { - type: 'action'; + type: typeof ActivityLogItemTypes.FLEET_ACTION; item: { // document _id id: string; @@ -97,7 +120,7 @@ export interface ActivityLogAction { }; } export interface ActivityLogActionResponse { - type: 'response'; + type: typeof ActivityLogItemTypes.FLEET_RESPONSE; item: { // document id id: string; @@ -105,7 +128,11 @@ export interface ActivityLogActionResponse { data: EndpointActionResponse; }; } -export type ActivityLogEntry = ActivityLogAction | ActivityLogActionResponse; +export type ActivityLogEntry = + | ActivityLogAction + | ActivityLogActionResponse + | EndpointActivityLogAction + | EndpointActivityLogActionResponse; export interface ActivityLog { page: number; pageSize: number; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index 05887d82caca..a57fa8d8e4ce 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -32,7 +32,6 @@ interface Range { const DatePickerWrapper = styled.div` width: ${(props) => props.theme.eui.fractions.single.percentage}; - max-width: 350px; `; const StickyFlexItem = styled(EuiFlexItem)` background: ${(props) => `${props.theme.eui.euiHeaderBackgroundColor}`}; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx index bbe0a6f3afcd..79af2ecb354f 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry.tsx @@ -9,24 +9,34 @@ import React, { memo, useMemo } from 'react'; import styled from 'styled-components'; import { EuiComment, EuiText, EuiAvatarProps, EuiCommentProps, IconType } from '@elastic/eui'; -import { Immutable, ActivityLogEntry } from '../../../../../../../common/endpoint/types'; +import { + Immutable, + ActivityLogEntry, + ActivityLogItemTypes, +} from '../../../../../../../common/endpoint/types'; import { FormattedRelativePreferenceDate } from '../../../../../../common/components/formatted_date'; import { LogEntryTimelineIcon } from './log_entry_timeline_icon'; +import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; import * as i18 from '../../translations'; const useLogEntryUIProps = ( - logEntry: Immutable + logEntry: Immutable, + theme: ReturnType ): { actionEventTitle: string; + avatarColor: EuiAvatarProps['color']; + avatarIconColor: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; commentText: string; commentType: EuiCommentProps['type']; displayComment: boolean; displayResponseEvent: boolean; + failedActionEventTitle: string; iconType: IconType; isResponseEvent: boolean; isSuccessful: boolean; + isCompleted: boolean; responseEventTitle: string; username: string | React.ReactNode; } => { @@ -34,15 +44,19 @@ const useLogEntryUIProps = ( let iconType: IconType = 'dot'; let commentType: EuiCommentProps['type'] = 'update'; let commentText: string = ''; + let avatarColor: EuiAvatarProps['color'] = theme.euiColorLightestShade; + let avatarIconColor: EuiAvatarProps['iconColor']; let avatarSize: EuiAvatarProps['size'] = 's'; + let failedActionEventTitle: string = ''; let isIsolateAction: boolean = false; let isResponseEvent: boolean = false; let isSuccessful: boolean = false; + let isCompleted: boolean = false; let displayComment: boolean = false; let displayResponseEvent: boolean = true; let username: EuiCommentProps['username'] = ''; - if (logEntry.type === 'action') { + if (logEntry.type === ActivityLogItemTypes.FLEET_ACTION) { avatarSize = 'm'; commentType = 'regular'; commentText = logEntry.item.data.data.comment?.trim() ?? ''; @@ -59,13 +73,51 @@ const useLogEntryUIProps = ( displayComment = true; } } - } else if (logEntry.type === 'response') { + } + if (logEntry.type === ActivityLogItemTypes.ACTION) { + avatarSize = 'm'; + commentType = 'regular'; + commentText = logEntry.item.data.EndpointActions.data.comment?.trim() ?? ''; + displayResponseEvent = false; + iconType = 'lockOpen'; + username = logEntry.item.data.user.id; + avatarIconColor = theme.euiColorVis9_behindText; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointReleaseAction; + if (logEntry.item.data.EndpointActions.data) { + const data = logEntry.item.data.EndpointActions.data; + if (data.command === 'isolate') { + iconType = 'lock'; + failedActionEventTitle = i18.ACTIVITY_LOG.LogEntry.action.failedEndpointIsolateAction; + } + if (commentText) { + displayComment = true; + } + } + } else if (logEntry.type === ActivityLogItemTypes.FLEET_RESPONSE) { isResponseEvent = true; if (logEntry.item.data.action_data.command === 'isolate') { isIsolateAction = true; } if (!!logEntry.item.data.completed_at && !logEntry.item.data.error) { isSuccessful = true; + } else { + avatarColor = theme.euiColorVis9_behindText; + } + } else if (logEntry.type === ActivityLogItemTypes.RESPONSE) { + iconType = 'check'; + isResponseEvent = true; + if (logEntry.item.data.EndpointActions.data.command === 'isolate') { + isIsolateAction = true; + } + if (logEntry.item.data.EndpointActions.completed_at) { + isCompleted = true; + if (!logEntry.item.data.error) { + isSuccessful = true; + avatarColor = theme.euiColorVis0_behindText; + } else { + isSuccessful = false; + avatarColor = theme.euiColorVis9_behindText; + } } } @@ -75,13 +127,23 @@ const useLogEntryUIProps = ( const getResponseEventTitle = () => { if (isIsolateAction) { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.isolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.isolationFailed; } } else { - if (isSuccessful) { + if (isCompleted) { + if (isSuccessful) { + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndSuccessful; + } + return i18.ACTIVITY_LOG.LogEntry.response.unisolationCompletedAndUnsuccessful; + } else if (isSuccessful) { return i18.ACTIVITY_LOG.LogEntry.response.unisolationSuccessful; } else { return i18.ACTIVITY_LOG.LogEntry.response.unisolationFailed; @@ -91,18 +153,22 @@ const useLogEntryUIProps = ( return { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, isSuccessful, + isCompleted, responseEventTitle: getResponseEventTitle(), username, }; - }, [logEntry]); + }, [logEntry, theme]); }; const StyledEuiComment = styled(EuiComment)` @@ -126,28 +192,41 @@ const StyledEuiComment = styled(EuiComment)` `; export const LogEntry = memo(({ logEntry }: { logEntry: Immutable }) => { + const theme = useEuiTheme(); const { actionEventTitle, + avatarColor, + avatarIconColor, avatarSize, commentText, commentType, displayComment, displayResponseEvent, + failedActionEventTitle, iconType, isResponseEvent, - isSuccessful, responseEventTitle, username, - } = useLogEntryUIProps(logEntry); + } = useLogEntryUIProps(logEntry, theme); return ( } - event={{displayResponseEvent ? responseEventTitle : actionEventTitle}} + event={ + + {displayResponseEvent + ? responseEventTitle + : failedActionEventTitle + ? failedActionEventTitle + : actionEventTitle} + + } timelineIcon={ - + } data-test-subj="timelineEntry" > diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx index 3ff311cd8a13..25e7c7d2c4a4 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/log_entry_timeline_icon.tsx @@ -7,32 +7,27 @@ import React, { memo } from 'react'; import { EuiAvatar, EuiAvatarProps } from '@elastic/eui'; -import { useEuiTheme } from '../../../../../../common/lib/theme/use_eui_theme'; export const LogEntryTimelineIcon = memo( ({ + avatarColor, + avatarIconColor, avatarSize, - isResponseEvent, - isSuccessful, iconType, + isResponseEvent, }: { + avatarColor: EuiAvatarProps['color']; + avatarIconColor?: EuiAvatarProps['iconColor']; avatarSize: EuiAvatarProps['size']; - isResponseEvent: boolean; - isSuccessful: boolean; iconType: EuiAvatarProps['iconType']; + isResponseEvent: boolean; }) => { - const euiTheme = useEuiTheme(); - return ( ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx index 123a51e5a52b..717368a1ff3a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoints.stories.tsx @@ -8,7 +8,11 @@ import React, { ComponentType } from 'react'; import moment from 'moment'; -import { ActivityLog, Immutable } from '../../../../../../common/endpoint/types'; +import { + ActivityLog, + Immutable, + ActivityLogItemTypes, +} from '../../../../../../common/endpoint/types'; import { EndpointDetailsFlyoutTabs } from './components/endpoint_details_tabs'; import { EndpointActivityLog } from './endpoint_activity_log'; import { EndpointDetailsFlyout } from '.'; @@ -26,7 +30,7 @@ export const dummyEndpointActivityLog = ( endDate: moment().toString(), data: [ { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -44,7 +48,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -63,7 +67,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { @@ -82,7 +86,7 @@ export const dummyEndpointActivityLog = ( }, }, { - type: 'action', + type: ActivityLogItemTypes.FLEET_ACTION, item: { id: '', data: { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx index b2c438659b77..727c2e8a3502 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.test.tsx @@ -42,6 +42,7 @@ import { import { getCurrentIsolationRequestState } from '../store/selectors'; import { licenseService } from '../../../../common/hooks/use_license'; import { FleetActionGenerator } from '../../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../../common/endpoint/data_generators/endpoint_action_generator'; import { APP_PATH, MANAGEMENT_PATH, @@ -807,7 +808,7 @@ describe('when on the endpoint list page', () => { let renderResult: ReturnType; const agentId = 'some_agent_id'; - let getMockData: () => ActivityLog; + let getMockData: (option?: { hasLogsEndpointActionResponses?: boolean }) => ActivityLog; beforeEach(async () => { window.IntersectionObserver = jest.fn(() => ({ root: null, @@ -828,10 +829,15 @@ describe('when on the endpoint list page', () => { }); const fleetActionGenerator = new FleetActionGenerator('seed'); - const responseData = fleetActionGenerator.generateResponse({ + const endpointActionGenerator = new EndpointActionGenerator('seed'); + const endpointResponseData = endpointActionGenerator.generateResponse({ + agent: { id: agentId }, + }); + const fleetResponseData = fleetActionGenerator.generateResponse({ agent_id: agentId, }); - const actionData = fleetActionGenerator.generate({ + + const fleetActionData = fleetActionGenerator.generate({ agents: [agentId], data: { comment: 'some comment', @@ -844,35 +850,49 @@ describe('when on the endpoint list page', () => { }, }); - getMockData = () => ({ - page: 1, - pageSize: 50, - startDate: 'now-1d', - endDate: 'now', - data: [ - { - type: 'response', - item: { - id: 'some_id_0', - data: responseData, + getMockData = (hasLogsEndpointActionResponses?: { + hasLogsEndpointActionResponses?: boolean; + }) => { + const response: ActivityLog = { + page: 1, + pageSize: 50, + startDate: 'now-1d', + endDate: 'now', + data: [ + { + type: 'fleetResponse', + item: { + id: 'some_id_1', + data: fleetResponseData, + }, }, - }, - { - type: 'action', - item: { - id: 'some_id_1', - data: actionData, + { + type: 'fleetAction', + item: { + id: 'some_id_2', + data: fleetActionData, + }, }, - }, - { - type: 'action', + { + type: 'fleetAction', + item: { + id: 'some_id_3', + data: isolatedActionData, + }, + }, + ], + }; + if (hasLogsEndpointActionResponses) { + response.data.unshift({ + type: 'response', item: { - id: 'some_id_3', - data: isolatedActionData, + id: 'some_id_0', + data: endpointResponseData, }, - }, - ], - }); + }); + } + return response; + }; renderResult = render(); await reactTestingLibrary.act(async () => { @@ -912,6 +932,25 @@ describe('when on the endpoint list page', () => { expect(`${logEntries[1]} .euiCommentTimeline__icon--regular`).not.toBe(null); }); + it('should display log accurately with endpoint responses', async () => { + const activityLogTab = await renderResult.findByTestId('activity_log'); + reactTestingLibrary.act(() => { + reactTestingLibrary.fireEvent.click(activityLogTab); + }); + await middlewareSpy.waitForAction('endpointDetailsActivityLogChanged'); + reactTestingLibrary.act(() => { + dispatchEndpointDetailsActivityLogChanged( + 'success', + getMockData({ hasLogsEndpointActionResponses: true }) + ); + }); + const logEntries = await renderResult.queryAllByTestId('timelineEntry'); + expect(logEntries.length).toEqual(4); + expect(`${logEntries[0]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[1]} .euiCommentTimeline__icon--update`).not.toBe(null); + expect(`${logEntries[2]} .euiCommentTimeline__icon--regular`).not.toBe(null); + }); + it('should display empty state when API call has failed', async () => { const activityLogTab = await renderResult.findByTestId('activity_log'); reactTestingLibrary.act(() => { diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts index c8a29eed3fda..9cd55a70005e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/translations.ts @@ -56,8 +56,44 @@ export const ACTIVITY_LOG = { defaultMessage: 'submitted request: Release host', } ), + failedEndpointReleaseAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointReleaseAction', + { + defaultMessage: 'failed to submit request: Release host', + } + ), + failedEndpointIsolateAction: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.action.failedEndpointIsolateAction', + { + defaultMessage: 'failed to submit request: Isolate host', + } + ), }, response: { + isolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndSuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint', + } + ), + isolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationCompletedAndUnsuccessful', + { + defaultMessage: 'Host isolation request completed by Endpoint with errors', + } + ), + unisolationCompletedAndSuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndSuccessful', + { + defaultMessage: 'Release request completed by Endpoint', + } + ), + unisolationCompletedAndUnsuccessful: i18n.translate( + 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.unisolationCompletedAndUnsuccessful', + { + defaultMessage: 'Release request completed by Endpoint with errors', + } + ), isolationSuccessful: i18n.translate( 'xpack.securitySolution.endpointDetails.activityLog.logEntry.response.isolationSuccessful', { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts index 4bd63c83169e..5ce796200078 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.test.ts @@ -30,9 +30,15 @@ import { } from '../../mocks'; import { registerActionAuditLogRoutes } from './audit_log'; import uuid from 'uuid'; -import { aMockAction, aMockResponse, MockAction, mockSearchResult, MockResponse } from './mocks'; +import { mockAuditLogSearchResult, Results } from './mocks'; import { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { ActivityLog } from '../../../../common/endpoint/types'; +import { + ActivityLog, + EndpointAction, + EndpointActionResponse, +} from '../../../../common/endpoint/types'; +import { FleetActionGenerator } from '../../../../common/endpoint/data_generators/fleet_action_generator'; +import { EndpointActionGenerator } from '../../../../common/endpoint/data_generators/endpoint_action_generator'; describe('Action Log API', () => { describe('schema', () => { @@ -93,17 +99,30 @@ describe('Action Log API', () => { }); describe('response', () => { - const mockID = 'XYZABC-000'; - const actionID = 'some-known-actionid'; + const mockAgentID = 'XYZABC-000'; let endpointAppContextService: EndpointAppContextService; + const fleetActionGenerator = new FleetActionGenerator('seed'); + const endpointActionGenerator = new EndpointActionGenerator('seed'); // convenience for calling the route and handler for audit log let getActivityLog: ( params: EndpointActionLogRequestParams, query?: EndpointActionLogRequestQuery ) => Promise>; - // convenience for injecting mock responses for actions index and responses - let havingActionsAndResponses: (actions: MockAction[], responses: MockResponse[]) => void; + + // convenience for injecting mock action requests and responses + // for .logs-endpoint and .fleet indices + let mockActions: ({ + numActions, + hasFleetActions, + hasFleetResponses, + hasResponses, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => void; let havingErrors: () => void; @@ -149,12 +168,113 @@ describe('Action Log API', () => { return mockResponse; }; - havingActionsAndResponses = (actions: MockAction[], responses: MockResponse[]) => { - esClientMock.asCurrentUser.search = jest.fn().mockImplementation((req) => { - const items: any[] = - req.index === '.fleet-actions' ? actions.splice(0, 50) : responses.splice(0, 1000); + // some arbitrary ids for needed actions + const getMockActionIds = (numAction: number): string[] => { + return [...Array(numAction).keys()].map(() => Math.random().toString(36).split('.')[1]); + }; + + // create as many actions as needed + const getEndpointActionsData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generate({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many responses as needed + const getEndpointResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + endpointActionGenerator.generateResponse({ + agent: { id: mockAgentID }, + EndpointActions: { + action_id: actionId, + }, + }) + ); + return data; + }; + // create as many fleet actions as needed + const getFleetResponseData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generateResponse({ + agent_id: mockAgentID, + action_id: actionId, + }) + ); + return data; + }; + // create as many fleet responses as needed + const getFleetActionData = (actionIds: string[]) => { + const data = actionIds.map((actionId) => + fleetActionGenerator.generate({ + agents: [mockAgentID], + action_id: actionId, + data: { + comment: 'some comment', + }, + }) + ); + return data; + }; + + // mock actions and responses results in a single response + mockActions = ({ + numActions, + hasFleetActions = false, + hasFleetResponses = false, + hasResponses = false, + }: { + numActions: number; + hasFleetActions?: boolean; + hasFleetResponses?: boolean; + hasResponses?: boolean; + }) => { + esClientMock.asCurrentUser.search = jest.fn().mockImplementationOnce(() => { + let actions: Results[] = []; + let fleetActions: Results[] = []; + let responses: Results[] = []; + let fleetResponses: Results[] = []; + + const actionIds = getMockActionIds(numActions); + + actions = getEndpointActionsData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.actions-default-2021.19.10-000001', + _source: e, + })); + + if (hasFleetActions) { + fleetActions = getFleetActionData(actionIds).map((e) => ({ + _index: '.fleet-actions-7', + _source: e, + })); + } - return Promise.resolve(mockSearchResult(items.map((x) => x.build()))); + if (hasFleetResponses) { + fleetResponses = getFleetResponseData(actionIds).map((e) => ({ + _index: '.ds-.fleet-actions-results-2021.19.10-000001', + _source: e, + })); + } + + if (hasResponses) { + responses = getEndpointResponseData(actionIds).map((e) => ({ + _index: '.ds-.logs-endpoint.action.responses-default-2021.19.10-000001', + _source: e, + })); + } + + const results = mockAuditLogSearchResult([ + ...actions, + ...fleetActions, + ...responses, + ...fleetResponses, + ]); + + return Promise.resolve(results); }); }; @@ -172,45 +292,80 @@ describe('Action Log API', () => { }); it('should return an empty array when nothing in audit log', async () => { - havingActionsAndResponses([], []); - const response = await getActivityLog({ agent_id: mockID }); + mockActions({ numActions: 0 }); + + const response = await getActivityLog({ agent_id: mockAgentID }); expect(response.ok).toBeCalled(); expect((response.ok.mock.calls[0][0]?.body as ActivityLog).data).toHaveLength(0); }); - it('should have actions and action responses', async () => { - havingActionsAndResponses( - [ - aMockAction().withAgent(mockID).withAction('isolate').withID(actionID), - aMockAction().withAgent(mockID).withAction('unisolate'), - ], - [aMockResponse(actionID, mockID).forAction(actionID).forAgent(mockID)] - ); - const response = await getActivityLog({ agent_id: mockID }); + it('should return fleet actions, fleet responses and endpoint responses', async () => { + mockActions({ + numActions: 2, + hasFleetActions: true, + hasFleetResponses: true, + hasResponses: true, + }); + + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(6); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should return only fleet actions and no responses', async () => { + mockActions({ numActions: 2, hasFleetActions: true }); + + const response = await getActivityLog({ agent_id: mockAgentID }); const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; + expect(response.ok).toBeCalled(); + expect(responseBody.data).toHaveLength(2); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + }); + + it('should only have fleet data', async () => { + mockActions({ numActions: 2, hasFleetActions: true, hasFleetResponses: true }); + const response = await getActivityLog({ agent_id: mockAgentID }); + const responseBody = response.ok.mock.calls[0][0]?.body as ActivityLog; expect(response.ok).toBeCalled(); - expect(responseBody.data).toHaveLength(3); - expect(responseBody.data.filter((e) => e.type === 'response')).toHaveLength(1); - expect(responseBody.data.filter((e) => e.type === 'action')).toHaveLength(2); + expect(responseBody.data).toHaveLength(4); + + expect( + responseBody.data.filter((e) => (e.item.data as EndpointAction).expiration) + ).toHaveLength(2); + expect( + responseBody.data.filter((e) => (e.item.data as EndpointActionResponse).completed_at) + ).toHaveLength(2); }); it('should throw errors when no results for some agentID', async () => { havingErrors(); try { - await getActivityLog({ agent_id: mockID }); + await getActivityLog({ agent_id: mockAgentID }); } catch (error) { - expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockID}`); + expect(error.message).toEqual(`Error fetching actions log for agent_id ${mockAgentID}`); } }); it('should return date ranges if present in the query', async () => { - havingActionsAndResponses([], []); + mockActions({ numActions: 0 }); + const startDate = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); const endDate = new Date().toISOString(); const response = await getActivityLog( - { agent_id: mockID }, + { agent_id: mockAgentID }, { page: 1, page_size: 50, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index e12299bedbb3..02f0cb486764 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -17,6 +17,7 @@ import { ENDPOINT_ACTION_RESPONSES_DS, ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, + failedFleetActionErrorCode, } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { @@ -33,6 +34,7 @@ import { getMetadataForEndpoints } from '../../services'; import { EndpointAppContext } from '../../types'; import { APP_ID } from '../../../../common/constants'; import { userCanIsolate } from '../../../../common/endpoint/actions'; +import { doLogsEndpointActionDsExists } from '../../utils'; /** * Registers the Host-(un-)isolation routes @@ -78,7 +80,7 @@ const createFailedActionResponseEntry = async ({ body: { ...doc, error: { - code: '424', + code: failedFleetActionErrorCode, message: 'Failed to deliver action request to fleet', }, }, @@ -88,31 +90,6 @@ const createFailedActionResponseEntry = async ({ } }; -const doLogsEndpointActionDsExists = async ({ - context, - logger, - dataStreamName, -}: { - context: SecuritySolutionRequestHandlerContext; - logger: Logger; - dataStreamName: string; -}): Promise => { - try { - const esClient = context.core.elasticsearch.client.asInternalUser; - const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ - name: dataStreamName, - }); - return doesIndexTemplateExist.statusCode === 404 ? false : true; - } catch (error) { - const errorType = error?.type ?? ''; - if (errorType !== 'resource_not_found_exception') { - logger.error(error); - throw error; - } - return false; - } -}; - export const isolationRequestHandler = function ( endpointContext: EndpointAppContext, isolate: boolean diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts index 34f7d140a78d..b50d80a9bae7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/mocks.ts @@ -13,11 +13,43 @@ import { ApiResponse } from '@elastic/elasticsearch'; import moment from 'moment'; import uuid from 'uuid'; import { + LogsEndpointAction, + LogsEndpointActionResponse, EndpointAction, EndpointActionResponse, ISOLATION_ACTIONS, } from '../../../../common/endpoint/types'; +export interface Results { + _index: string; + _source: + | LogsEndpointAction + | LogsEndpointActionResponse + | EndpointAction + | EndpointActionResponse; +} +export const mockAuditLogSearchResult = (results?: Results[]) => { + const response = { + body: { + hits: { + total: { value: results?.length ?? 0, relation: 'eq' }, + hits: + results?.map((a: Results) => ({ + _index: a._index, + _id: Math.random().toString(36).split('.')[1], + _score: 0.0, + _source: a._source, + })) ?? [], + }, + }, + statusCode: 200, + headers: {}, + warnings: [], + meta: {} as any, + }; + return response; +}; + export const mockSearchResult = (results: any = []): ApiResponse => { return { body: { diff --git a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts index 711d78ba51b5..d59ecb674196 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/actions.ts @@ -6,15 +6,28 @@ */ import { ElasticsearchClient, Logger } from 'kibana/server'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; import { SecuritySolutionRequestHandlerContext } from '../../types'; import { ActivityLog, + ActivityLogEntry, EndpointAction, + LogsEndpointAction, EndpointActionResponse, EndpointPendingActions, + LogsEndpointActionResponse, } from '../../../common/endpoint/types'; -import { catchAndWrapError } from '../utils'; +import { + catchAndWrapError, + categorizeActionResults, + categorizeResponseResults, + getActionRequestsResult, + getActionResponsesResult, + getTimeSortedData, + getUniqueLogData, +} from '../utils'; import { EndpointMetadataService } from './metadata'; const PENDING_ACTION_RESPONSE_MAX_LAPSED_TIME = 300000; // 300k ms === 5 minutes @@ -38,9 +51,9 @@ export const getAuditLogResponse = async ({ }): Promise => { const size = Math.floor(pageSize / 2); const from = page <= 1 ? 0 : page * size - size + 1; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const data = await getActivityLog({ - esClient, + context, from, size, startDate, @@ -59,7 +72,7 @@ export const getAuditLogResponse = async ({ }; const getActivityLog = async ({ - esClient, + context, size, from, startDate, @@ -67,83 +80,39 @@ const getActivityLog = async ({ elasticAgentId, logger, }: { - esClient: ElasticsearchClient; + context: SecuritySolutionRequestHandlerContext; elasticAgentId: string; size: number; from: number; startDate: string; endDate: string; logger: Logger; -}) => { - const options = { - headers: { - 'X-elastic-product-origin': 'fleet', - }, - ignore: [404], - }; - - let actionsResult; - let responsesResult; - const dateFilters = [ - { range: { '@timestamp': { gte: startDate } } }, - { range: { '@timestamp': { lte: endDate } } }, - ]; +}): Promise => { + let actionsResult: ApiResponse, unknown>; + let responsesResult: ApiResponse, unknown>; try { // fetch actions with matching agent_id - const baseActionFilters = [ - { term: { agents: elasticAgentId } }, - { term: { input_type: 'endpoint' } }, - { term: { type: 'INPUT_ACTION' } }, - ]; - const actionsFilters = [...baseActionFilters, ...dateFilters]; - actionsResult = await esClient.search( - { - index: AGENT_ACTIONS_INDEX, - size, - from, - body: { - query: { - bool: { - // @ts-ignore - filter: actionsFilters, - }, - }, - sort: [ - { - '@timestamp': { - order: 'desc', - }, - }, - ], - }, - }, - options - ); - const actionIds = actionsResult?.body?.hits?.hits?.map( - (e) => (e._source as EndpointAction).action_id - ); + const { actionIds, actionRequests } = await getActionRequestsResult({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, + }); + actionsResult = actionRequests; - // fetch responses with matching `action_id`s - const baseResponsesFilter = [ - { term: { agent_id: elasticAgentId } }, - { terms: { action_id: actionIds } }, - ]; - const responsesFilters = [...baseResponsesFilter, ...dateFilters]; - responsesResult = await esClient.search( - { - index: AGENT_ACTIONS_RESULTS_INDEX, - size: 1000, - body: { - query: { - bool: { - filter: responsesFilters, - }, - }, - }, - }, - options - ); + // fetch responses with matching unique set of `action_id`s + responsesResult = await getActionResponsesResult({ + actionIds: [...new Set(actionIds)], // de-dupe `action_id`s + context, + logger, + elasticAgentId, + startDate, + endDate, + }); } catch (error) { logger.error(error); throw error; @@ -153,21 +122,26 @@ const getActivityLog = async ({ throw new Error(`Error fetching actions log for agent_id ${elasticAgentId}`); } - const responses = responsesResult?.body?.hits?.hits?.length - ? responsesResult?.body?.hits?.hits?.map((e) => ({ - type: 'response', - item: { id: e._id, data: e._source }, - })) - : []; - const actions = actionsResult?.body?.hits?.hits?.length - ? actionsResult?.body?.hits?.hits?.map((e) => ({ - type: 'action', - item: { id: e._id, data: e._source }, - })) - : []; - const sortedData = ([...responses, ...actions] as ActivityLog['data']).sort((a, b) => - new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 - ); + // label record as `action`, `fleetAction` + const responses = categorizeResponseResults({ + results: responsesResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // label record as `response`, `fleetResponse` + const actions = categorizeActionResults({ + results: actionsResult?.body?.hits?.hits as Array< + SearchHit + >, + }); + + // filter out the duplicate endpoint actions that also have fleetActions + // include endpoint actions that have no fleet actions + const uniqueLogData = getUniqueLogData([...responses, ...actions]); + + // sort by @timestamp in desc order, newest first + const sortedData = getTimeSortedData(uniqueLogData); return sortedData; }; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts new file mode 100644 index 000000000000..f75b265bf24d --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/audit_log_helpers.ts @@ -0,0 +1,266 @@ +/* + * 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 { Logger } from 'kibana/server'; +import { SearchRequest } from 'src/plugins/data/public'; +import { SearchHit, SearchResponse } from '@elastic/elasticsearch/api/types'; +import { ApiResponse } from '@elastic/elasticsearch'; +import { AGENT_ACTIONS_INDEX, AGENT_ACTIONS_RESULTS_INDEX } from '../../../../fleet/common'; +import { + ENDPOINT_ACTIONS_INDEX, + ENDPOINT_ACTION_RESPONSES_INDEX, + failedFleetActionErrorCode, +} from '../../../common/endpoint/constants'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { + ActivityLog, + ActivityLogAction, + EndpointActivityLogAction, + ActivityLogActionResponse, + EndpointActivityLogActionResponse, + ActivityLogItemTypes, + EndpointAction, + LogsEndpointAction, + EndpointActionResponse, + LogsEndpointActionResponse, + ActivityLogEntry, +} from '../../../common/endpoint/types'; +import { doesLogsEndpointActionsIndexExist } from '../utils'; + +const actionsIndices = [AGENT_ACTIONS_INDEX, ENDPOINT_ACTIONS_INDEX]; +const responseIndices = [AGENT_ACTIONS_RESULTS_INDEX, ENDPOINT_ACTION_RESPONSES_INDEX]; +export const logsEndpointActionsRegex = new RegExp(`(^\.ds-\.logs-endpoint\.actions-default-).+`); +export const logsEndpointResponsesRegex = new RegExp( + `(^\.ds-\.logs-endpoint\.action\.responses-default-).+` +); +const queryOptions = { + headers: { + 'X-elastic-product-origin': 'fleet', + }, + ignore: [404], +}; + +const getDateFilters = ({ startDate, endDate }: { startDate: string; endDate: string }) => { + return [ + { range: { '@timestamp': { gte: startDate } } }, + { range: { '@timestamp': { lte: endDate } } }, + ]; +}; + +export const getUniqueLogData = (activityLogEntries: ActivityLogEntry[]): ActivityLogEntry[] => { + // find the error responses for actions that didn't make it to fleet index + const onlyResponsesForFleetErrors = activityLogEntries + .filter( + (e) => + e.type === ActivityLogItemTypes.RESPONSE && + e.item.data.error?.code === failedFleetActionErrorCode + ) + .map( + (e: ActivityLogEntry) => (e.item.data as LogsEndpointActionResponse).EndpointActions.action_id + ); + + // all actions and responses minus endpoint actions. + const nonEndpointActionsDocs = activityLogEntries.filter( + (e) => e.type !== ActivityLogItemTypes.ACTION + ); + + // only endpoint actions that match the error responses + const onlyEndpointActionsDocWithoutFleetActions = activityLogEntries + .filter((e) => e.type === ActivityLogItemTypes.ACTION) + .filter((e: ActivityLogEntry) => + onlyResponsesForFleetErrors.includes( + (e.item.data as LogsEndpointAction).EndpointActions.action_id + ) + ); + + // join the error actions and the rest + return [...nonEndpointActionsDocs, ...onlyEndpointActionsDocWithoutFleetActions]; +}; + +export const categorizeResponseResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isResponseDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointResponsesRegex, + index: e._index, + }); + return isResponseDoc + ? { + type: ActivityLogItemTypes.RESPONSE, + item: { id: e._id, data: e._source as LogsEndpointActionResponse }, + } + : { + type: ActivityLogItemTypes.FLEET_RESPONSE, + item: { id: e._id, data: e._source as EndpointActionResponse }, + }; + }) + : []; +}; + +export const categorizeActionResults = ({ + results, +}: { + results: Array>; +}): Array => { + return results?.length + ? results?.map((e) => { + const isActionDoc: boolean = matchesIndexPattern({ + regexPattern: logsEndpointActionsRegex, + index: e._index, + }); + return isActionDoc + ? { + type: ActivityLogItemTypes.ACTION, + item: { id: e._id, data: e._source as LogsEndpointAction }, + } + : { + type: ActivityLogItemTypes.FLEET_ACTION, + item: { id: e._id, data: e._source as EndpointAction }, + }; + }) + : []; +}; + +export const getTimeSortedData = (data: ActivityLog['data']): ActivityLog['data'] => { + return data.sort((a, b) => + new Date(b.item.data['@timestamp']) > new Date(a.item.data['@timestamp']) ? 1 : -1 + ); +}; + +export const getActionRequestsResult = async ({ + context, + logger, + elasticAgentId, + startDate, + endDate, + size, + from, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + startDate: string; + endDate: string; + size: number; + from: number; +}): Promise<{ + actionIds: string[]; + actionRequests: ApiResponse, unknown>; +}> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseActionFilters = [ + { term: { agents: elasticAgentId } }, + { term: { input_type: 'endpoint' } }, + { term: { type: 'INPUT_ACTION' } }, + ]; + const actionsFilters = [...baseActionFilters, ...dateFilters]; + + const hasLogsEndpointActionsIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTIONS_INDEX, + }); + + const actionsSearchQuery: SearchRequest = { + index: hasLogsEndpointActionsIndex ? actionsIndices : AGENT_ACTIONS_INDEX, + size, + from, + body: { + query: { + bool: { + filter: actionsFilters, + }, + }, + sort: [ + { + '@timestamp': { + order: 'desc', + }, + }, + ], + }, + }; + + let actionRequests: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionRequests = await esClient.search(actionsSearchQuery, queryOptions); + const actionIds = actionRequests?.body?.hits?.hits?.map((e) => { + return logsEndpointActionsRegex.test(e._index) + ? (e._source as LogsEndpointAction).EndpointActions.action_id + : (e._source as EndpointAction).action_id; + }); + + return { actionIds, actionRequests }; + } catch (error) { + logger.error(error); + throw error; + } +}; + +export const getActionResponsesResult = async ({ + context, + logger, + elasticAgentId, + actionIds, + startDate, + endDate, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + elasticAgentId: string; + actionIds: string[]; + startDate: string; + endDate: string; +}): Promise, unknown>> => { + const dateFilters = getDateFilters({ startDate, endDate }); + const baseResponsesFilter = [ + { term: { agent_id: elasticAgentId } }, + { terms: { action_id: actionIds } }, + ]; + const responsesFilters = [...baseResponsesFilter, ...dateFilters]; + + const hasLogsEndpointActionResponsesIndex = await doesLogsEndpointActionsIndexExist({ + context, + logger, + indexName: ENDPOINT_ACTION_RESPONSES_INDEX, + }); + + const responsesSearchQuery: SearchRequest = { + index: hasLogsEndpointActionResponsesIndex ? responseIndices : AGENT_ACTIONS_RESULTS_INDEX, + size: 1000, + body: { + query: { + bool: { + filter: responsesFilters, + }, + }, + }, + }; + + let actionResponses: ApiResponse, unknown>; + try { + const esClient = context.core.elasticsearch.client.asCurrentUser; + actionResponses = await esClient.search(responsesSearchQuery, queryOptions); + } catch (error) { + logger.error(error); + throw error; + } + return actionResponses; +}; + +const matchesIndexPattern = ({ + regexPattern, + index, +}: { + regexPattern: RegExp; + index: string; +}): boolean => regexPattern.test(index); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts index 34cabf79aff0..6c40073f8c65 100644 --- a/x-pack/plugins/security_solution/server/endpoint/utils/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/utils/index.ts @@ -7,3 +7,5 @@ export * from './fleet_agent_status_to_endpoint_host_status'; export * from './wrap_errors'; +export * from './audit_log_helpers'; +export * from './yes_no_data_stream'; diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts new file mode 100644 index 000000000000..d2894c8c64c1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.test.ts @@ -0,0 +1,100 @@ +/* + * 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 { + elasticsearchServiceMock, + savedObjectsClientMock, + loggingSystemMock, +} from 'src/core/server/mocks'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; +import { createRouteHandlerContext } from '../mocks'; +import { + doLogsEndpointActionDsExists, + doesLogsEndpointActionsIndexExist, +} from './yes_no_data_stream'; + +describe('Accurately answers if index template for data stream exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent data stream index template', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.existsIndexTemplate = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doLogsEndpointActionDsExists({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + dataStreamName: '.test-stream.name', + }); + expect(doesItExist).toBeTruthy(); + }); +}); + +describe('Accurately answers if index exists', () => { + let ctxt: jest.Mocked; + + beforeEach(() => { + ctxt = createRouteHandlerContext( + elasticsearchServiceMock.createScopedClusterClient(), + savedObjectsClientMock.create() + ); + }); + + const mockEsApiResponse = (response: { body: boolean; statusCode: number }) => { + return jest.fn().mockImplementationOnce(() => Promise.resolve(response)); + }; + + it('Returns FALSE for a non-existent index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: false, + statusCode: 404, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeFalsy(); + }); + + it('Returns TRUE for an existing index', async () => { + ctxt.core.elasticsearch.client.asInternalUser.indices.exists = mockEsApiResponse({ + body: true, + statusCode: 200, + }); + const doesItExist = await doesLogsEndpointActionsIndexExist({ + context: ctxt, + logger: loggingSystemMock.create().get('host-isolation'), + indexName: '.test-index.name-default', + }); + expect(doesItExist).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.ts new file mode 100644 index 000000000000..dea2e46c3c25 --- /dev/null +++ b/x-pack/plugins/security_solution/server/endpoint/utils/yes_no_data_stream.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 { Logger } from 'src/core/server'; +import { SecuritySolutionRequestHandlerContext } from '../../types'; + +export const doLogsEndpointActionDsExists = async ({ + context, + logger, + dataStreamName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + dataStreamName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexTemplateExist = await esClient.indices.existsIndexTemplate({ + name: dataStreamName, + }); + return doesIndexTemplateExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'resource_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; + +export const doesLogsEndpointActionsIndexExist = async ({ + context, + logger, + indexName, +}: { + context: SecuritySolutionRequestHandlerContext; + logger: Logger; + indexName: string; +}): Promise => { + try { + const esClient = context.core.elasticsearch.client.asInternalUser; + const doesIndexExist = await esClient.indices.exists({ + index: indexName, + }); + return doesIndexExist.statusCode === 404 ? false : true; + } catch (error) { + const errorType = error?.type ?? ''; + if (errorType !== 'index_not_found_exception') { + logger.error(error); + throw error; + } + return false; + } +}; From ec3809658fdc0850106c6e2672ae46c3f6621d96 Mon Sep 17 00:00:00 2001 From: Caroline Horn <549577+cchaos@users.noreply.github.com> Date: Mon, 18 Oct 2021 23:56:00 -0400 Subject: [PATCH 16/53] [Unified Integrations] Clean up empty states, tutorial links and routing to prefer unified integrations (#114911) Cleans up the integrations view and redirects all links to the integration manager. --- .../chrome/ui/header/collapsible_nav.tsx | 2 +- .../__snapshots__/add_data.test.tsx.snap | 16 +- .../components/add_data/add_data.test.tsx | 4 +- .../components/add_data/add_data.tsx | 152 +++++++++--------- .../components/sample_data/index.tsx | 4 +- .../components/tutorial_directory.js | 56 +------ .../public/application/components/welcome.tsx | 3 +- src/plugins/home/public/index.ts | 1 - src/plugins/home/public/services/index.ts | 1 - .../home/public/services/tutorials/index.ts | 1 - .../tutorials/tutorial_service.mock.ts | 1 - .../tutorials/tutorial_service.test.tsx | 32 ---- .../services/tutorials/tutorial_service.ts | 18 --- .../empty_index_list_prompt.tsx | 2 +- .../__snapshots__/overview.test.tsx.snap | 110 +++---------- .../public/components/overview/overview.tsx | 12 +- .../public/assets/elastic_beats_card_dark.svg | 1 - .../assets/elastic_beats_card_light.svg | 1 - .../__snapshots__/no_data_page.test.tsx.snap | 4 +- .../elastic_agent_card.test.tsx.snap | 55 ++++++- .../elastic_beats_card.test.tsx.snap | 70 -------- .../no_data_card/elastic_agent_card.test.tsx | 10 +- .../no_data_card/elastic_agent_card.tsx | 44 ++++- .../no_data_card/elastic_beats_card.test.tsx | 45 ------ .../no_data_card/elastic_beats_card.tsx | 66 -------- .../no_data_page/no_data_card/index.ts | 1 - .../no_data_page/no_data_page.tsx | 14 +- .../components/app/RumDashboard/RumHome.tsx | 8 +- .../routing/templates/no_data_config.ts | 10 +- .../epm/components/package_list_grid.tsx | 2 +- .../components/home_integration/index.tsx | 8 - .../tutorial_directory_header_link.tsx | 16 +- .../tutorial_directory_notice.tsx | 147 ----------------- x-pack/plugins/fleet/public/plugin.ts | 7 +- .../infra/public/pages/logs/page_content.tsx | 2 +- .../infra/public/pages/logs/page_template.tsx | 6 +- .../logs/stream/page_no_indices_content.tsx | 4 +- .../infra/public/pages/metrics/index.tsx | 4 +- .../metric_detail/components/invalid_node.tsx | 4 +- .../public/pages/metrics/page_template.tsx | 9 +- .../components/app/header/header_menu.tsx | 2 +- .../public/utils/no_data_config.ts | 7 +- .../security_solution/common/constants.ts | 2 +- .../components/overview_empty/index.test.tsx | 12 +- .../components/overview_empty/index.tsx | 50 ++---- .../translations/translations/ja-JP.json | 11 -- .../translations/translations/zh-CN.json | 11 -- x-pack/test/accessibility/apps/home.ts | 27 ---- 48 files changed, 292 insertions(+), 783 deletions(-) delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg delete mode 100644 src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx delete mode 100644 src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx delete mode 100644 x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_notice.tsx diff --git a/src/core/public/chrome/ui/header/collapsible_nav.tsx b/src/core/public/chrome/ui/header/collapsible_nav.tsx index ad590865b9e1..ccc0e17b655b 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.tsx @@ -362,7 +362,7 @@ export function CollapsibleNav({ iconType="plusInCircleFilled" > {i18n.translate('core.ui.primaryNav.addData', { - defaultMessage: 'Add data', + defaultMessage: 'Add integrations', })} diff --git a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap index 26b5697f008b..de6beab31247 100644 --- a/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap +++ b/src/plugins/home/public/application/components/add_data/__snapshots__/add_data.test.tsx.snap @@ -17,7 +17,7 @@ exports[`AddData render 1`] = ` id="homDataAdd__title" > @@ -43,17 +43,25 @@ exports[`AddData render 1`] = ` grow={false} > diff --git a/src/plugins/home/public/application/components/add_data/add_data.test.tsx b/src/plugins/home/public/application/components/add_data/add_data.test.tsx index 4018ae67c19e..3aa51f89c7d6 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.test.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.test.tsx @@ -27,7 +27,9 @@ beforeEach(() => { jest.clearAllMocks(); }); -const applicationStartMock = {} as unknown as ApplicationStart; +const applicationStartMock = { + capabilities: { navLinks: { integrations: true } }, +} as unknown as ApplicationStart; const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); diff --git a/src/plugins/home/public/application/components/add_data/add_data.tsx b/src/plugins/home/public/application/components/add_data/add_data.tsx index 97ba28a04a07..50d6079dd8df 100644 --- a/src/plugins/home/public/application/components/add_data/add_data.tsx +++ b/src/plugins/home/public/application/components/add_data/add_data.tsx @@ -22,8 +22,6 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { METRIC_TYPE } from '@kbn/analytics'; import { ApplicationStart } from 'kibana/public'; import { createAppNavigationHandler } from '../app_navigation_handler'; -// @ts-expect-error untyped component -import { Synopsis } from '../synopsis'; import { getServices } from '../../kibana_services'; import { RedirectAppLinks } from '../../../../../kibana_react/public'; @@ -35,87 +33,91 @@ interface Props { export const AddData: FC = ({ addBasePath, application, isDarkMode }) => { const { trackUiMetric } = getServices(); + const canAccessIntegrations = application.capabilities.navLinks.integrations; + if (canAccessIntegrations) { + return ( + <> +
+ + + +

+ +

+
- return ( - <> -
- - - -

- -

-
+ - + +

+ +

+
- -

- -

-
+ - + + + + {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} + { + trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); + createAppNavigationHandler('/app/integrations/browse')(event); + }} + > + + + + - - - - {/* eslint-disable-next-line @elastic/eui/href-or-on-click */} - { - trackUiMetric(METRIC_TYPE.CLICK, 'home_tutorial_directory'); - createAppNavigationHandler('/app/home#/tutorial_directory')(event); - }} + + - - - - - - - - - - -
+ + +
+ - - - - -
+ + + +
+
- - - ); + + + ); + } else { + return null; + } }; diff --git a/src/plugins/home/public/application/components/sample_data/index.tsx b/src/plugins/home/public/application/components/sample_data/index.tsx index d6b9328f57e9..b65fbb5d002b 100644 --- a/src/plugins/home/public/application/components/sample_data/index.tsx +++ b/src/plugins/home/public/application/components/sample_data/index.tsx @@ -40,7 +40,7 @@ export function SampleDataCard({ urlBasePath, onDecline, onConfirm }: Props) { image={cardGraphicURL} textAlign="left" title={ - + } description={ - + { - const notices = getServices().tutorialService.getDirectoryNotices(); - return notices.length ? ( - - {notices.map((DirectoryNotice, index) => ( - - - - ))} - - ) : null; - }; - renderHeaderLinks = () => { const headerLinks = getServices().tutorialService.getDirectoryHeaderLinks(); return headerLinks.length ? ( @@ -245,7 +203,6 @@ class TutorialDirectoryUi extends React.Component { render() { const headerLinks = this.renderHeaderLinks(); const tabs = this.getTabs(); - const notices = this.renderNotices(); return ( + ), tabs, rightSideItems: headerLinks ? [headerLinks] : [], }} > - {notices && ( - <> - {notices} - - - )} {this.renderTabContent()} ); diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index ca7e6874c75c..03dff22c7b33 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -48,8 +48,7 @@ export class Welcome extends React.Component { }; private redirecToAddData() { - const path = this.services.addBasePath('#/tutorial_directory'); - window.location.href = path; + this.services.application.navigateToApp('integrations', { path: '/browse' }); } private onSampleDataDecline = () => { diff --git a/src/plugins/home/public/index.ts b/src/plugins/home/public/index.ts index dd02bf65dd8b..7abaf5d19f00 100644 --- a/src/plugins/home/public/index.ts +++ b/src/plugins/home/public/index.ts @@ -23,7 +23,6 @@ export type { FeatureCatalogueSolution, Environment, TutorialVariables, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './services'; diff --git a/src/plugins/home/public/services/index.ts b/src/plugins/home/public/services/index.ts index 65913df6310b..2ee68a9eef0c 100644 --- a/src/plugins/home/public/services/index.ts +++ b/src/plugins/home/public/services/index.ts @@ -22,7 +22,6 @@ export { TutorialService } from './tutorials'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorials'; diff --git a/src/plugins/home/public/services/tutorials/index.ts b/src/plugins/home/public/services/tutorials/index.ts index 8de12c31249d..e007a5ea4d55 100644 --- a/src/plugins/home/public/services/tutorials/index.ts +++ b/src/plugins/home/public/services/tutorials/index.ts @@ -11,7 +11,6 @@ export { TutorialService } from './tutorial_service'; export type { TutorialVariables, TutorialServiceSetup, - TutorialDirectoryNoticeComponent, TutorialDirectoryHeaderLinkComponent, TutorialModuleNoticeComponent, } from './tutorial_service'; diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts index 0c109d61912c..ab38a32a1a5b 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.mock.ts @@ -25,7 +25,6 @@ const createMock = (): jest.Mocked> => { const service = { setup: jest.fn(), getVariables: jest.fn(() => ({})), - getDirectoryNotices: jest.fn(() => []), getDirectoryHeaderLinks: jest.fn(() => []), getModuleNotices: jest.fn(() => []), getCustomStatusCheck: jest.fn(), diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx index a88cf526e371..b90165aafb45 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx +++ b/src/plugins/home/public/services/tutorials/tutorial_service.test.tsx @@ -27,22 +27,6 @@ describe('TutorialService', () => { }).toThrow(); }); - test('allows multiple register directory notice calls', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('def', () => ); - }).not.toThrow(); - }); - - test('throws when same directory notice is registered twice', () => { - const setup = new TutorialService().setup(); - expect(() => { - setup.registerDirectoryNotice('abc', () =>
); - setup.registerDirectoryNotice('abc', () => ); - }).toThrow(); - }); - test('allows multiple register directory header link calls', () => { const setup = new TutorialService().setup(); expect(() => { @@ -91,22 +75,6 @@ describe('TutorialService', () => { }); }); - describe('getDirectoryNotices', () => { - test('returns empty array', () => { - const service = new TutorialService(); - expect(service.getDirectoryNotices()).toEqual([]); - }); - - test('returns last state of register calls', () => { - const service = new TutorialService(); - const setup = service.setup(); - const notices = [() =>
, () => ]; - setup.registerDirectoryNotice('abc', notices[0]); - setup.registerDirectoryNotice('def', notices[1]); - expect(service.getDirectoryNotices()).toEqual(notices); - }); - }); - describe('getDirectoryHeaderLinks', () => { test('returns empty array', () => { const service = new TutorialService(); diff --git a/src/plugins/home/public/services/tutorials/tutorial_service.ts b/src/plugins/home/public/services/tutorials/tutorial_service.ts index 839b0702a499..81b6bbe72e3e 100644 --- a/src/plugins/home/public/services/tutorials/tutorial_service.ts +++ b/src/plugins/home/public/services/tutorials/tutorial_service.ts @@ -11,9 +11,6 @@ import React from 'react'; /** @public */ export type TutorialVariables = Partial>; -/** @public */ -export type TutorialDirectoryNoticeComponent = React.FC; - /** @public */ export type TutorialDirectoryHeaderLinkComponent = React.FC; @@ -27,7 +24,6 @@ type CustomComponent = () => Promise; export class TutorialService { private tutorialVariables: TutorialVariables = {}; - private tutorialDirectoryNotices: { [key: string]: TutorialDirectoryNoticeComponent } = {}; private tutorialDirectoryHeaderLinks: { [key: string]: TutorialDirectoryHeaderLinkComponent; } = {}; @@ -47,16 +43,6 @@ export class TutorialService { this.tutorialVariables[key] = value; }, - /** - * Registers a component that will be rendered at the top of tutorial directory page. - */ - registerDirectoryNotice: (id: string, component: TutorialDirectoryNoticeComponent) => { - if (this.tutorialDirectoryNotices[id]) { - throw new Error(`directory notice ${id} already set`); - } - this.tutorialDirectoryNotices[id] = component; - }, - /** * Registers a component that will be rendered next to tutorial directory title/header area. */ @@ -94,10 +80,6 @@ export class TutorialService { return this.tutorialVariables; } - public getDirectoryNotices() { - return Object.values(this.tutorialDirectoryNotices); - } - public getDirectoryHeaderLinks() { return Object.values(this.tutorialDirectoryHeaderLinks); } diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx index 1331eb9b7c4a..d00f9e2368e2 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_index_list_prompt/empty_index_list_prompt.tsx @@ -91,7 +91,7 @@ export const EmptyIndexListPrompt = ({ { - navigateToApp('home', { path: '#/tutorial_directory' }); + navigateToApp('home', { path: '/app/integrations/browse' }); closeFlyout(); }} icon={} diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index 6da2f95fa394..babcab15a497 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -226,10 +226,7 @@ exports[`Overview render 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -259,11 +256,7 @@ exports[`Overview render 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -533,10 +526,7 @@ exports[`Overview without features 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -563,16 +553,10 @@ exports[`Overview without features 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", + "/app/integrations/browse", ], Array [ - "home#/tutorial_directory", - ], - Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -602,11 +586,7 @@ exports[`Overview without features 1`] = ` "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -642,19 +622,11 @@ exports[`Overview without features 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "/app/home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -801,10 +773,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -831,20 +800,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -880,11 +842,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -898,10 +856,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -928,20 +883,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -977,11 +925,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } @@ -1001,10 +945,7 @@ exports[`Overview without solutions 1`] = ` [MockFunction] { "calls": Array [ Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], Array [ "kibana_landing_page", @@ -1031,20 +972,13 @@ exports[`Overview without solutions 1`] = ` "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], Array [ - "/app/home#/tutorial_directory", - ], - Array [ - "home#/tutorial_directory", + "/app/integrations/browse", ], ], "results": Array [ Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, Object { "type": "return", @@ -1080,11 +1014,7 @@ exports[`Overview without solutions 1`] = ` }, Object { "type": "return", - "value": "/app/home#/tutorial_directory", - }, - Object { - "type": "return", - "value": "home#/tutorial_directory", + "value": "/app/integrations/browse", }, ], } diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 07769e2f3c47..6a0279bd1246 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -61,7 +61,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => const IS_DARK_THEME = uiSettings.get('theme:darkMode'); // Home does not have a locator implemented, so hard-code it here. - const addDataHref = addBasePath('/app/home#/tutorial_directory'); + const addDataHref = addBasePath('/app/integrations/browse'); const devToolsHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({}); const managementHref = share.url.locators .get('MANAGEMENT_APP_LOCATOR') @@ -86,8 +86,14 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => }), logo: 'logoKibana', actions: { - beats: { - href: addBasePath(`home#/tutorial_directory`), + elasticAgent: { + title: i18n.translate('kibanaOverview.noDataConfig.title', { + defaultMessage: 'Add integrations', + }), + description: i18n.translate('kibanaOverview.noDataConfig.description', { + defaultMessage: + 'Use Elastic Agent or Beats to collect data and build out Analytics solutions.', + }), }, }, docsLink: docLinks.links.kibana, diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg deleted file mode 100644 index 8652d8d92150..000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg b/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg deleted file mode 100644 index f54786c1b950..000000000000 --- a/src/plugins/kibana_react/public/assets/elastic_beats_card_light.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap index d8bc5745ec8e..8842a3c9f584 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/__snapshots__/no_data_page.test.tsx.snap @@ -73,9 +73,9 @@ exports[`NoDataPage render 1`] = ` - diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index 3f72ae5597a9..f66d05140b2e 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -13,7 +13,36 @@ exports[`ElasticAgentCard props button 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } +/> +`; + +exports[`ElasticAgentCard props category 1`] = ` + + Add Elastic Agent + + } + href="/app/integrations/browse/custom" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } /> `; @@ -30,7 +59,13 @@ exports[`ElasticAgentCard props href 1`] = ` href="#" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -48,7 +83,13 @@ exports[`ElasticAgentCard props recommended 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; @@ -65,6 +106,12 @@ exports[`ElasticAgentCard renders 1`] = ` href="/app/integrations/browse" image="/plugins/kibanaReact/assets/elastic_agent_card.svg" paddingSize="l" - title="Add Elastic Agent" + title={ + + + Add Elastic Agent + + + } /> `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap deleted file mode 100644 index af26f9e93eba..000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_beats_card.test.tsx.snap +++ /dev/null @@ -1,70 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`ElasticBeatsCard props button 1`] = ` - - Button - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props href 1`] = ` - - Button - - } - href="#" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard props recommended 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; - -exports[`ElasticBeatsCard renders 1`] = ` - - Add data - - } - href="/app/home#/tutorial_directory" - image="/plugins/kibanaReact/assets/elastic_beats_card_light.svg" - paddingSize="l" - title="Add data" -/> -`; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx index 45cc32cae06d..b971abf06a43 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.test.tsx @@ -14,7 +14,10 @@ jest.mock('../../../context', () => ({ ...jest.requireActual('../../../context'), useKibana: jest.fn().mockReturnValue({ services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, + http: { + basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) }, + }, + application: { capabilities: { navLinks: { integrations: true } } }, uiSettings: { get: jest.fn() }, }, }), @@ -41,5 +44,10 @@ describe('ElasticAgentCard', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); + + test('category', () => { + const component = shallow(); + expect(component).toMatchSnapshot(); + }); }); }); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index f071bd9fab25..5a91e568471d 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -9,7 +9,7 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; +import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../context'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; @@ -27,13 +27,40 @@ export const ElasticAgentCard: FunctionComponent = ({ href, button, layout, + category, ...cardRest }) => { const { - services: { http }, + services: { http, application }, } = useKibana(); const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; + const image = addBasePath(`/plugins/kibanaReact/assets/elastic_agent_card.svg`); + const canAccessFleet = application.capabilities.navLinks.integrations; + const hasCategory = category ? `/${category}` : ''; + + if (!canAccessFleet) { + return ( + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.title', { + defaultMessage: `Contact your administrator`, + })} + + } + description={ + + {i18n.translate('kibana-react.noDataPage.elasticAgentCard.noPermission.description', { + defaultMessage: `This integration is not yet enabled. Your administrator has the required permissions to turn it on.`, + })} + + } + isDisabled + /> + ); + } const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticAgentCard.title', { defaultMessage: 'Add Elastic Agent', @@ -51,12 +78,17 @@ export const ElasticAgentCard: FunctionComponent = ({ return ( + {defaultCTAtitle} + + } description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', { defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`, })} - image={addBasePath(`${basePathUrl}elastic_agent_card.svg`)} betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} footer={footer} layout={layout as 'vertical' | undefined} diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx deleted file mode 100644 index 6ea41bf6b3e1..000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.test.tsx +++ /dev/null @@ -1,45 +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 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 { shallow } from 'enzyme'; -import React from 'react'; -import { ElasticBeatsCard } from './elastic_beats_card'; - -jest.mock('../../../context', () => ({ - ...jest.requireActual('../../../context'), - useKibana: jest.fn().mockReturnValue({ - services: { - http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, - uiSettings: { get: jest.fn() }, - }, - }), -})); - -describe('ElasticBeatsCard', () => { - test('renders', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - describe('props', () => { - test('recommended', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('button', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - - test('href', () => { - const component = shallow(); - expect(component).toMatchSnapshot(); - }); - }); -}); diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx deleted file mode 100644 index 0372d1209648..000000000000 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_beats_card.tsx +++ /dev/null @@ -1,66 +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 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, { FunctionComponent } from 'react'; -import { i18n } from '@kbn/i18n'; -import { CoreStart } from 'kibana/public'; -import { EuiButton, EuiCard } from '@elastic/eui'; -import { useKibana } from '../../../context'; -import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; - -export type ElasticBeatsCardProps = NoDataPageActions & { - solution: string; -}; - -export const ElasticBeatsCard: FunctionComponent = ({ - recommended, - title, - button, - href, - solution, // unused for now - layout, - ...cardRest -}) => { - const { - services: { http, uiSettings }, - } = useKibana(); - const addBasePath = http.basePath.prepend; - const basePathUrl = '/plugins/kibanaReact/assets/'; - const IS_DARK_THEME = uiSettings.get('theme:darkMode'); - - const defaultCTAtitle = i18n.translate('kibana-react.noDataPage.elasticBeatsCard.title', { - defaultMessage: 'Add data', - }); - - const footer = - typeof button !== 'string' && typeof button !== 'undefined' ? ( - button - ) : ( - // The href and/or onClick are attached to the whole Card, so the button is just for show. - // Do not add the behavior here too or else it will propogate through - {button || title || defaultCTAtitle} - ); - - return ( - - ); -}; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts index 3744239d9a47..e05d4d9675ca 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/index.ts @@ -7,5 +7,4 @@ */ export * from './elastic_agent_card'; -export * from './elastic_beats_card'; export * from './no_data_card'; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx index 56eb0f34617d..b2d9ef6ca500 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_page.tsx @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { KibanaPageTemplateProps } from '../page_template'; -import { ElasticAgentCard, ElasticBeatsCard, NoDataCard } from './no_data_card'; +import { ElasticAgentCard, NoDataCard } from './no_data_card'; import { KibanaPageTemplateSolutionNavAvatar } from '../solution_nav'; export const NO_DATA_PAGE_MAX_WIDTH = 950; @@ -55,6 +55,10 @@ export type NoDataPageActions = Partial & { * Remapping `onClick` to any element */ onClick?: MouseEventHandler; + /** + * Category to auto-select within Fleet + */ + category?: string; }; export type NoDataPageActionsProps = Record; @@ -107,18 +111,12 @@ export const NoDataPage: FunctionComponent = ({ const actionsKeys = Object.keys(sortedData); const renderActions = useMemo(() => { return Object.values(sortedData).map((action, i) => { - if (actionsKeys[i] === 'elasticAgent') { + if (actionsKeys[i] === 'elasticAgent' || actionsKeys[i] === 'beats') { return ( ); - } else if (actionsKeys[i] === 'beats') { - return ( - - - - ); } else { return ( ), discussForumLink: ( - + import('./tutorial_directory_notice')); -export const TutorialDirectoryNotice: TutorialDirectoryNoticeComponent = () => ( - }> - - -); - const TutorialDirectoryHeaderLinkLazy = React.lazy( () => import('./tutorial_directory_header_link') ); diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx index 074a1c40bdb1..18fdd875c737 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_directory_header_link.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { memo, useState, useEffect } from 'react'; +import React, { memo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty } from '@elastic/eui'; import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/public'; @@ -13,25 +13,15 @@ import type { TutorialDirectoryHeaderLinkComponent } from 'src/plugins/home/publ import { RedirectAppLinks } from '../../../../../../src/plugins/kibana_react/public'; import { useLink, useCapabilities, useStartServices } from '../../hooks'; -import { tutorialDirectoryNoticeState$ } from './tutorial_directory_notice'; - const TutorialDirectoryHeaderLink: TutorialDirectoryHeaderLinkComponent = memo(() => { const { getHref } = useLink(); const { application } = useStartServices(); const { show: hasIngestManager } = useCapabilities(); - const [noticeState, setNoticeState] = useState({ + const [noticeState] = useState({ settingsDataLoaded: false, - hasSeenNotice: false, }); - useEffect(() => { - const subscription = tutorialDirectoryNoticeState$.subscribe((value) => setNoticeState(value)); - return () => { - subscription.unsubscribe(); - }; - }, []); - - return hasIngestManager && noticeState.settingsDataLoaded && noticeState.hasSeenNotice ? ( + return hasIngestManager && noticeState.settingsDataLoaded ? ( { - const { getHref } = useLink(); - const { application } = useStartServices(); - const { show: hasIngestManager } = useCapabilities(); - const { data: settingsData, isLoading } = useGetSettings(); - const [dismissedNotice, setDismissedNotice] = useState(false); - - const dismissNotice = useCallback(async () => { - setDismissedNotice(true); - await sendPutSettings({ - has_seen_add_data_notice: true, - }); - }, []); - - useEffect(() => { - tutorialDirectoryNoticeState$.next({ - settingsDataLoaded: !isLoading, - hasSeenNotice: Boolean(dismissedNotice || settingsData?.item?.has_seen_add_data_notice), - }); - }, [isLoading, settingsData, dismissedNotice]); - - const hasSeenNotice = - isLoading || settingsData?.item?.has_seen_add_data_notice || dismissedNotice; - - return hasIngestManager && !hasSeenNotice ? ( - <> - - - - ), - }} - /> - } - > -

- - - - ), - }} - /> -

- - -
- - - - - -
-
- -
- { - dismissNotice(); - }} - > - - -
-
-
-
- - - ) : null; -}); - -// Needed for React.lazy -// eslint-disable-next-line import/no-default-export -export default TutorialDirectoryNotice; diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index e1f263b0763e..4a2a6900cc78 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -44,11 +44,7 @@ import { CUSTOM_LOGS_INTEGRATION_NAME, INTEGRATIONS_BASE_PATH } from './constant import { licenseService } from './hooks'; import { setHttpClient } from './hooks/use_request'; import { createPackageSearchProvider } from './search_provider'; -import { - TutorialDirectoryNotice, - TutorialDirectoryHeaderLink, - TutorialModuleNotice, -} from './components/home_integration'; +import { TutorialDirectoryHeaderLink, TutorialModuleNotice } from './components/home_integration'; import { createExtensionRegistrationCallback } from './services/ui_extensions'; import type { UIExtensionRegistrationCallback, UIExtensionsStorage } from './types'; import { LazyCustomLogsAssetsExtension } from './lazy_custom_logs_assets_extension'; @@ -197,7 +193,6 @@ export class FleetPlugin implements Plugin { diff --git a/x-pack/plugins/infra/public/pages/logs/page_template.tsx b/x-pack/plugins/infra/public/pages/logs/page_template.tsx index 7ee60ab84bf2..6de13b495f0b 100644 --- a/x-pack/plugins/infra/public/pages/logs/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/logs/page_template.tsx @@ -44,13 +44,13 @@ export const LogsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.title', { - defaultMessage: 'Add logs with Beats', + defaultMessage: 'Add a logging integration', }), description: i18n.translate('xpack.infra.logs.noDataConfig.beatsCard.description', { defaultMessage: - 'Use Beats to send logs to Elasticsearch. We make it easy with modules for many popular systems and apps.', + 'Use the Elastic Agent or Beats to send logs to Elasticsearch. We make it easy with integrations for many popular systems and apps.', }), - href: basePath + `/app/home#/tutorial_directory/logging`, + href: basePath + `/app/integrations/browse`, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx index bc3bc22f3f1b..2259a8d3528a 100644 --- a/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx +++ b/x-pack/plugins/infra/public/pages/logs/stream/page_no_indices_content.tsx @@ -22,8 +22,8 @@ export const LogsPageNoIndicesContent = () => { const canConfigureSource = application?.capabilities?.logs?.configureSource ? true : false; const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/logging', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index ae375dc504e7..1a79cd996087 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -93,9 +93,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx index 2a436eac30b2..17e6382ce65c 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/components/invalid_node.tsx @@ -18,8 +18,8 @@ interface InvalidNodeErrorProps { export const InvalidNodeError: React.FunctionComponent = ({ nodeName }) => { const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', + app: 'integrations', + hash: '/browse', }); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx index 41ea12c28084..4da671283644 100644 --- a/x-pack/plugins/infra/public/pages/metrics/page_template.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/page_template.tsx @@ -10,7 +10,6 @@ import { i18n } from '@kbn/i18n'; import { useKibanaContextForPlugin } from '../../hooks/use_kibana'; import type { LazyObservabilityPageTemplateProps } from '../../../../observability/public'; import { KibanaPageTemplateProps } from '../../../../../../src/plugins/kibana_react/public'; -import { useLinkProps } from '../../hooks/use_link_props'; interface MetricsPageTemplateProps extends LazyObservabilityPageTemplateProps { hasData?: boolean; @@ -30,11 +29,6 @@ export const MetricsPageTemplate: React.FC = ({ }, } = useKibanaContextForPlugin(); - const tutorialLinkProps = useLinkProps({ - app: 'home', - hash: '/tutorial_directory/metrics', - }); - const noDataConfig: KibanaPageTemplateProps['noDataConfig'] = hasData ? undefined : { @@ -44,13 +38,12 @@ export const MetricsPageTemplate: React.FC = ({ actions: { beats: { title: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.title', { - defaultMessage: 'Add metrics with Beats', + defaultMessage: 'Add a metrics integration', }), description: i18n.translate('xpack.infra.metrics.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats to send metrics data to Elasticsearch. We make it easy with modules for many popular systems and apps.', }), - ...tutorialLinkProps, }, }, docsLink: docLinks.links.observability.guide, diff --git a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx index 707cb241501f..0ed01b7d3673 100644 --- a/x-pack/plugins/observability/public/components/app/header/header_menu.tsx +++ b/x-pack/plugins/observability/public/components/app/header/header_menu.tsx @@ -26,7 +26,7 @@ export function ObservabilityHeaderMenu(): React.ReactElement | null { {addDataLinkText} diff --git a/x-pack/plugins/observability/public/utils/no_data_config.ts b/x-pack/plugins/observability/public/utils/no_data_config.ts index 1e16fb145bdc..2c87b1434a0b 100644 --- a/x-pack/plugins/observability/public/utils/no_data_config.ts +++ b/x-pack/plugins/observability/public/utils/no_data_config.ts @@ -24,12 +24,15 @@ export function getNoDataConfig({ defaultMessage: 'Observability', }), actions: { - beats: { + elasticAgent: { + title: i18n.translate('xpack.observability.noDataConfig.beatsCard.title', { + defaultMessage: 'Add integrations', + }), description: i18n.translate('xpack.observability.noDataConfig.beatsCard.description', { defaultMessage: 'Use Beats and APM agents to send observability data to Elasticsearch. We make it easy with support for many popular systems, apps, and languages.', }), - href: basePath.prepend(`/app/home#/tutorial_directory/logging`), + href: basePath.prepend(`/app/integrations`), }, }, docsLink, diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5c41e92661e5..5a7e19e2cdd0 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -16,7 +16,7 @@ export const APP_NAME = 'Security'; export const APP_ICON = 'securityAnalyticsApp'; export const APP_ICON_SOLUTION = 'logoSecurity'; export const APP_PATH = `/app/security`; -export const ADD_DATA_PATH = `/app/home#/tutorial_directory/security`; +export const ADD_DATA_PATH = `/app/integrations/browse/security`; export const DEFAULT_BYTES_FORMAT = 'format:bytes:defaultPattern'; export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx index 5fa2725f9ee6..61e9e66f1bb8 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.test.tsx @@ -45,9 +45,10 @@ describe('OverviewEmpty', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { elasticAgent: { + category: 'security', description: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - href: '/app/integrations/browse/security', + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', @@ -68,8 +69,11 @@ describe('OverviewEmpty', () => { it('render with correct actions ', () => { expect(wrapper.find('[data-test-subj="empty-page"]').prop('noDataConfig')).toEqual({ actions: { - beats: { - href: '/app/home#/tutorial_directory/security', + elasticAgent: { + category: 'security', + description: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + title: 'Add a Security integration', }, }, docsLink: 'https://www.elastic.co/guide/en/security/mocked-test-branch/index.html', diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx index bc7633394319..9b20c079002e 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_empty/index.tsx @@ -5,13 +5,10 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React from 'react'; import { i18n } from '@kbn/i18n'; import { useKibana } from '../../../common/lib/kibana'; -import { ADD_DATA_PATH } from '../../../../common/constants'; -import { pagePathGetters } from '../../../../../fleet/public'; import { SOLUTION_NAME } from '../../../../public/common/translations'; -import { useUserPrivileges } from '../../../common/components/user_privileges'; import { KibanaPageTemplate, @@ -19,42 +16,27 @@ import { } from '../../../../../../../src/plugins/kibana_react/public'; const OverviewEmptyComponent: React.FC = () => { - const { http, docLinks } = useKibana().services; - const basePath = http.basePath.get(); - const canAccessFleet = useUserPrivileges().endpointPrivileges.canAccessFleet; - const integrationsPathComponents = pagePathGetters.integrations_all({ category: 'security' }); - - const agentAction: NoDataPageActionsProps = useMemo( - () => ({ - elasticAgent: { - href: `${basePath}${integrationsPathComponents[0]}${integrationsPathComponents[1]}`, - description: i18n.translate( - 'xpack.securitySolution.pages.emptyPage.beatsCard.description', - { - defaultMessage: - 'Use Elastic Agent to collect security events and protect your endpoints from threats. Manage your agents in Fleet and add integrations with a single click.', - } - ), - }, - }), - [basePath, integrationsPathComponents] - ); - - const beatsAction: NoDataPageActionsProps = useMemo( - () => ({ - beats: { - href: `${basePath}${ADD_DATA_PATH}`, - }, - }), - [basePath] - ); + const { docLinks } = useKibana().services; + + const agentAction: NoDataPageActionsProps = { + elasticAgent: { + category: 'security', + title: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.title', { + defaultMessage: 'Add a Security integration', + }), + description: i18n.translate('xpack.securitySolution.pages.emptyPage.beatsCard.description', { + defaultMessage: + 'Use Elastic Agent to collect security events and protect your endpoints from threats.', + }), + }, + }; return ( diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 3df5e4ee6c48..852b01977b78 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3009,11 +3009,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 件中 {errorsLength} 件の kibana オブジェクトが追加できません。エラー:{errorMessage}", "home.tutorial.selectionLegend": "デプロイタイプ", "home.tutorial.selfManagedButtonLabel": "自己管理", - "home.tutorial.tabs.allTitle": "すべて", - "home.tutorial.tabs.loggingTitle": "ログ", - "home.tutorial.tabs.metricsTitle": "メトリック", "home.tutorial.tabs.sampleDataTitle": "サンプルデータ", - "home.tutorial.tabs.securitySolutionTitle": "セキュリティ", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "予期せぬステータス確認ステータス {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "予期せぬ指示タイプ {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "一般的なアプリやサービスからデータを取り込みます。", @@ -4204,8 +4200,6 @@ "kibana-react.noDataPage.cantDecide.link": "詳細については、ドキュメントをご確認ください。", "kibana-react.noDataPage.elasticAgentCard.description": "Elasticエージェントを使用すると、シンプルで統一された方法でコンピューターからデータを収集するできます。", "kibana-react.noDataPage.elasticAgentCard.title": "Elasticエージェントの追加", - "kibana-react.noDataPage.elasticBeatsCard.description": "Beatsを使用して、さまざまなシステムのデータをElasticsearchに追加します。", - "kibana-react.noDataPage.elasticBeatsCard.title": "データの追加", "kibana-react.noDataPage.intro": "データを追加して開始するか、{solution}については{link}をご覧ください。", "kibana-react.noDataPage.intro.link": "詳細", "kibana-react.noDataPage.noDataPage.recommended": "推奨", @@ -11076,12 +11070,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "エージェントをFleetサーバーに登録", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleetサーバーが使用できます。スケーラビリティとセキュリティが改善されています。{existingAgentsMessage} Fleetを使用し続けるには、Fleetサーバーと新しいバージョンのElasticエージェントを各ホストにインストールする必要があります。詳細については、{link}をご覧ください。", "xpack.fleet.genericActionsMenuText": "開く", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "メッセージを消去", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "統合を試す", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "Elasticエージェント統合では、シンプルかつ統合された方法で、ログ、メトリック、他の種類のデータの監視をホストに追加することができます。複数のBeatsをインストールする必要はありません。このため、インフラストラクチャ全体でのポリシーのデプロイが簡単で高速になりました。詳細については、{blogPostLink}をお読みください。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "発表ブログ投稿", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix} Elasticエージェント統合", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "一般公開へ:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}このモジュールの新しいバージョンは{availableAsIntegrationLink}です。統合と新しいElasticエージェントの詳細については、{blogPostLink}をお読みください。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "発表ブログ投稿", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "Elasticエージェント統合として提供", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index d9af3edb8101..9d88c757f1e5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3038,11 +3038,7 @@ "home.tutorial.savedObject.unableToAddErrorMessage": "{savedObjectsLength} 个 kibana 对象中有 {errorsLength} 个无法添加,错误:{errorMessage}", "home.tutorial.selectionLegend": "部署类型", "home.tutorial.selfManagedButtonLabel": "自管型", - "home.tutorial.tabs.allTitle": "全部", - "home.tutorial.tabs.loggingTitle": "日志", - "home.tutorial.tabs.metricsTitle": "指标", "home.tutorial.tabs.sampleDataTitle": "样例数据", - "home.tutorial.tabs.securitySolutionTitle": "安全", "home.tutorial.unexpectedStatusCheckStateErrorDescription": "意外的状态检查状态 {statusCheckState}", "home.tutorial.unhandledInstructionTypeErrorDescription": "未处理的指令类型 {visibleInstructions}", "home.tutorialDirectory.featureCatalogueDescription": "从热门应用和服务中采集数据。", @@ -4244,8 +4240,6 @@ "kibana-react.noDataPage.cantDecide.link": "请参阅我们的文档以了解更多信息。", "kibana-react.noDataPage.elasticAgentCard.description": "使用 Elastic 代理以简单统一的方式从您的计算机中收集数据。", "kibana-react.noDataPage.elasticAgentCard.title": "添加 Elastic 代理", - "kibana-react.noDataPage.elasticBeatsCard.description": "使用 Beats 将各种系统的数据添加到 Elasticsearch。", - "kibana-react.noDataPage.elasticBeatsCard.title": "添加数据", "kibana-react.noDataPage.intro": "添加您的数据以开始,或{link}{solution}。", "kibana-react.noDataPage.intro.link": "了解详情", "kibana-react.noDataPage.noDataPage.recommended": "推荐", @@ -11191,12 +11185,7 @@ "xpack.fleet.fleetServerUpgradeModal.modalTitle": "将代理注册到 Fleet 服务器", "xpack.fleet.fleetServerUpgradeModal.onPremDescriptionMessage": "Fleet 服务器现在可用且提供改善的可扩展性和安全性。{existingAgentsMessage}要继续使用 Fleet,必须在各个主机上安装 Fleet 服务器和新版 Elastic 代理。详细了解我们的 {link}。", "xpack.fleet.genericActionsMenuText": "打开", - "xpack.fleet.homeIntegration.tutorialDirectory.dismissNoticeButtonText": "关闭消息", "xpack.fleet.homeIntegration.tutorialDirectory.fleetAppButtonText": "试用集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText": "通过 Elastic 代理集成,可以简单统一的方式将日志、指标和其他类型数据的监测添加到主机。不再需要安装多个 Beats,这样将策略部署到整个基础架构更容易也更快速。有关更多信息,请阅读我们的{blogPostLink}。", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeText.blogPostLink": "公告博客", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle": "{newPrefix}Elastic 代理集成", - "xpack.fleet.homeIntegration.tutorialDirectory.noticeTitle.newPrefix": "已正式发布:", "xpack.fleet.homeIntegration.tutorialModule.noticeText": "{notePrefix}此模块的较新版本{availableAsIntegrationLink}。要详细了解集成和新 Elastic 代理,请阅读我们的{blogPostLink}。", "xpack.fleet.homeIntegration.tutorialModule.noticeText.blogPostLink": "公告博客", "xpack.fleet.homeIntegration.tutorialModule.noticeText.integrationLink": "将作为 Elastic 代理集成来提供", diff --git a/x-pack/test/accessibility/apps/home.ts b/x-pack/test/accessibility/apps/home.ts index a7158d9579b6..61297859c29f 100644 --- a/x-pack/test/accessibility/apps/home.ts +++ b/x-pack/test/accessibility/apps/home.ts @@ -64,33 +64,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await a11y.testAppSnapshot(); }); - it('Add data page meets a11y requirements ', async () => { - await home.clickGoHome(); - await testSubjects.click('homeAddData'); - await a11y.testAppSnapshot(); - }); - - it('Sample data page meets a11y requirements ', async () => { - await testSubjects.click('homeTab-sampleData'); - await a11y.testAppSnapshot(); - }); - - it('click on Add logs panel to open all log examples page meets a11y requirements ', async () => { - await testSubjects.click('sampleDataSetCardlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on ActiveMQ logs panel to open tutorial meets a11y requirements', async () => { - await testSubjects.click('homeTab-all'); - await testSubjects.click('homeSynopsisLinkactivemqlogs'); - await a11y.testAppSnapshot(); - }); - - it('click on cloud tutorial meets a11y requirements', async () => { - await testSubjects.click('onCloudTutorial'); - await a11y.testAppSnapshot(); - }); - it('passes with searchbox open', async () => { await testSubjects.click('nav-search-popover'); await a11y.testAppSnapshot(); From fd0fc770623b483a0eb7de48a4e3407446de9bd7 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 18 Oct 2021 22:24:20 -0600 Subject: [PATCH 17/53] Fixes console errors seen (#115448) ## Summary During testing I encountered this error message: ``` [2021-10-18T13:19:07.053-06:00][ERROR][plugins.securitySolution] The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-null "to": 2021-10-18T19:19:00.835Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected. ``` This error was happening whenever I had a rule that was using an immediately invoked action and was encountering an error such as a non ECS compliant signal. The root cause is that I was not checking everywhere to ensure we had a throttle rule to ensure scheduling. This fixes that by adding an `if` statement/guard around the areas of code. I also improve the error message by adding which ruleId the error is coming from. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- ...dule_throttle_notification_actions.test.ts | 2 +- .../schedule_throttle_notification_actions.ts | 1 + .../create_security_rule_type_wrapper.ts | 60 +++++++++-------- .../legacy_notifications/one_action.json | 2 +- .../signals/signal_rule_alert_type.test.ts | 64 ++++++++++++++++++- .../signals/signal_rule_alert_type.ts | 61 +++++++++--------- 6 files changed, 129 insertions(+), 61 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 81f229c636bd..964df3c91eb0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -278,7 +278,7 @@ describe('schedule_throttle_notification_actions', () => { }); expect(logger.error).toHaveBeenCalledWith( - 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.' + 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected. Check your rule with ruleId: "rule-123" for data integrity issues' ); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index 5bf18496e637..7b4b314cc891 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -145,6 +145,7 @@ export const scheduleThrottledNotificationActions = async ({ ` "from": now-${throttle}`, ` "to": ${startedAt.toISOString()}.`, ' This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.', + ` Check your rule with ruleId: "${ruleId}" for data integrity issues`, ].join('') ); } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 0ad416e86e31..0fe7cbdc9bd9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -375,20 +375,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -407,20 +409,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: ruleSO.attributes.throttle, - startedAt, - id: ruleSO.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex: ruleDataClient.indexName, - ruleId, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - signals: result.createdSignals, - logger, - }); + if (ruleSO.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json index 1966dcf5ff53..bf980e370e3a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/legacy_notifications/one_action.json @@ -3,7 +3,7 @@ "interval": "1m", "actions": [ { - "id": "42534430-2092-11ec-99a6-05d79563c01a", + "id": "1fa31c30-3046-11ec-8971-1f3f7bae65af", "group": "default", "params": { "message": "Hourly\nRule {{context.rule.name}} generated {{state.signals_count}} alerts" diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 88b276358a70..6a84776ccee5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -536,14 +536,74 @@ describe('signal_rule_alert_type', () => { errors: ['Error that bubbled up.'], }; (queryExecutor as jest.Mock).mockResolvedValue(result); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + it('should NOT call scheduleThrottledNotificationActions if result is false and the throttle is not set', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockResolvedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); + it('should call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset', async () => { (queryExecutor as jest.Mock).mockRejectedValue({}); - await alert.executor(payload); + const ruleAlert = getAlertMock(false, getQueryRuleParams()); + ruleAlert.throttle = '1h'; + const payLoadWithThrottle = getPayload( + ruleAlert, + alertServices + ) as jest.Mocked; + payLoadWithThrottle.rule.throttle = '1h'; + alertServices.savedObjectsClient.get.mockResolvedValue({ + id: 'id', + type: 'type', + references: [], + attributes: ruleAlert, + }); + await alert.executor(payLoadWithThrottle); expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); }); + + it('should NOT call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset if throttle is not defined', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockRejectedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(0); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 4e98bee83aeb..90220814fb92 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -474,21 +474,22 @@ export const signalRulesAlertType = ({ ); } else { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); - + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -507,20 +508,22 @@ export const signalRulesAlertType = ({ } } catch (error) { // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early - await scheduleThrottledNotificationActions({ - alertInstance: services.alertInstanceFactory(alertId), - throttle: savedObject.attributes.throttle, - startedAt, - id: savedObject.id, - kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) - ?.kibana_siem_app_url, - outputIndex, - ruleId, - signals: result.createdSignals, - esClient: services.scopedClusterClient.asCurrentUser, - notificationRuleParams, - logger, - }); + if (savedObject.attributes.throttle != null) { + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + } const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( 'An error occurred during rule execution:', From e53f4d2f28d127935bd7fe5dfa235a81e30b9460 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Mon, 18 Oct 2021 22:37:00 -0600 Subject: [PATCH 18/53] [Security Solutions] Makes legacy actions/notification system, legacy action status, and exception lists multiple space shareable (#115427) ## Summary See https://github.com/elastic/kibana/issues/114548 Makes the following saved objects multiple-isolated: * siem-detection-engine-rule-status * exception-list * siem-detection-engine-rule-actions ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- x-pack/plugins/lists/server/saved_objects/exception_list.ts | 3 ++- .../rule_actions/legacy_saved_object_mappings.ts | 3 ++- .../legacy_rule_status_saved_object_mappings.ts | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/lists/server/saved_objects/exception_list.ts b/x-pack/plugins/lists/server/saved_objects/exception_list.ts index 8354e64d64a6..3d31bdd56114 100644 --- a/x-pack/plugins/lists/server/saved_objects/exception_list.ts +++ b/x-pack/plugins/lists/server/saved_objects/exception_list.ts @@ -177,11 +177,12 @@ const combinedMappings: SavedObjectsType['mappings'] = { }; export const exceptionListType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', hidden: false, mappings: combinedMappings, migrations, name: exceptionListSavedObjectType, - namespaceType: 'single', + namespaceType: 'multiple-isolated', }; export const exceptionListAgnosticType: SavedObjectsType = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts index 3d6a405225fe..835ccd92f9cc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_saved_object_mappings.ts @@ -59,9 +59,10 @@ const legacyRuleActionsSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we no longer need legacy migrations for rule actions (8.0.0) */ export const legacyType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleActionsSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: legacyRuleActionsSavedObjectMappings, migrations: legacyRuleActionsSavedObjectMigration, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts index 3fe3fc06cc7d..298c75b8b7d5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/legacy_rule_status/legacy_rule_status_saved_object_mappings.ts @@ -65,9 +65,10 @@ export const ruleStatusSavedObjectMappings: SavedObjectsType['mappings'] = { * @deprecated Remove this once we've fully migrated to event-log and no longer require addition status SO (8.x) */ export const legacyRuleStatusType: SavedObjectsType = { + convertToMultiNamespaceTypeVersion: '8.0.0', name: legacyRuleStatusSavedObjectType, hidden: false, - namespaceType: 'single', + namespaceType: 'multiple-isolated', mappings: ruleStatusSavedObjectMappings, migrations: legacyRuleStatusSavedObjectMigration, }; From 57ff4a7172bb68067df9b809d592748deeb1114c Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 19 Oct 2021 00:43:12 -0400 Subject: [PATCH 19/53] [Security Solution][Endpoint] Adds additional endpoint privileges to the `useUserPrivileges()` hook (#115051) * Adds new `canIsolateHost` and `canCreateArtifactsByPolicy` privileges for endpoint * Refactor `useEndpointPrivileges` mocks to also provide a test function to return the full set of default privileges * refactor useEndpointPrivileges tests to be more resilient to future changes --- .../__mocks__/use_endpoint_privileges.ts | 18 ---- .../__mocks__/use_endpoint_privileges.ts | 12 +++ .../user_privileges/endpoint/index.ts | 9 ++ .../user_privileges/endpoint/mocks.ts | 29 ++++++ .../use_endpoint_privileges.test.ts | 89 ++++++++++--------- .../{ => endpoint}/use_endpoint_privileges.ts | 25 ++++-- .../user_privileges/endpoint/utils.ts | 19 ++++ .../components/user_privileges/index.tsx | 10 +-- .../components/user_info/index.test.tsx | 2 +- .../alerts/use_alerts_privileges.test.tsx | 6 +- .../alerts/use_signal_index.test.tsx | 2 +- .../search_exceptions.test.tsx | 17 ++-- .../search_exceptions/search_exceptions.tsx | 2 +- .../view/event_filters_list_page.test.tsx | 2 +- .../host_isolation_exceptions_list.test.tsx | 3 +- .../policy_trusted_apps_empty_unassigned.tsx | 2 +- .../policy_trusted_apps_flyout.test.tsx | 2 +- .../policy_trusted_apps_layout.test.tsx | 22 ++--- .../layout/policy_trusted_apps_layout.tsx | 2 +- .../list/policy_trusted_apps_list.test.tsx | 10 +-- .../list/policy_trusted_apps_list.tsx | 2 +- .../view/trusted_apps_page.test.tsx | 2 +- .../public/overview/pages/overview.test.tsx | 2 +- 23 files changed, 170 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts rename x-pack/plugins/security_solution/public/common/components/user_privileges/{ => endpoint}/use_endpoint_privileges.test.ts (63%) rename x-pack/plugins/security_solution/public/common/components/user_privileges/{ => endpoint}/use_endpoint_privileges.ts (71%) create mode 100644 x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts deleted file mode 100644 index 80ca53453418..000000000000 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts +++ /dev/null @@ -1,18 +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 { EndpointPrivileges } from '../use_endpoint_privileges'; - -export const useEndpointPrivileges = jest.fn(() => { - const endpointPrivilegesMock: EndpointPrivileges = { - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - }; - return endpointPrivilegesMock; -}); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts new file mode 100644 index 000000000000..ae9aacaf3d55 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/__mocks__/use_endpoint_privileges.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getEndpointPrivilegesInitialStateMock } from '../mocks'; + +export { getEndpointPrivilegesInitialState } from '../utils'; + +export const useEndpointPrivileges = jest.fn(getEndpointPrivilegesInitialStateMock); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts new file mode 100644 index 000000000000..adea89ce1a05 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/index.ts @@ -0,0 +1,9 @@ +/* + * 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 './use_endpoint_privileges'; +export { getEndpointPrivilegesInitialState } from './utils'; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts new file mode 100644 index 000000000000..2851c92816ce --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/mocks.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { EndpointPrivileges } from './use_endpoint_privileges'; +import { getEndpointPrivilegesInitialState } from './utils'; + +export const getEndpointPrivilegesInitialStateMock = ( + overrides: Partial = {} +): EndpointPrivileges => { + // Get the initial state and set all permissions to `true` (enabled) for testing + const endpointPrivilegesMock: EndpointPrivileges = { + ...( + Object.entries(getEndpointPrivilegesInitialState()) as Array< + [keyof EndpointPrivileges, boolean] + > + ).reduce((mockPrivileges, [key, value]) => { + mockPrivileges[key] = !value; + + return mockPrivileges; + }, {} as EndpointPrivileges), + ...overrides, + }; + + return endpointPrivilegesMock; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts similarity index 63% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts index 82443e913499..d4ba29a4ef95 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.test.ts @@ -6,16 +6,17 @@ */ import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; -import { useHttp, useCurrentUser } from '../../lib/kibana'; +import { useHttp, useCurrentUser } from '../../../lib/kibana'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; -import { securityMock } from '../../../../../security/public/mocks'; -import { appRoutesService } from '../../../../../fleet/common'; -import { AuthenticatedUser } from '../../../../../security/common'; -import { licenseService } from '../../hooks/use_license'; -import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks'; - -jest.mock('../../lib/kibana'); -jest.mock('../../hooks/use_license', () => { +import { securityMock } from '../../../../../../security/public/mocks'; +import { appRoutesService } from '../../../../../../fleet/common'; +import { AuthenticatedUser } from '../../../../../../security/common'; +import { licenseService } from '../../../hooks/use_license'; +import { fleetGetCheckPermissionsHttpMock } from '../../../../management/pages/mocks'; +import { getEndpointPrivilegesInitialStateMock } from './mocks'; + +jest.mock('../../../lib/kibana'); +jest.mock('../../../hooks/use_license', () => { const licenseServiceInstance = { isPlatinumPlus: jest.fn(), }; @@ -27,6 +28,8 @@ jest.mock('../../hooks/use_license', () => { }; }); +const licenseServiceMock = licenseService as jest.Mocked; + describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; let fleetApiMock: ReturnType; @@ -45,7 +48,7 @@ describe('When using useEndpointPrivileges hook', () => { fleetApiMock = fleetGetCheckPermissionsHttpMock( useHttp() as Parameters[0] ); - (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); + licenseServiceMock.isPlatinumPlus.mockReturnValue(true); render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); @@ -69,34 +72,31 @@ describe('When using useEndpointPrivileges hook', () => { (useCurrentUser as jest.Mock).mockReturnValue(null); const { rerender } = render(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Make user service available (useCurrentUser as jest.Mock).mockReturnValue(authenticatedUser); rerender(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: true, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + loading: true, + }) + ); // Release the API response await act(async () => { fleetApiMock.waitForApi(); releaseApiResponse!(); }); - expect(result.current).toEqual({ - canAccessEndpointManagement: true, - canAccessFleet: true, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual(getEndpointPrivilegesInitialStateMock()); }); it('should call Fleet permissions api to determine user privilege to fleet', async () => { @@ -113,12 +113,11 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: true, // this is only true here because I did not adjust the API mock - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + }) + ); }); it('should set privileges to false if fleet api check returns failure', async () => { @@ -130,11 +129,21 @@ describe('When using useEndpointPrivileges hook', () => { render(); await waitForNextUpdate(); await fleetApiMock.waitForApi(); - expect(result.current).toEqual({ - canAccessEndpointManagement: false, - canAccessFleet: false, - loading: false, - isPlatinumPlus: true, - }); + expect(result.current).toEqual( + getEndpointPrivilegesInitialStateMock({ + canAccessEndpointManagement: false, + canAccessFleet: false, + }) + ); }); + + it.each([['canIsolateHost'], ['canCreateArtifactsByPolicy']])( + 'should set %s to false if license is not PlatinumPlus', + async (privilege) => { + licenseServiceMock.isPlatinumPlus.mockReturnValue(false); + render(); + await waitForNextUpdate(); + expect(result.current).toEqual(expect.objectContaining({ [privilege]: false })); + } + ); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts similarity index 71% rename from x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts rename to x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts index 315935104d10..36f02d22487f 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/use_endpoint_privileges.ts @@ -6,9 +6,9 @@ */ import { useEffect, useMemo, useRef, useState } from 'react'; -import { useCurrentUser, useHttp } from '../../lib/kibana'; -import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; -import { useLicense } from '../../hooks/use_license'; +import { useCurrentUser, useHttp } from '../../../lib/kibana'; +import { appRoutesService, CheckPermissionsResponse } from '../../../../../../fleet/common'; +import { useLicense } from '../../../hooks/use_license'; export interface EndpointPrivileges { loading: boolean; @@ -16,6 +16,11 @@ export interface EndpointPrivileges { canAccessFleet: boolean; /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; + /** if user has permissions to create Artifacts by Policy */ + canCreateArtifactsByPolicy: boolean; + /** If user has permissions to use the Host isolation feature */ + canIsolateHost: boolean; + /** @deprecated do not use. instead, use one of the other privileges defined */ isPlatinumPlus: boolean; } @@ -29,7 +34,7 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { const http = useHttp(); const user = useCurrentUser(); const isMounted = useRef(true); - const license = useLicense(); + const isPlatinumPlusLicense = useLicense().isPlatinumPlus(); const [canAccessFleet, setCanAccessFleet] = useState(false); const [fleetCheckDone, setFleetCheckDone] = useState(false); @@ -61,13 +66,19 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { }, [user?.roles]); const privileges = useMemo(() => { - return { + const privilegeList: EndpointPrivileges = { loading: !fleetCheckDone || !user, canAccessFleet, canAccessEndpointManagement: canAccessFleet && isSuperUser, - isPlatinumPlus: license.isPlatinumPlus(), + canCreateArtifactsByPolicy: isPlatinumPlusLicense, + canIsolateHost: isPlatinumPlusLicense, + // FIXME: Remove usages of the property below + /** @deprecated */ + isPlatinumPlus: isPlatinumPlusLicense, }; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user, license]); + + return privilegeList; + }, [canAccessFleet, fleetCheckDone, isSuperUser, user, isPlatinumPlusLicense]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.ts new file mode 100644 index 000000000000..df91314479f1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/endpoint/utils.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 { EndpointPrivileges } from './use_endpoint_privileges'; + +export const getEndpointPrivilegesInitialState = (): EndpointPrivileges => { + return { + loading: true, + canAccessFleet: false, + canAccessEndpointManagement: false, + canIsolateHost: false, + canCreateArtifactsByPolicy: false, + isPlatinumPlus: false, + }; +}; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index 437d27278102..bc0640296b33 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -11,9 +11,10 @@ import { DeepReadonly } from 'utility-types'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; import { useFetchListPrivileges } from '../../../detections/components/user_privileges/use_fetch_list_privileges'; -import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; +import { EndpointPrivileges, useEndpointPrivileges } from './endpoint'; import { SERVER_APP_ID } from '../../../../common/constants'; +import { getEndpointPrivilegesInitialState } from './endpoint/utils'; export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; @@ -24,12 +25,7 @@ export interface UserPrivilegesState { export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: { - loading: true, - canAccessEndpointManagement: false, - canAccessFleet: false, - isPlatinumPlus: false, - }, + endpointPrivileges: getEndpointPrivilegesInitialState(), kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 3d95dca81165..1a8588017e4d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -17,7 +17,7 @@ import { UserPrivilegesProvider } from '../../../common/components/user_privileg jest.mock('../../../common/lib/kibana'); jest.mock('../../containers/detection_engine/alerts/api'); -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useUserInfo', () => { beforeAll(() => { diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 40894c1d0192..1dc142360609 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -12,6 +12,7 @@ import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useUserPrivileges } from '../../../../common/components/user_privileges'; import { Privilege } from './types'; import { UseAlertsPrivelegesReturn, useAlertsPrivileges } from './use_alerts_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); @@ -86,12 +87,11 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, - endpointPrivileges: { + endpointPrivileges: getEndpointPrivilegesInitialStateMock({ loading: true, canAccessEndpointManagement: false, canAccessFleet: false, - isPlatinumPlus: true, - }, + }), kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index ade83fed4fd6..ad4ad5062c9d 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -13,7 +13,7 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index 084978d35d03..3b987a721141 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -11,11 +11,12 @@ import { AppContextTestRender, createAppRootMockRenderer } from '../../../common import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; import { SearchExceptions, SearchExceptionsProps } from '.'; -jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); +import { getEndpointPrivilegesInitialStateMock } from '../../../common/components/user_privileges/endpoint/mocks'; +jest.mock('../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let onSearchMock: jest.Mock; const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; @@ -29,13 +30,11 @@ describe('Search exceptions', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: false, - ...endpointOverrides, - }); + ): EndpointPrivileges => + getEndpointPrivilegesInitialStateMock({ + isPlatinumPlus: false, + ...endpointOverrides, + }); beforeEach(() => { onSearchMock = jest.fn(); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 1f3eab5db294..569916ac2031 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,7 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; -import { useEndpointPrivileges } from '../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; export interface SearchExceptionsProps { defaultValue?: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx index 0729f95bb44a..02efce1ab59e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/event_filters/view/event_filters_list_page.test.tsx @@ -14,7 +14,7 @@ import { isFailedResourceState, isLoadedResourceState } from '../../../state'; // Needed to mock the data services used by the ExceptionItem component jest.mock('../../../../common/lib/kibana'); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Event Filters List Page', () => { let render: () => ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 5113457e5bcc..625da11a3644 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -16,8 +16,7 @@ import { getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; import { useLicense } from '../../../../common/hooks/use_license'; -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); - +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); jest.mock('../service'); jest.mock('../../../../common/hooks/use_license'); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index ee52e1210a48..c12bec03ada0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; interface CommonProps { policyId: string; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx index c1d00f7a3f99..8e412d2020b7 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx @@ -21,7 +21,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index 43e19c00bcc8..dbb18a1b0f2e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -19,13 +19,11 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; -import { - EndpointPrivileges, - useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; jest.mock('../../../../trusted_apps/service'); -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; let mockedContext: AppContextTestRender; @@ -37,16 +35,6 @@ let http: typeof coreStart.http; const generator = new EndpointDocGenerator(); describe('Policy trusted apps layout', () => { - const loadedUserEndpointPrivilegesState = ( - endpointOverrides: Partial = {} - ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, - ...endpointOverrides, - }); - beforeEach(() => { mockedContext = createAppRootMockRenderer(); http = mockedContext.coreStart.http; @@ -137,7 +125,7 @@ describe('Policy trusted apps layout', () => { it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); @@ -155,7 +143,7 @@ describe('Policy trusted apps layout', () => { it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { mockUseEndpointPrivileges.mockReturnValue( - loadedUserEndpointPrivilegesState({ + getEndpointPrivilegesInitialStateMock({ isPlatinumPlus: false, }) ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index a3f1ed215286..49f76ad2e02c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -30,7 +30,7 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useAppUrl } from '../../../../../../common/lib/kibana'; import { APP_ID } from '../../../../../../../common/constants'; import { getTrustedAppsListPath } from '../../../../../common/routing'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index a8d3cc150546..e18d3c01791c 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -24,9 +24,10 @@ import { APP_ID } from '../../../../../../../common/constants'; import { EndpointPrivileges, useEndpointPrivileges, -} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +} from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; +import { getEndpointPrivilegesInitialStateMock } from '../../../../../../common/components/user_privileges/endpoint/mocks'; -jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { @@ -43,10 +44,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { const loadedUserEndpointPrivilegesState = ( endpointOverrides: Partial = {} ): EndpointPrivileges => ({ - loading: false, - canAccessFleet: true, - canAccessEndpointManagement: true, - isPlatinumPlus: true, + ...getEndpointPrivilegesInitialStateMock(), ...endpointOverrides, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index f6afd9d50248..def0f490b7fe 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,7 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; -import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index f39fd47c7877..b4366a892292 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -52,7 +52,7 @@ jest.mock('../../../../common/hooks/use_license', () => { }; }); -jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); +jest.mock('../../../../common/components/user_privileges/endpoint/use_endpoint_privileges'); describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index cab02450f888..ab5ae4f613e3 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -30,7 +30,7 @@ import { mockCtiLinksResponse, } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; -import { EndpointPrivileges } from '../../common/components/user_privileges/use_endpoint_privileges'; +import { EndpointPrivileges } from '../../common/components/user_privileges/endpoint/use_endpoint_privileges'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; From 730df8852f63bc6b652cca4f31dd56ea4e2addba Mon Sep 17 00:00:00 2001 From: "Joey F. Poon" Date: Tue, 19 Oct 2021 00:09:21 -0500 Subject: [PATCH 20/53] [Security Solution] fix endpoint list agent status logic (#115286) --- .../server/endpoint/routes/metadata/handlers.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 027107bcf1a5..e98cdc4f1140 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -48,6 +48,7 @@ import { } from './support/query_strategies'; import { NotFoundError } from '../../errors'; import { EndpointHostUnEnrolledError } from '../../services/metadata'; +import { getAgentStatus } from '../../../../../fleet/common/services/agent_status'; export interface MetadataRequestContext { esClient?: IScopedClusterClient; @@ -522,10 +523,11 @@ async function queryUnitedIndex( const agentPolicy = agentPoliciesMap[agent.policy_id!]; // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const endpointPolicy = endpointPoliciesMap[agent.policy_id!]; + const fleetAgentStatus = getAgentStatus(agent as Agent); + return { metadata, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - host_status: fleetAgentStatusToEndpointHostStatus(agent.last_checkin_status!), + host_status: fleetAgentStatusToEndpointHostStatus(fleetAgentStatus), policy_info: { agent: { applied: { From cb17ec78969b11922993d70959cfa32574b3ca81 Mon Sep 17 00:00:00 2001 From: Dario Gieselaar Date: Tue, 19 Oct 2021 09:22:29 +0200 Subject: [PATCH 21/53] [APM] Add readme for @elastic/apm-generator (#115368) --- packages/elastic-apm-generator/README.md | 93 ++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/packages/elastic-apm-generator/README.md b/packages/elastic-apm-generator/README.md index e69de29bb2d1..e43187a8155d 100644 --- a/packages/elastic-apm-generator/README.md +++ b/packages/elastic-apm-generator/README.md @@ -0,0 +1,93 @@ +# @elastic/apm-generator + +`@elastic/apm-generator` is an experimental tool to generate synthetic APM data. It is intended to be used for development and testing of the Elastic APM app in Kibana. + +At a high-level, the module works by modeling APM events/metricsets with [a fluent API](https://en.wikipedia.org/wiki/Fluent_interface). The models can then be serialized and converted to Elasticsearch documents. In the future we might support APM Server as an output as well. + +## Usage + +This section assumes that you've installed Kibana's dependencies by running `yarn kbn bootstrap` in the repository's root folder. + +This library can currently be used in two ways: + +- Imported as a Node.js module, for instance to be used in Kibana's functional test suite. +- With a command line interface, to index data based on some example scenarios. + +### Using the Node.js module + +#### Concepts + +- `Service`: a logical grouping for a monitored service. A `Service` object contains fields like `service.name`, `service.environment` and `agent.name`. +- `Instance`: a single instance of a monitored service. E.g., the workload for a monitored service might be spread across multiple containers. An `Instance` object contains fields like `service.node.name` and `container.id`. +- `Timerange`: an object that will return an array of timestamps based on an interval and a rate. These timestamps can be used to generate events/metricsets. +- `Transaction`, `Span`, `APMError` and `Metricset`: events/metricsets that occur on an instance. For more background, see the [explanation of the APM data model](https://www.elastic.co/guide/en/apm/get-started/7.15/apm-data-model.html) + + +#### Example + +```ts +import { service, timerange, toElasticsearchOutput } from '@elastic/apm-generator'; + +const instance = service('synth-go', 'production', 'go') + .instance('instance-a'); + +const from = new Date('2021-01-01T12:00:00.000Z').getTime(); +const to = new Date('2021-01-01T12:00:00.000Z').getTime() - 1; + +const traceEvents = timerange(from, to) + .interval('1m') + .rate(10) + .flatMap(timestamp => instance.transaction('GET /api/product/list') + .timestamp(timestamp) + .duration(1000) + .success() + .children( + instance.span('GET apm-*/_search', 'db', 'elasticsearch') + .timestamp(timestamp + 50) + .duration(900) + .destination('elasticsearch') + .success() + ).serialize() + ); + +const metricsets = timerange(from, to) + .interval('30s') + .rate(1) + .flatMap(timestamp => instance.appMetrics({ + 'system.memory.actual.free': 800, + 'system.memory.total': 1000, + 'system.cpu.total.norm.pct': 0.6, + 'system.process.cpu.total.norm.pct': 0.7, + }).timestamp(timestamp) + .serialize() + ); + +const esEvents = toElasticsearchOutput(traceEvents.concat(metricsets)); +``` + +#### Generating metricsets + +`@elastic/apm-generator` can also automatically generate transaction metrics, span destination metrics and transaction breakdown metrics based on the generated trace events. If we expand on the previous example: + +```ts +import { getTransactionMetrics, getSpanDestinationMetrics, getBreakdownMetrics } from '@elastic/apm-generator'; + +const esEvents = toElasticsearchOutput([ + ...traceEvents, + ...getTransactionMetrics(traceEvents), + ...getSpanDestinationMetrics(traceEvents), + ...getBreakdownMetrics(traceEvents) +]); +``` + +### CLI + +Via the CLI, you can upload examples. The supported examples are listed in `src/lib/es.ts`. A `--target` option that specifies the Elasticsearch URL should be defined when running the `example` command. Here's an example: + +`$ node packages/elastic-apm-generator/src/scripts/es.js example simple-trace --target=http://admin:changeme@localhost:9200` + +The following options are supported: +- `to`: the end of the time range, in ISO format. By default, the current time will be used. +- `from`: the start of the time range, in ISO format. By default, `to` minus 15 minutes will be used. +- `apm-server-version`: the version used in the index names bootstrapped by APM Server, e.g. `7.16.0`. __If these indices do not exist, the script will exit with an error. It will not bootstrap the indices itself.__ + From fd4b85b1c687b0a4d837b2715bedbdfce9cba77f Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Tue, 19 Oct 2021 08:34:34 +0100 Subject: [PATCH 22/53] Update heading on telemetry management section. (#115425) --- .../telemetry_management_section.test.tsx.snap | 4 ++-- .../public/components/telemetry_management_section.tsx | 8 ++++---- x-pack/plugins/translations/translations/ja-JP.json | 2 -- x-pack/plugins/translations/translations/zh-CN.json | 2 -- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 758ecf54f4bf..72947b151491 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -53,7 +53,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` loading={false} setting={ Object { - "ariaName": "Provide usage statistics", + "ariaName": "Provide usage data", "category": Array [], "defVal": true, "description": @@ -109,7 +109,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` />

, - "displayName": "Provide usage statistics", + "displayName": "Provide usage data", "isCustom": true, "isOverridden": false, "name": "telemetry:enabled", diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index 3686cb10706b..037603cb165d 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -116,14 +116,14 @@ export class TelemetryManagementSection extends Component { setting={{ type: 'boolean', name: 'telemetry:enabled', - displayName: i18n.translate('telemetry.provideUsageStatisticsTitle', { - defaultMessage: 'Provide usage statistics', + displayName: i18n.translate('telemetry.provideUsageDataTitle', { + defaultMessage: 'Provide usage data', }), value: enabled, description: this.renderDescription(), defVal: true, - ariaName: i18n.translate('telemetry.provideUsageStatisticsAriaName', { - defaultMessage: 'Provide usage statistics', + ariaName: i18n.translate('telemetry.provideUsageDataAriaName', { + defaultMessage: 'Provide usage data', }), requiresPageReload: false, category: [], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 852b01977b78..acf9daebaf55 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4508,8 +4508,6 @@ "telemetry.optInNoticeSeenErrorToastText": "通知の消去中にエラーが発生しました", "telemetry.optInSuccessOff": "使用状況データ収集がオフです。", "telemetry.optInSuccessOn": "使用状況データ収集がオンです。", - "telemetry.provideUsageStatisticsAriaName": "使用統計を提供", - "telemetry.provideUsageStatisticsTitle": "使用統計を提供", "telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー", "telemetry.securityData": "Endpoint Security データ", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9d88c757f1e5..5890f8553e7c 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4553,8 +4553,6 @@ "telemetry.optInNoticeSeenErrorToastText": "关闭声明时发生错误", "telemetry.optInSuccessOff": "使用情况数据收集已关闭。", "telemetry.optInSuccessOn": "使用情况数据收集已打开。", - "telemetry.provideUsageStatisticsAriaName": "提供使用情况统计", - "telemetry.provideUsageStatisticsTitle": "提供使用情况统计", "telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明", "telemetry.securityData": "终端安全数据", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", From 533e5d8d393c20551f811d06632f19a44bcd25ab Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 19 Oct 2021 03:36:46 -0400 Subject: [PATCH 23/53] [Security Solution][Endpoint] Change Trusted Apps to use `item_id` as its identifier and Enable Trusted Apps filtering by id in the UI (#115276) * Add `item_id` to list of searchable fields * trusted apps api changes to use `item_id` instead of SO `id` * Change Policy Details Trusted App "View all details" action URL to show TA list filtered by the TA id --- .../list/policy_trusted_apps_list.test.tsx | 2 +- .../list/policy_trusted_apps_list.tsx | 2 +- .../pages/trusted_apps/constants.ts | 1 + .../routes/trusted_apps/handlers.test.ts | 5 +- .../endpoint/routes/trusted_apps/mapping.ts | 2 +- .../routes/trusted_apps/service.test.ts | 24 +++++- .../endpoint/routes/trusted_apps/service.ts | 77 +++++++++++++------ 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index e18d3c01791c..9165aec3bef8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -205,7 +205,7 @@ describe('when rendering the PolicyTrustedAppsList', () => { expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith( APP_ID, expect.objectContaining({ - path: '/administration/trusted_apps?show=edit&id=89f72d8a-05b5-4350-8cad-0dc3661d6e67', + path: '/administration/trusted_apps?filter=89f72d8a-05b5-4350-8cad-0dc3661d6e67', }) ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index def0f490b7fe..89ff6bd099be 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -113,7 +113,7 @@ export const PolicyTrustedAppsList = memo( for (const trustedApp of trustedAppItems) { const isGlobal = trustedApp.effectScope.type === 'global'; - const viewUrlPath = getTrustedAppsListPath({ id: trustedApp.id, show: 'edit' }); + const viewUrlPath = getTrustedAppsListPath({ filter: trustedApp.id }); const assignedPoliciesMenuItems: ArtifactEntryCollapsibleCardProps['policies'] = trustedApp.effectScope.type === 'global' ? undefined diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts index 0602ae18c140..beefb8587d78 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/constants.ts @@ -8,6 +8,7 @@ export const SEARCHABLE_FIELDS: Readonly = [ `name`, `description`, + 'item_id', `entries.value`, `entries.entries.value`, ]; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts index 547c1f6a2e5f..614ad4fb548e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts @@ -110,7 +110,7 @@ const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } const packagePolicyClient = createPackagePolicyServiceMock() as jest.Mocked; -describe('handlers', () => { +describe('TrustedApps API Handlers', () => { beforeEach(() => { packagePolicyClient.getByIDs.mockReset(); }); @@ -195,6 +195,7 @@ describe('handlers', () => { const mockResponse = httpServerMock.createResponseFactory(); exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null); + exceptionsListClient.getExceptionListItem.mockResolvedValue(null); await deleteTrustedAppHandler( createHandlerContextMock(), @@ -582,7 +583,7 @@ describe('handlers', () => { }); it('should return 404 if trusted app does not exist', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null); + exceptionsListClient.getExceptionListItem.mockResolvedValue(null); await updateHandler( createHandlerContextMock(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts index 2c085c14db00..08c1a3a809d4 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts @@ -122,7 +122,7 @@ export const exceptionListItemToTrustedApp = ( const grouped = entriesToConditionEntriesMap(exceptionListItem.entries); return { - id: exceptionListItem.id, + id: exceptionListItem.item_id, version: exceptionListItem._version || '', name: exceptionListItem.name, description: exceptionListItem.description, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts index dce84df73592..c57416ff1c97 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts @@ -85,9 +85,10 @@ const TRUSTED_APP: TrustedApp = { ], }; -describe('service', () => { +describe('TrustedApps service', () => { beforeEach(() => { exceptionsListClient.deleteExceptionListItem.mockReset(); + exceptionsListClient.getExceptionListItem.mockReset(); exceptionsListClient.createExceptionListItem.mockReset(); exceptionsListClient.findExceptionListItem.mockReset(); exceptionsListClient.createTrustedAppsList.mockReset(); @@ -96,6 +97,7 @@ describe('service', () => { describe('deleteTrustedApp', () => { it('should delete existing trusted app', async () => { + exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined(); @@ -107,6 +109,7 @@ describe('service', () => { }); it('should throw for non existing trusted app', async () => { + exceptionsListClient.getExceptionListItem.mockResolvedValue(null); exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null); await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf( @@ -393,7 +396,7 @@ describe('service', () => { }); it('should throw a Not Found error if trusted app is not found prior to making update', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null); + exceptionsListClient.getExceptionListItem.mockResolvedValue(null); await expect( updateTrustedApp( exceptionsListClient, @@ -489,5 +492,22 @@ describe('service', () => { TrustedAppNotFoundError ); }); + + it('should try to find trusted app by `itemId` and then by `id`', async () => { + exceptionsListClient.getExceptionListItem.mockResolvedValue(null); + await getTrustedApp(exceptionsListClient, '123').catch(() => Promise.resolve()); + + expect(exceptionsListClient.getExceptionListItem).toHaveBeenCalledTimes(2); + expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(1, { + itemId: '123', + id: undefined, + namespaceType: 'agnostic', + }); + expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(2, { + itemId: undefined, + id: '123', + namespaceType: 'agnostic', + }); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts index 856a615c1ffa..7a4b2372ece8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts @@ -15,13 +15,13 @@ import { DeleteTrustedAppsRequestParams, GetOneTrustedAppResponse, GetTrustedAppsListRequest, - GetTrustedAppsSummaryResponse, GetTrustedAppsListResponse, + GetTrustedAppsSummaryRequest, + GetTrustedAppsSummaryResponse, PostTrustedAppCreateRequest, PostTrustedAppCreateResponse, PutTrustedAppUpdateRequest, PutTrustedAppUpdateResponse, - GetTrustedAppsSummaryRequest, TrustedApp, } from '../../../../common/endpoint/types'; @@ -33,8 +33,8 @@ import { } from './mapping'; import { TrustedAppNotFoundError, - TrustedAppVersionConflictError, TrustedAppPolicyNotExistsError, + TrustedAppVersionConflictError, } from './errors'; import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { PackagePolicy } from '../../../../../fleet/common'; @@ -87,30 +87,61 @@ const isUserTryingToModifyEffectScopeWithoutPermissions = ( } }; -export const deleteTrustedApp = async ( +/** + * Attempts to first fine the ExceptionItem using `item_id` and if not found, then a second attempt wil be done + * against the Saved Object `id`. + * @param exceptionsListClient + * @param id + */ +export const findTrustedAppExceptionItemByIdOrItemId = async ( exceptionsListClient: ExceptionListClient, - { id }: DeleteTrustedAppsRequestParams -) => { - const exceptionListItem = await exceptionsListClient.deleteExceptionListItem({ - id, + id: string +): Promise => { + const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({ + itemId: id, + id: undefined, + namespaceType: 'agnostic', + }); + + if (trustedAppExceptionItem) { + return trustedAppExceptionItem; + } + + return exceptionsListClient.getExceptionListItem({ itemId: undefined, + id, namespaceType: 'agnostic', }); +}; - if (!exceptionListItem) { +export const deleteTrustedApp = async ( + exceptionsListClient: ExceptionListClient, + { id }: DeleteTrustedAppsRequestParams +): Promise => { + const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( + exceptionsListClient, + id + ); + + if (!trustedAppExceptionItem) { throw new TrustedAppNotFoundError(id); } + + await exceptionsListClient.deleteExceptionListItem({ + id: trustedAppExceptionItem.id, + itemId: undefined, + namespaceType: 'agnostic', + }); }; export const getTrustedApp = async ( exceptionsListClient: ExceptionListClient, id: string ): Promise => { - const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({ - itemId: '', - id, - namespaceType: 'agnostic', - }); + const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( + exceptionsListClient, + id + ); if (!trustedAppExceptionItem) { throw new TrustedAppNotFoundError(id); @@ -189,19 +220,18 @@ export const updateTrustedApp = async ( updatedTrustedApp: PutTrustedAppUpdateRequest, isAtLeastPlatinum: boolean ): Promise => { - const currentTrustedApp = await exceptionsListClient.getExceptionListItem({ - itemId: '', - id, - namespaceType: 'agnostic', - }); + const currentTrustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( + exceptionsListClient, + id + ); - if (!currentTrustedApp) { + if (!currentTrustedAppExceptionItem) { throw new TrustedAppNotFoundError(id); } if ( isUserTryingToModifyEffectScopeWithoutPermissions( - exceptionListItemToTrustedApp(currentTrustedApp), + exceptionListItemToTrustedApp(currentTrustedAppExceptionItem), updatedTrustedApp, isAtLeastPlatinum ) @@ -226,7 +256,10 @@ export const updateTrustedApp = async ( try { updatedTrustedAppExceptionItem = await exceptionsListClient.updateExceptionListItem( - updatedTrustedAppToUpdateExceptionListItemOptions(currentTrustedApp, updatedTrustedApp) + updatedTrustedAppToUpdateExceptionListItemOptions( + currentTrustedAppExceptionItem, + updatedTrustedApp + ) ); } catch (e) { if (e?.output?.statusCode === 409) { From bfe648d49606135aa52814932d7611893496c447 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Tue, 19 Oct 2021 09:37:01 +0200 Subject: [PATCH 24/53] Fix alerts Count table title overflow wraps prematurely (#115364) --- .../components/alerts_kpis/alerts_count_panel/index.tsx | 2 +- .../components/alerts_kpis/alerts_histogram_panel/index.tsx | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 29324d186784..c8d45ca67068 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -94,7 +94,7 @@ export const AlertsCountPanel = memo( {i18n.COUNT_TABLE_TITLE}
} titleSize="s" hideSubtitle > diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index 0613c619d89b..07fa81f27684 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -257,7 +257,11 @@ export const AlertsHistogramPanel = memo( }, [showLinkToAlerts, goToDetectionEngine, formatUrl]); const titleText = useMemo( - () => (onlyField == null ? title : i18n.TOP(onlyField)), + () => ( + + {onlyField == null ? title : i18n.TOP(onlyField)} + + ), [onlyField, title] ); From db834698974ea40b452316d0ca3d37a1541bed64 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Tue, 19 Oct 2021 10:00:49 +0200 Subject: [PATCH 25/53] [Security Solution] Generate host isolation exceptions artifact (#115160) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/endpoint/lib/artifacts/common.ts | 3 + .../server/endpoint/lib/artifacts/lists.ts | 20 +++ .../manifest_manager/manifest_manager.test.ts | 137 ++++++++++-------- .../manifest_manager/manifest_manager.ts | 42 ++++++ 4 files changed, 140 insertions(+), 62 deletions(-) diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts index af5e38646430..60f91330d455 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/common.ts @@ -25,6 +25,9 @@ export const ArtifactConstants = { SUPPORTED_EVENT_FILTERS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], GLOBAL_EVENT_FILTERS_NAME: 'endpoint-eventfilterlist', + + SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS: ['macos', 'windows', 'linux'], + GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME: 'endpoint-hostisolationexceptionlist', }; export const ManifestConstants = { diff --git a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts index e27a09efd971..e26a2c7f4b4b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts +++ b/x-pack/plugins/security_solution/server/endpoint/lib/artifacts/lists.ts @@ -15,6 +15,7 @@ import { validate } from '@kbn/securitysolution-io-ts-utils'; import { ENDPOINT_EVENT_FILTERS_LIST_ID, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; @@ -65,6 +66,7 @@ export async function getFilteredEndpointExceptionList( | typeof ENDPOINT_LIST_ID | typeof ENDPOINT_TRUSTED_APPS_LIST_ID | typeof ENDPOINT_EVENT_FILTERS_LIST_ID + | typeof ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID ): Promise { const exceptions: WrappedTranslatedExceptionList = { entries: [] }; let page = 1; @@ -148,6 +150,24 @@ export async function getEndpointEventFiltersList( ); } +export async function getHostIsolationExceptionsList( + eClient: ExceptionListClient, + schemaVersion: string, + os: string, + policyId?: string +): Promise { + const osFilter = `exception-list-agnostic.attributes.os_types:\"${os}\"`; + const policyFilter = `(exception-list-agnostic.attributes.tags:\"policy:all\"${ + policyId ? ` or exception-list-agnostic.attributes.tags:\"policy:${policyId}\"` : '' + })`; + + return getFilteredEndpointExceptionList( + eClient, + schemaVersion, + `${osFilter} and ${policyFilter}`, + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID + ); +} /** * Translates Exception list items to Exceptions the endpoint can understand * @param exceptions diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts index d75e347b86bd..0ef2abd5f50a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.test.ts @@ -7,6 +7,7 @@ import { savedObjectsClientMock } from 'src/core/server/mocks'; import { + ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, ENDPOINT_LIST_ID, ENDPOINT_TRUSTED_APPS_LIST_ID, } from '@kbn/securitysolution-list-constants'; @@ -66,6 +67,12 @@ describe('ManifestManager', () => { const ARTIFACT_NAME_EVENT_FILTERS_MACOS = 'endpoint-eventfilterlist-macos-v1'; const ARTIFACT_NAME_EVENT_FILTERS_WINDOWS = 'endpoint-eventfilterlist-windows-v1'; const ARTIFACT_NAME_EVENT_FILTERS_LINUX = 'endpoint-eventfilterlist-linux-v1'; + const ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_MACOS = + 'endpoint-hostisolationexceptionlist-macos-v1'; + const ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_WINDOWS = + 'endpoint-hostisolationexceptionlist-windows-v1'; + const ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_LINUX = + 'endpoint-hostisolationexceptionlist-linux-v1'; let ARTIFACTS: InternalArtifactCompleteSchema[] = []; let ARTIFACTS_BY_ID: { [K: string]: InternalArtifactCompleteSchema } = {}; @@ -157,31 +164,29 @@ describe('ManifestManager', () => { const manifestManagerContext = buildManifestManagerContextMock({ savedObjectsClient }); const manifestManager = new ManifestManager(manifestManagerContext); - savedObjectsClient.get = jest - .fn() - .mockImplementation(async (objectType: string, id: string) => { - if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { - return { - attributes: { - created: '20-01-2020 10:00:00.000Z', - schemaVersion: 'v2', - semanticVersion: '1.0.0', - artifacts: [ - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_LINUX, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, - ], - }, - version: '2.0.0', - }; - } else { - return null; - } - }); + savedObjectsClient.get = jest.fn().mockImplementation(async (objectType: string) => { + if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { + return { + attributes: { + created: '20-01-2020 10:00:00.000Z', + schemaVersion: 'v2', + semanticVersion: '1.0.0', + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_LINUX, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + ], + }, + version: '2.0.0', + }; + } else { + return null; + } + }); ( manifestManagerContext.artifactClient as jest.Mocked @@ -218,31 +223,29 @@ describe('ManifestManager', () => { const manifestManagerContext = buildManifestManagerContextMock({ savedObjectsClient }); const manifestManager = new ManifestManager(manifestManagerContext); - savedObjectsClient.get = jest - .fn() - .mockImplementation(async (objectType: string, id: string) => { - if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { - return { - attributes: { - created: '20-01-2020 10:00:00.000Z', - schemaVersion: 'v2', - semanticVersion: '1.0.0', - artifacts: [ - { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_LINUX, policyId: undefined }, - { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, - { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, - ], - }, - version: '2.0.0', - }; - } else { - return null; - } - }); + savedObjectsClient.get = jest.fn().mockImplementation(async (objectType: string) => { + if (objectType === ManifestConstants.SAVED_OBJECT_TYPE) { + return { + attributes: { + created: '20-01-2020 10:00:00.000Z', + schemaVersion: 'v2', + semanticVersion: '1.0.0', + artifacts: [ + { artifactId: ARTIFACT_ID_EXCEPTIONS_MACOS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_LINUX, policyId: undefined }, + { artifactId: ARTIFACT_ID_EXCEPTIONS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_MACOS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_1 }, + { artifactId: ARTIFACT_ID_TRUSTED_APPS_WINDOWS, policyId: TEST_POLICY_ID_2 }, + ], + }, + version: '2.0.0', + }; + } else { + return null; + } + }); ( manifestManagerContext.artifactClient as jest.Mocked @@ -278,6 +281,9 @@ describe('ManifestManager', () => { ARTIFACT_NAME_EVENT_FILTERS_MACOS, ARTIFACT_NAME_EVENT_FILTERS_WINDOWS, ARTIFACT_NAME_EVENT_FILTERS_LINUX, + ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_MACOS, + ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_WINDOWS, + ARTIFACT_NAME_HOST_ISOLATION_EXCEPTIONS_LINUX, ]; const getArtifactIds = (artifacts: InternalArtifactSchema[]) => [ @@ -310,7 +316,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); const manifest = await manifestManager.buildNewManifest(); @@ -321,7 +327,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(9); + expect(artifacts.length).toBe(12); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); for (const artifact of artifacts) { @@ -336,16 +342,18 @@ describe('ManifestManager', () => { test('Builds fully new manifest if no baseline parameter passed and present exception list items', async () => { const exceptionListItem = getExceptionListItemSchemaMock({ os_types: ['macos'] }); const trustedAppListItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); + const hostIsolationExceptionsItem = getExceptionListItemSchemaMock({ os_types: ['linux'] }); const context = buildManifestManagerContextMock({}); const manifestManager = new ManifestManager(context); context.exceptionListClient.findExceptionListItem = mockFindExceptionListItemResponses({ [ENDPOINT_LIST_ID]: { macos: [exceptionListItem] }, [ENDPOINT_TRUSTED_APPS_LIST_ID]: { linux: [trustedAppListItem] }, + [ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID]: { linux: [hostIsolationExceptionsItem] }, }); context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); @@ -358,7 +366,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(9); + expect(artifacts.length).toBe(12); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(getArtifactObject(artifacts[0])).toStrictEqual({ @@ -374,6 +382,11 @@ describe('ManifestManager', () => { expect(getArtifactObject(artifacts[6])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[7])).toStrictEqual({ entries: [] }); expect(getArtifactObject(artifacts[8])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[9])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[10])).toStrictEqual({ entries: [] }); + expect(getArtifactObject(artifacts[11])).toStrictEqual({ + entries: translateToEndpointExceptions([hostIsolationExceptionsItem], 'v1'), + }); for (const artifact of artifacts) { expect(manifest.isDefaultArtifact(artifact)).toBe(true); @@ -395,7 +408,7 @@ describe('ManifestManager', () => { context.packagePolicyService.listIds = mockPolicyListIdsResponse([TEST_POLICY_ID_1]); context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); const oldManifest = await manifestManager.buildNewManifest(); @@ -413,7 +426,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(9); + expect(artifacts.length).toBe(12); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(artifacts[0]).toStrictEqual(oldManifest.getAllArtifacts()[0]); @@ -462,7 +475,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); @@ -474,7 +487,7 @@ describe('ManifestManager', () => { const artifacts = manifest.getAllArtifacts(); - expect(artifacts.length).toBe(10); + expect(artifacts.length).toBe(13); expect(getArtifactIds(artifacts)).toStrictEqual(SUPPORTED_ARTIFACT_NAMES); expect(getArtifactObject(artifacts[0])).toStrictEqual({ @@ -653,7 +666,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => object); + .mockImplementation((_type: string, object: InternalManifestSchema) => object); await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); @@ -690,7 +703,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.update = jest .fn() - .mockImplementation((type: string, id: string, object: InternalManifestSchema) => object); + .mockImplementation((_type: string, _id: string, object: InternalManifestSchema) => object); await expect(manifestManager.commit(manifest)).resolves.toBeUndefined(); @@ -1023,7 +1036,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); const manifest = await manifestManager.buildNewManifest(); @@ -1046,7 +1059,7 @@ describe('ManifestManager', () => { context.savedObjectsClient.create = jest .fn() - .mockImplementation((type: string, object: InternalManifestSchema) => ({ + .mockImplementation((_type: string, object: InternalManifestSchema) => ({ attributes: object, })); diff --git a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts index 5c1d327b1b89..736bf1c58cb9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts +++ b/x-pack/plugins/security_solution/server/endpoint/services/artifacts/manifest_manager/manifest_manager.ts @@ -26,6 +26,7 @@ import { getEndpointEventFiltersList, getEndpointExceptionList, getEndpointTrustedAppsList, + getHostIsolationExceptionsList, Manifest, } from '../../../lib/artifacts'; import { @@ -237,6 +238,46 @@ export class ManifestManager { ); } + protected async buildHostIsolationExceptionsArtifacts(): Promise { + const defaultArtifacts: InternalArtifactCompleteSchema[] = []; + const policySpecificArtifacts: Record = {}; + + for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { + defaultArtifacts.push(await this.buildHostIsolationExceptionForOs(os)); + } + + await iterateAllListItems( + (page) => this.listEndpointPolicyIds(page), + async (policyId) => { + for (const os of ArtifactConstants.SUPPORTED_HOST_ISOLATION_EXCEPTIONS_OPERATING_SYSTEMS) { + policySpecificArtifacts[policyId] = policySpecificArtifacts[policyId] || []; + policySpecificArtifacts[policyId].push( + await this.buildHostIsolationExceptionForOs(os, policyId) + ); + } + } + ); + + return { defaultArtifacts, policySpecificArtifacts }; + } + + protected async buildHostIsolationExceptionForOs( + os: string, + policyId?: string + ): Promise { + return buildArtifact( + await getHostIsolationExceptionsList( + this.exceptionListClient, + this.schemaVersion, + os, + policyId + ), + this.schemaVersion, + os, + ArtifactConstants.GLOBAL_HOST_ISOLATION_EXCEPTIONS_NAME + ); + } + /** * Writes new artifact SO. * @@ -381,6 +422,7 @@ export class ManifestManager { this.buildExceptionListArtifacts(), this.buildTrustedAppsArtifacts(), this.buildEventFiltersArtifacts(), + this.buildHostIsolationExceptionsArtifacts(), ]); const manifest = new Manifest({ From 4d2f76974b9fd88c6fadd446024dc192528da29f Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 19 Oct 2021 11:16:06 +0300 Subject: [PATCH 26/53] [Discover] Enable description for saved search modal (#114257) * [Discover] enable description for saved search * [Discover] remove i18n translations for removed description * [Discover] apply Tim's suggestion * [Discover] update snapshot * [Discover] reorder top nav buttons in tests * [Description] fix description save action Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/top_nav/discover_topnav.test.tsx | 2 +- .../components/top_nav/get_top_nav_links.test.ts | 16 +++++++++------- .../main/components/top_nav/get_top_nav_links.ts | 4 +++- .../main/components/top_nav/on_save_search.tsx | 10 +++++----- .../embeddable/saved_search_embeddable.tsx | 4 ++++ .../plugins/translations/translations/ja-JP.json | 1 - .../plugins/translations/translations/zh-CN.json | 1 - 7 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx index 4b572f6e348b..808346b53304 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/discover_topnav.test.tsx @@ -42,7 +42,7 @@ describe('Discover topnav component', () => { const props = getProps(true); const component = shallowWithIntl(); const topMenuConfig = component.props().config.map((obj: TopNavMenuData) => obj.id); - expect(topMenuConfig).toEqual(['options', 'new', 'save', 'open', 'share', 'inspect']); + expect(topMenuConfig).toEqual(['options', 'new', 'open', 'share', 'inspect', 'save']); }); test('generated config of TopNavMenu config is correct when no discover save permissions are assigned', () => { diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts index d31ac6e0f2fe..20c5b9bae332 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.test.ts @@ -53,13 +53,6 @@ test('getTopNavLinks result', () => { "run": [Function], "testId": "discoverNewButton", }, - Object { - "description": "Save Search", - "id": "save", - "label": "Save", - "run": [Function], - "testId": "discoverSaveButton", - }, Object { "description": "Open Saved Search", "id": "open", @@ -81,6 +74,15 @@ test('getTopNavLinks result', () => { "run": [Function], "testId": "openInspectorButton", }, + Object { + "description": "Save Search", + "emphasize": true, + "iconType": "save", + "id": "save", + "label": "Save", + "run": [Function], + "testId": "discoverSaveButton", + }, ] `); }); diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index 81be66247030..44d2999947f4 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -76,6 +76,8 @@ export const getTopNavLinks = ({ defaultMessage: 'Save Search', }), testId: 'discoverSaveButton', + iconType: 'save', + emphasize: true, run: () => onSaveSearch({ savedSearch, services, indexPattern, navigateTo, state }), }; @@ -153,9 +155,9 @@ export const getTopNavLinks = ({ return [ ...(services.capabilities.advancedSettings.save ? [options] : []), newSearch, - ...(services.capabilities.discover.save ? [saveSearch] : []), openSearch, shareSearch, inspectSearch, + ...(services.capabilities.discover.save ? [saveSearch] : []), ]; }; diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx index 18766b5df7f3..25b04e12c650 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/on_save_search.tsx @@ -98,16 +98,19 @@ export async function onSaveSearch({ const onSave = async ({ newTitle, newCopyOnSave, + newDescription, isTitleDuplicateConfirmed, onTitleDuplicate, }: { newTitle: string; newCopyOnSave: boolean; + newDescription: string; isTitleDuplicateConfirmed: boolean; onTitleDuplicate: () => void; }) => { const currentTitle = savedSearch.title; savedSearch.title = newTitle; + savedSearch.description = newDescription; const saveOptions: SaveSavedSearchOptions = { onTitleDuplicate, copyOnSave: newCopyOnSave, @@ -136,14 +139,11 @@ export async function onSaveSearch({ onClose={() => {}} title={savedSearch.title ?? ''} showCopyOnSave={!!savedSearch.id} + description={savedSearch.description} objectType={i18n.translate('discover.localMenu.saveSaveSearchObjectType', { defaultMessage: 'search', })} - description={i18n.translate('discover.localMenu.saveSaveSearchDescription', { - defaultMessage: - 'Save your Discover search so you can use it in visualizations and dashboards', - })} - showDescription={false} + showDescription={true} /> ); showSaveModal(saveModal, services.core.i18n.Context); diff --git a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx index 8849806cf595..89c47559d7b4 100644 --- a/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/application/embeddable/saved_search_embeddable.tsx @@ -402,6 +402,10 @@ export class SavedSearchEmbeddable return this.inspectorAdapters; } + public getDescription() { + return this.savedSearch.description; + } + public destroy() { super.destroy(); if (this.searchProps) { diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index acf9daebaf55..59a8cdaab641 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2528,7 +2528,6 @@ "discover.localMenu.openSavedSearchDescription": "保存された検索を開きます", "discover.localMenu.openTitle": "開く", "discover.localMenu.optionsDescription": "オプション", - "discover.localMenu.saveSaveSearchDescription": "ビジュアライゼーションとダッシュボードで使用できるように Discover の検索を保存します", "discover.localMenu.saveSaveSearchObjectType": "検索", "discover.localMenu.saveSearchDescription": "検索を保存します", "discover.localMenu.saveTitle": "保存", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5890f8553e7c..4e901f51d54d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2554,7 +2554,6 @@ "discover.localMenu.openSavedSearchDescription": "打开已保存搜索", "discover.localMenu.openTitle": "打开", "discover.localMenu.optionsDescription": "选项", - "discover.localMenu.saveSaveSearchDescription": "保存您的 Discover 搜索,以便可以在可视化和仪表板中使用该搜索", "discover.localMenu.saveSaveSearchObjectType": "搜索", "discover.localMenu.saveSearchDescription": "保存搜索", "discover.localMenu.saveTitle": "保存", From 32e1fc2396588b168650bd76ebd91127bc158576 Mon Sep 17 00:00:00 2001 From: Ryland Herrick Date: Tue, 19 Oct 2021 03:23:27 -0500 Subject: [PATCH 27/53] [Security Solution][Rules] Halt Indicator Match execution after interval has passed (#115288) * Throw an error to stop execution if IM rule has exceeded its interval * Extract and unit test our timeout validation * Add integration test around timeout behavior Configures a very slow rule to trigger a timeout and assert the corresponding failure. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../threat_mapping/create_threat_signals.ts | 6 ++- .../signals/threat_mapping/utils.test.ts | 23 ++++++++++ .../signals/threat_mapping/utils.ts | 21 +++++++++ .../tests/create_threat_matching.ts | 46 +++++++++++++++++++ 4 files changed, 95 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts index 169a820392a6..677a2028acdf 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/create_threat_signals.ts @@ -11,7 +11,7 @@ import { getThreatList, getThreatListCount } from './get_threat_list'; import { CreateThreatSignalsOptions } from './types'; import { createThreatSignal } from './create_threat_signal'; import { SearchAfterAndBulkCreateReturnType } from '../types'; -import { combineConcurrentResults } from './utils'; +import { buildExecutionIntervalValidator, combineConcurrentResults } from './utils'; import { buildThreatEnrichment } from './build_threat_enrichment'; export const createThreatSignals = async ({ @@ -46,6 +46,9 @@ export const createThreatSignals = async ({ const params = ruleSO.attributes.params; logger.debug(buildRuleMessage('Indicator matching rule starting')); const perPage = concurrentSearches * itemsPerSearch; + const verifyExecutionCanProceed = buildExecutionIntervalValidator( + ruleSO.attributes.schedule.interval + ); let results: SearchAfterAndBulkCreateReturnType = { success: true, @@ -99,6 +102,7 @@ export const createThreatSignals = async ({ }); while (threatList.hits.hits.length !== 0) { + verifyExecutionCanProceed(); const chunks = chunk(itemsPerSearch, threatList.hits.hits); logger.debug(buildRuleMessage(`${chunks.length} concurrent indicator searches are starting.`)); const concurrentSearchesPerformed = chunks.map>( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts index ec826b44023f..f029b02127b0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.test.ts @@ -10,6 +10,7 @@ import { sampleSignalHit } from '../__mocks__/es_results'; import { ThreatMatchNamedQuery } from './types'; import { + buildExecutionIntervalValidator, calculateAdditiveMax, calculateMax, calculateMaxLookBack, @@ -712,4 +713,26 @@ describe('utils', () => { }); }); }); + + describe('buildExecutionIntervalValidator', () => { + it('succeeds if the validator is called within the specified interval', () => { + const validator = buildExecutionIntervalValidator('1m'); + expect(() => validator()).not.toThrowError(); + }); + + it('throws an error if the validator is called after the specified interval', async () => { + const validator = buildExecutionIntervalValidator('1s'); + + await new Promise((r) => setTimeout(r, 1001)); + expect(() => validator()).toThrowError( + 'Current rule execution has exceeded its allotted interval (1s) and has been stopped.' + ); + }); + + it('throws an error if the interval cannot be parsed', () => { + expect(() => buildExecutionIntervalValidator('badString')).toThrowError( + 'Unable to parse rule interval (badString); stopping rule execution since allotted duration is undefined' + ); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts index 4d9fda43f032..99f6609faec9 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/utils.ts @@ -5,7 +5,10 @@ * 2.0. */ +import moment from 'moment'; + import { SearchAfterAndBulkCreateReturnType, SignalSourceHit } from '../types'; +import { parseInterval } from '../utils'; import { ThreatMatchNamedQuery } from './types'; /** @@ -146,3 +149,21 @@ export const decodeThreatMatchNamedQuery = (encoded: string): ThreatMatchNamedQu export const extractNamedQueries = (hit: SignalSourceHit): ThreatMatchNamedQuery[] => hit.matched_queries?.map((match) => decodeThreatMatchNamedQuery(match)) ?? []; + +export const buildExecutionIntervalValidator: (interval: string) => () => void = (interval) => { + const intervalDuration = parseInterval(interval); + + if (intervalDuration == null) { + throw new Error( + `Unable to parse rule interval (${interval}); stopping rule execution since allotted duration is undefined.` + ); + } + + const executionEnd = moment().add(intervalDuration); + return () => { + if (moment().isAfter(executionEnd)) { + const message = `Current rule execution has exceeded its allotted interval (${interval}) and has been stopped.`; + throw new Error(message); + } + }; +}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 0aad3c699805..223529fce54f 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -411,6 +411,52 @@ export default ({ getService }: FtrProviderContext) => { expect(signalsOpen.hits.hits.length).equal(0); }); + describe('timeout behavior', () => { + it('will return an error if a rule execution exceeds the rule interval', async () => { + const rule: CreateRulesSchema = { + description: 'Detecting root and admin users', + name: 'Query with a short interval', + severity: 'high', + index: ['auditbeat-*'], + type: 'threat_match', + risk_score: 55, + language: 'kuery', + rule_id: 'rule-1', + from: '1900-01-01T00:00:00.000Z', + query: '*:*', + threat_query: '*:*', // broad query to take more time + threat_index: ['auditbeat-*'], // We use auditbeat as both the matching index and the threat list for simplicity + threat_mapping: [ + { + entries: [ + { + field: 'host.name', + value: 'host.name', + type: 'mapping', + }, + ], + }, + ], + threat_filters: [], + concurrent_searches: 1, + interval: '1s', // short interval + items_per_search: 1, // iterate only 1 threat item per loop to ensure we're slow + }; + + const { id } = await createRule(supertest, rule); + await waitForRuleSuccessOrStatus(supertest, id, 'failed'); + + const { body } = await supertest + .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .set('kbn-xsrf', 'true') + .send({ ids: [id] }) + .expect(200); + expect(body[id].current_status.last_failure_message).to.contain( + 'execution has exceeded its allotted interval' + ); + }); + }); + describe('indicator enrichment', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/filebeat/threat_intel'); From 1fb28dcc69fe69993675cc198191dd2e91e092ea Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Tue, 19 Oct 2021 10:49:58 +0200 Subject: [PATCH 28/53] IM rule default interval timeout and lookback - 1h (#115185) * Make 1h default value for IM rule interval and lookback time * Fix test name * Move value to cosntants * Update lookback * Change lookback to 5 minutes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../indicator_match_rule.spec.ts | 19 +++++++++++++++++++ .../cypress/screens/create_new_rule.ts | 6 ++++++ .../rules/step_schedule_rule/index.tsx | 19 +++++++++++++++---- .../detection_engine/rules/create/index.tsx | 1 + 4 files changed, 41 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 871e50821b58..8735b8d49974 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -102,6 +102,12 @@ import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted, } from '../../tasks/create_new_rule'; +import { + SCHEDULE_INTERVAL_AMOUNT_INPUT, + SCHEDULE_INTERVAL_UNITS_INPUT, + SCHEDULE_LOOKBACK_AMOUNT_INPUT, + SCHEDULE_LOOKBACK_UNITS_INPUT, +} from '../../screens/create_new_rule'; import { goBackToRuleDetails, waitForKibana } from '../../tasks/edit_rule'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; @@ -383,6 +389,19 @@ describe('indicator match', () => { getIndicatorMappingComboField(2).should('not.exist'); }); }); + + describe('Schedule', () => { + it('IM rule has 1h time interval and lookback by default', () => { + selectIndicatorMatchType(); + fillDefineIndicatorMatchRuleAndContinue(getNewThreatIndicatorRule()); + fillAboutRuleAndContinue(getNewThreatIndicatorRule()); + + cy.get(SCHEDULE_INTERVAL_AMOUNT_INPUT).invoke('val').should('eql', '1'); + cy.get(SCHEDULE_INTERVAL_UNITS_INPUT).invoke('val').should('eql', 'h'); + cy.get(SCHEDULE_LOOKBACK_AMOUNT_INPUT).invoke('val').should('eql', '5'); + cy.get(SCHEDULE_LOOKBACK_UNITS_INPUT).invoke('val').should('eql', 'm'); + }); + }); }); describe('Generating signals', () => { diff --git a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts index 3510df618687..aadaa5dfa0d8 100644 --- a/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts +++ b/x-pack/plugins/security_solution/cypress/screens/create_new_rule.ts @@ -201,6 +201,12 @@ export const SCHEDULE_INTERVAL_AMOUNT_INPUT = export const SCHEDULE_INTERVAL_UNITS_INPUT = '[data-test-subj="detectionEngineStepScheduleRuleInterval"] [data-test-subj="timeType"]'; +export const SCHEDULE_LOOKBACK_AMOUNT_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="interval"]'; + +export const SCHEDULE_LOOKBACK_UNITS_INPUT = + '[data-test-subj="detectionEngineStepScheduleRuleFrom"] [data-test-subj="timeType"]'; + export const SEVERITY_DROPDOWN = '[data-test-subj="detectionEngineStepAboutRuleSeverity"] [data-test-subj="select"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx index 528494c9331a..9d7c2b76b385 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/step_schedule_rule/index.tsx @@ -6,6 +6,7 @@ */ import React, { FC, memo, useCallback, useEffect } from 'react'; +import { Type } from '@kbn/securitysolution-io-ts-alerting-types'; import { RuleStep, @@ -16,16 +17,25 @@ import { StepRuleDescription } from '../description_step'; import { ScheduleItem } from '../schedule_item_form'; import { Form, UseField, useForm } from '../../../../shared_imports'; import { StepContentWrapper } from '../step_content_wrapper'; +import { isThreatMatchRule } from '../../../../../common/detection_engine/utils'; import { NextStep } from '../next_step'; import { schema } from './schema'; interface StepScheduleRuleProps extends RuleStepProps { defaultValues?: ScheduleStepRule | null; + ruleType?: Type; } -const stepScheduleDefaultValue: ScheduleStepRule = { - interval: '5m', - from: '1m', +const DEFAULT_INTERVAL = '5m'; +const DEFAULT_FROM = '1m'; +const THREAT_MATCH_INTERVAL = '1h'; +const THREAT_MATCH_FROM = '5m'; + +const getStepScheduleDefaultValue = (ruleType: Type | undefined): ScheduleStepRule => { + return { + interval: isThreatMatchRule(ruleType) ? THREAT_MATCH_INTERVAL : DEFAULT_INTERVAL, + from: isThreatMatchRule(ruleType) ? THREAT_MATCH_FROM : DEFAULT_FROM, + }; }; const StepScheduleRuleComponent: FC = ({ @@ -37,8 +47,9 @@ const StepScheduleRuleComponent: FC = ({ isUpdateView = false, onSubmit, setForm, + ruleType, }) => { - const initialState = defaultValues ?? stepScheduleDefaultValue; + const initialState = defaultValues ?? getStepScheduleDefaultValue(ruleType); const { form } = useForm({ defaultValue: initialState, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx index a2f4385aeeb8..d37acaeb0ffe 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/create/index.tsx @@ -401,6 +401,7 @@ const CreateRulePageComponent: React.FC = () => { > Date: Tue, 19 Oct 2021 11:03:46 +0200 Subject: [PATCH 29/53] [Security Solution][Detections] Hide building block rules in "Security/Overview" (#105611) * Hide building block rules in "Security/Overview" * Add Cypress tests for alerts generated by building block rules Co-authored-by: Dmitry Shevchenko --- .../building_block_alerts.spec.ts | 40 +++++++++++++++++++ .../security_solution/cypress/objects/rule.ts | 20 ++++++++++ .../cypress/screens/overview.ts | 2 + .../cypress/tasks/api_calls/rules.ts | 1 + .../components/signals_by_category/index.tsx | 15 +++++-- .../use_filters_for_signals_by_category.ts | 37 +++++++++++++++++ 6 files changed, 111 insertions(+), 4 deletions(-) create mode 100644 x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts create mode 100644 x-pack/plugins/security_solution/public/overview/components/signals_by_category/use_filters_for_signals_by_category.ts diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts new file mode 100644 index 000000000000..262ffe8163e5 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/building_block_alerts.spec.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getBuildingBlockRule } from '../../objects/rule'; +import { OVERVIEW_ALERTS_HISTOGRAM } from '../../screens/overview'; +import { OVERVIEW } from '../../screens/security_header'; +import { goToRuleDetails } from '../../tasks/alerts_detection_rules'; +import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; +import { cleanKibana } from '../../tasks/common'; +import { waitForAlertsToPopulate, waitForTheRuleToBeExecuted } from '../../tasks/create_new_rule'; +import { loginAndWaitForPage } from '../../tasks/login'; +import { navigateFromHeaderTo } from '../../tasks/security_header'; +import { DETECTIONS_RULE_MANAGEMENT_URL } from '../../urls/navigation'; + +const EXPECTED_NUMBER_OF_ALERTS = 16; + +describe('Alerts generated by building block rules', () => { + beforeEach(() => { + cleanKibana(); + }); + + it('Alerts should be visible on the Rule Detail page and not visible on the Overview page', () => { + createCustomRuleActivated(getBuildingBlockRule()); + loginAndWaitForPage(DETECTIONS_RULE_MANAGEMENT_URL); + goToRuleDetails(); + waitForTheRuleToBeExecuted(); + + // Check that generated events are visible on the Details page + waitForAlertsToPopulate(EXPECTED_NUMBER_OF_ALERTS); + + navigateFromHeaderTo(OVERVIEW); + + // Check that generated events are hidden on the Overview page + cy.get(OVERVIEW_ALERTS_HISTOGRAM).should('contain.text', 'No data to display'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 4b061865d632..27973854097d 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -58,6 +58,7 @@ export interface CustomRule { lookBack: Interval; timeline: CompleteTimeline; maxSignals: number; + buildingBlockType?: string; } export interface ThresholdRule extends CustomRule { @@ -188,6 +189,25 @@ export const getNewRule = (): CustomRule => ({ maxSignals: 100, }); +export const getBuildingBlockRule = (): CustomRule => ({ + customQuery: 'host.name: *', + index: getIndexPatterns(), + name: 'Building Block Rule Test', + description: 'The new rule description.', + severity: 'High', + riskScore: '17', + tags: ['test', 'newRule'], + referenceUrls: ['http://example.com/', 'https://example.com/'], + falsePositivesExamples: ['False1', 'False2'], + mitre: [getMitre1(), getMitre2()], + note: '# test markdown', + runsEvery: getRunsEvery(), + lookBack: getLookBack(), + timeline: getTimeline(), + maxSignals: 100, + buildingBlockType: 'default', +}); + export const getUnmappedRule = (): CustomRule => ({ customQuery: '*:*', index: ['unmapped*'], diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1376a39e5ee7..1945b7e3ce3e 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -166,3 +166,5 @@ export const OVERVIEW_RISKY_HOSTS_VIEW_DASHBOARD_BUTTON = export const OVERVIEW_RISKY_HOSTS_TOTAL_EVENT_COUNT = `${OVERVIEW_RISKY_HOSTS_LINKS} [data-test-subj="header-panel-subtitle"]`; export const OVERVIEW_RISKY_HOSTS_ENABLE_MODULE_BUTTON = '[data-test-subj="risky-hosts-enable-module-button"]'; + +export const OVERVIEW_ALERTS_HISTOGRAM = '[data-test-subj="alerts-histogram-panel"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index 04ff0fcabc08..fd2838e5b3ca 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -114,6 +114,7 @@ export const createCustomRuleActivated = ( enabled: true, tags: ['rule1'], max_signals: maxSignals, + building_block_type: rule.buildingBlockType, }, headers: { 'kbn-xsrf': 'cypress-creds' }, failOnStatusCode: false, diff --git a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx index 321e6d00b530..cbeb1464e1b4 100644 --- a/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/signals_by_category/index.tsx @@ -7,19 +7,24 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; +import { Filter, Query } from '@kbn/es-query'; import { AlertsHistogramPanel } from '../../../detections/components/alerts_kpis/alerts_histogram_panel'; import { useSignalIndex } from '../../../detections/containers/detection_engine/alerts/use_signal_index'; import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; -import { Filter, Query } from '../../../../../../../src/plugins/data/public'; + import { InputsModelId } from '../../../common/store/inputs/constants'; -import * as i18n from '../../pages/translations'; import { UpdateDateRange } from '../../../common/components/charts/common'; + import { AlertsStackByField } from '../../../detections/components/alerts_kpis/common/types'; +import * as i18n from '../../pages/translations'; + +import { useFiltersForSignalsByCategory } from './use_filters_for_signals_by_category'; + interface Props { combinedQueries?: string; - filters?: Filter[]; + filters: Filter[]; headerChildren?: React.ReactNode; /** Override all defaults, and only display this field */ onlyField?: AlertsStackByField; @@ -43,6 +48,8 @@ const SignalsByCategoryComponent: React.FC = ({ }) => { const dispatch = useDispatch(); const { signalIndexName } = useSignalIndex(); + const filtersForSignalsByCategory = useFiltersForSignalsByCategory(filters); + const updateDateRangeCallback = useCallback( ({ x }) => { if (!x) { @@ -63,7 +70,7 @@ const SignalsByCategoryComponent: React.FC = ({ return ( { + // TODO: Once we are past experimental phase this code should be removed + const ruleRegistryEnabled = useIsExperimentalFeatureEnabled('ruleRegistryEnabled'); + + const resultingFilters = useMemo( + () => [ + ...baseFilters, + ...(ruleRegistryEnabled + ? buildShowBuildingBlockFilterRuleRegistry(SHOW_BUILDING_BLOCK_ALERTS) // TODO: Once we are past experimental phase this code should be removed + : buildShowBuildingBlockFilter(SHOW_BUILDING_BLOCK_ALERTS)), + ], + [baseFilters, ruleRegistryEnabled] + ); + + return resultingFilters; +}; From b9024c6ad5561ddd5dc415c1dec786e1b534c75c Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Tue, 19 Oct 2021 11:08:20 +0200 Subject: [PATCH 30/53] timepicker-url sync functional test (#115173) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../test/functional/apps/monitoring/index.js | 2 +- .../functional/apps/monitoring/time_filter.js | 26 +++++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 6a5b6ea81317..a67964d32516 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -42,7 +42,7 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./beats/listing')); loadTestFile(require.resolve('./beats/beat_detail')); - // loadTestFile(require.resolve('./time_filter')); + loadTestFile(require.resolve('./time_filter')); loadTestFile(require.resolve('./enable_monitoring')); loadTestFile(require.resolve('./setup/metricbeat_migration')); diff --git a/x-pack/test/functional/apps/monitoring/time_filter.js b/x-pack/test/functional/apps/monitoring/time_filter.js index 910b91d07039..76e7bc5cd043 100644 --- a/x-pack/test/functional/apps/monitoring/time_filter.js +++ b/x-pack/test/functional/apps/monitoring/time_filter.js @@ -12,22 +12,44 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['header', 'timePicker']); const testSubjects = getService('testSubjects'); const clusterList = getService('monitoringClusterList'); + const browser = getService('browser'); + + const assertTimePickerRange = async (start, end) => { + const timeConfig = await PageObjects.timePicker.getTimeConfig(); + expect(timeConfig.start).to.eql(start); + expect(timeConfig.end).to.eql(end); + }; describe('Timefilter', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); + const from = 'Aug 15, 2017 @ 21:00:00.000'; + const to = 'Aug 16, 2017 @ 00:00:00.000'; + before(async () => { await setup('x-pack/test/functional/es_archives/monitoring/multicluster', { - from: 'Aug 15, 2017 @ 21:00:00.000', - to: 'Aug 16, 2017 @ 00:00:00.000', + from, + to, }); await clusterList.assertDefaults(); + await clusterList.closeAlertsModal(); }); after(async () => { await tearDown(); }); + it('syncs timepicker with url hash updates', async () => { + await assertTimePickerRange(from, to); + + await browser.execute(() => { + const hash = window.location.hash; + window.location.hash = hash.replace(/time:\(([^)]+)\)/, 'time:(from:now-15m,to:now)'); + }); + + await assertTimePickerRange('~ 15 minutes ago', 'now'); + }); + // FLAKY: https://github.com/elastic/kibana/issues/48910 it.skip('should send another request when clicking Refresh', async () => { await testSubjects.click('querySubmitButton'); From 5fe9a319c001444819d0f79e5ea2e3a375eb2771 Mon Sep 17 00:00:00 2001 From: mgiota Date: Tue, 19 Oct 2021 11:09:48 +0200 Subject: [PATCH 31/53] [RAC] [Metrics UI] Include group name in the reason message (#115171) * [RAC] [Metrics UI] Include group name in the reason message * remove console log * fix i18n errors * fix more i18n errors * fix i18n & check errors and move group to the end of the reason text * add empty lines at the end of translation files * fix more i18n tests * try to remove manually added translations * Revert "try to remove manually added translations" This reverts commit 6949af2f70aff46b088bab5c942497ad46081d90. * apply i18n_check fix and reorder values in the formatted reason * log threshold reformat reason message and move group info at the end --- .../server/lib/alerting/common/messages.ts | 18 ++++++---- .../inventory_metric_threshold_executor.ts | 35 +++++++++++-------- .../log_threshold/reason_formatters.ts | 4 +-- .../metric_threshold_executor.ts | 9 ++--- .../translations/translations/ja-JP.json | 3 -- .../translations/translations/zh-CN.json | 3 -- 6 files changed, 40 insertions(+), 32 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts index 084043f357bb..23c89abf4a7a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/common/messages.ts +++ b/x-pack/plugins/infra/server/lib/alerting/common/messages.ts @@ -109,15 +109,17 @@ const thresholdToI18n = ([a, b]: Array) => { }; export const buildFiredAlertReason: (alertResult: { + group: string; metric: string; comparator: Comparator; threshold: Array; currentValue: number | string; -}) => string = ({ metric, comparator, threshold, currentValue }) => +}) => string = ({ group, metric, comparator, threshold, currentValue }) => i18n.translate('xpack.infra.metrics.alerting.threshold.firedAlertReason', { defaultMessage: - '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue})', + '{metric} is {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}', values: { + group, metric, comparator: comparatorToI18n(comparator, threshold.map(toNumber), toNumber(currentValue)), threshold: thresholdToI18n(threshold), @@ -126,14 +128,15 @@ export const buildFiredAlertReason: (alertResult: { }); export const buildRecoveredAlertReason: (alertResult: { + group: string; metric: string; comparator: Comparator; threshold: Array; currentValue: number | string; -}) => string = ({ metric, comparator, threshold, currentValue }) => +}) => string = ({ group, metric, comparator, threshold, currentValue }) => i18n.translate('xpack.infra.metrics.alerting.threshold.recoveredAlertReason', { defaultMessage: - '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue})', + '{metric} is now {comparator} a threshold of {threshold} (current value is {currentValue}) for {group}', values: { metric, comparator: recoveredComparatorToI18n( @@ -143,19 +146,22 @@ export const buildRecoveredAlertReason: (alertResult: { ), threshold: thresholdToI18n(threshold), currentValue, + group, }, }); export const buildNoDataAlertReason: (alertResult: { + group: string; metric: string; timeSize: number; timeUnit: string; -}) => string = ({ metric, timeSize, timeUnit }) => +}) => string = ({ group, metric, timeSize, timeUnit }) => i18n.translate('xpack.infra.metrics.alerting.threshold.noDataAlertReason', { - defaultMessage: '{metric} has reported no data over the past {interval}', + defaultMessage: '{metric} has reported no data over the past {interval} for {group}', values: { metric, interval: `${timeSize}${timeUnit}`, + group, }, }); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index 5cd093c6f147..3dd702126735 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -102,18 +102,18 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ) ); const inventoryItems = Object.keys(first(results)!); - for (const item of inventoryItems) { + for (const group of inventoryItems) { // AND logic; all criteria must be across the threshold const shouldAlertFire = results.every((result) => { // Grab the result of the most recent bucket - return last(result[item].shouldFire); + return last(result[group].shouldFire); }); - const shouldAlertWarn = results.every((result) => last(result[item].shouldWarn)); + const shouldAlertWarn = results.every((result) => last(result[group].shouldWarn)); // AND logic; because we need to evaluate all criteria, if one of them reports no data then the // whole alert is in a No Data/Error state - const isNoData = results.some((result) => last(result[item].isNoData)); - const isError = results.some((result) => result[item].isError); + const isNoData = results.some((result) => last(result[group].isNoData)); + const isError = results.some((result) => result[group].isError); const nextState = isError ? AlertStates.ERROR @@ -129,7 +129,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = reason = results .map((result) => buildReasonWithVerboseMetricName( - result[item], + group, + result[group], buildFiredAlertReason, nextState === AlertStates.WARNING ) @@ -142,19 +143,23 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = */ // } else if (nextState === AlertStates.OK && prevState?.alertState === AlertStates.ALERT) { // reason = results - // .map((result) => buildReasonWithVerboseMetricName(result[item], buildRecoveredAlertReason)) + // .map((result) => buildReasonWithVerboseMetricName(group, result[group], buildRecoveredAlertReason)) // .join('\n'); } if (alertOnNoData) { if (nextState === AlertStates.NO_DATA) { reason = results - .filter((result) => result[item].isNoData) - .map((result) => buildReasonWithVerboseMetricName(result[item], buildNoDataAlertReason)) + .filter((result) => result[group].isNoData) + .map((result) => + buildReasonWithVerboseMetricName(group, result[group], buildNoDataAlertReason) + ) .join('\n'); } else if (nextState === AlertStates.ERROR) { reason = results - .filter((result) => result[item].isError) - .map((result) => buildReasonWithVerboseMetricName(result[item], buildErrorAlertReason)) + .filter((result) => result[group].isError) + .map((result) => + buildReasonWithVerboseMetricName(group, result[group], buildErrorAlertReason) + ) .join('\n'); } } @@ -166,7 +171,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; - const alertInstance = alertInstanceFactory(`${item}`, reason); + const alertInstance = alertInstanceFactory(`${group}`, reason); alertInstance.scheduleActions( /** * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on @@ -174,12 +179,12 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = */ actionGroupId as unknown as InventoryMetricThresholdAllowedActionGroups, { - group: item, + group, alertState: stateToAlertMessage[nextState], reason, timestamp: moment().toISOString(), value: mapToConditionsLookup(results, (result) => - formatMetric(result[item].metric, result[item].currentValue) + formatMetric(result[group].metric, result[group].currentValue) ), threshold: mapToConditionsLookup(criteria, (c) => c.threshold), metric: mapToConditionsLookup(criteria, (c) => c.metric), @@ -190,6 +195,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = }); const buildReasonWithVerboseMetricName = ( + group: string, resultItem: any, buildReason: (r: any) => string, useWarningThreshold?: boolean @@ -197,6 +203,7 @@ const buildReasonWithVerboseMetricName = ( if (!resultItem) return ''; const resultWithVerboseMetricName = { ...resultItem, + group, metric: toMetricOpt(resultItem.metric)?.text || (resultItem.metric === 'custom' diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts index cd579b9965b6..f70e0a0140ce 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/reason_formatters.ts @@ -34,7 +34,7 @@ export const getReasonMessageForGroupedCountAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.groupedCountAlertReasonDescription', { defaultMessage: - '{groupName}: {actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions.', + '{actualCount, plural, one {{actualCount} log entry} other {{actualCount} log entries} } ({translatedComparator} {expectedCount}) match the conditions for {groupName}.', values: { actualCount, expectedCount, @@ -66,7 +66,7 @@ export const getReasonMessageForGroupedRatioAlert = ( ) => i18n.translate('xpack.infra.logs.alerting.threshold.groupedRatioAlertReasonDescription', { defaultMessage: - '{groupName}: The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}).', + 'The log entries ratio is {actualRatio} ({translatedComparator} {expectedRatio}) for {groupName}.', values: { actualRatio, expectedRatio, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index af5f945eeb4b..e4887e922bb6 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -143,9 +143,10 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => if (nextState === AlertStates.ALERT || nextState === AlertStates.WARNING) { reason = alertResults .map((result) => - buildFiredAlertReason( - formatAlertResult(result[group], nextState === AlertStates.WARNING) - ) + buildFiredAlertReason({ + ...formatAlertResult(result[group], nextState === AlertStates.WARNING), + group, + }) ) .join('\n'); /* @@ -181,7 +182,7 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => if (nextState === AlertStates.NO_DATA) { reason = alertResults .filter((result) => result[group].isNoData) - .map((result) => buildNoDataAlertReason(result[group])) + .map((result) => buildNoDataAlertReason({ ...result[group], group })) .join('\n'); } else if (nextState === AlertStates.ERROR) { reason = alertResults diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 59a8cdaab641..aac0f651b8de 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -13446,15 +13446,12 @@ "xpack.infra.metrics.alerting.threshold.errorAlertReason": "{metric}のデータのクエリを試行しているときに、Elasticsearchが失敗しました", "xpack.infra.metrics.alerting.threshold.errorState": "エラー", "xpack.infra.metrics.alerting.threshold.fired": "アラート", - "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})", "xpack.infra.metrics.alerting.threshold.gtComparator": "より大きい", "xpack.infra.metrics.alerting.threshold.ltComparator": "より小さい", - "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric}は過去{interval}にデータを報告していません", "xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[データなし]", "xpack.infra.metrics.alerting.threshold.noDataState": "データなし", "xpack.infra.metrics.alerting.threshold.okState": "OK [回復済み]", "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "の間にない", - "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric}は{comparator} {threshold}のしきい値です(現在の値は{currentValue})", "xpack.infra.metrics.alerting.threshold.thresholdRange": "{a}と{b}", "xpack.infra.metrics.alerting.threshold.warning": "警告", "xpack.infra.metrics.alerting.threshold.warningState": "警告", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4e901f51d54d..50d90f514458 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -13634,15 +13634,12 @@ "xpack.infra.metrics.alerting.threshold.errorAlertReason": "Elasticsearch 尝试查询 {metric} 的数据时出现故障", "xpack.infra.metrics.alerting.threshold.errorState": "错误", "xpack.infra.metrics.alerting.threshold.fired": "告警", - "xpack.infra.metrics.alerting.threshold.firedAlertReason": "{metric} {comparator}阈值 {threshold}(当前值为 {currentValue})", "xpack.infra.metrics.alerting.threshold.gtComparator": "大于", "xpack.infra.metrics.alerting.threshold.ltComparator": "小于", - "xpack.infra.metrics.alerting.threshold.noDataAlertReason": "{metric} 在过去 {interval}中未报告数据", "xpack.infra.metrics.alerting.threshold.noDataFormattedValue": "[无数据]", "xpack.infra.metrics.alerting.threshold.noDataState": "无数据", "xpack.infra.metrics.alerting.threshold.okState": "正常 [已恢复]", "xpack.infra.metrics.alerting.threshold.outsideRangeComparator": "不介于", - "xpack.infra.metrics.alerting.threshold.recoveredAlertReason": "{metric} 现在{comparator}阈值 {threshold}(当前值为 {currentValue})", "xpack.infra.metrics.alerting.threshold.thresholdRange": "{a} 和 {b}", "xpack.infra.metrics.alerting.threshold.warning": "警告", "xpack.infra.metrics.alerting.threshold.warningState": "警告", From f9afe67f1e554cf3b295c4c43bf2b3f68c103120 Mon Sep 17 00:00:00 2001 From: Andrew Goldstein Date: Tue, 19 Oct 2021 04:10:14 -0600 Subject: [PATCH 32/53] [Security Solution] Improves the formatting of array values and JSON in the Event and Alert Details panels (#115141) ## [Security Solution] Improves the formatting of array values and JSON in the Event and Alert Details panels This PR improves the formatting of array values and JSON in the Event and Alert details panels by: - in the `Table` tab, formatting array values such that each value appears on a separate line, (instead of joining the values on a single line) - in the `JSON` tab, displaying the raw search hit JSON, instead displaying a JSON representation based on the `Fields` API ### Table value formatting In the Event and Alert details `Table` tab, array values were joined on a single line, as shown in the _before_ screenshot below: ![event-details-value-formatting-before](https://user-images.githubusercontent.com/4459398/137524968-6450cd73-3154-457d-b850-32a3e7faaab2.png) _Above: (before) array values were joined on a single line_ Array values are now formatted such that each value appears on a separate line, as shown in the _after_ screenshot below: ![event-details-value-formatting-after](https://user-images.githubusercontent.com/4459398/137436705-b0bec735-5a83-402e-843a-2776e1c80da9.png) _Above: (after) array values each appear on a separte line_ ### JSON formatting The `JSON` tab previously displayed a JSON representation based on the `Fields` API. Array values were previously represented as a joined string, as shown in the _before_ screenshot below: ![event-details-json-formatting-before](https://user-images.githubusercontent.com/4459398/137525039-d1b14f21-5f9c-4201-905e-8b08f00bb5a0.png) _Above: (before) array values were previously represented as a joined string_ The `JSON` tab now displays the raw search hit JSON, per the _after_ screenshot below: ![event-details-json-formatting-after](https://user-images.githubusercontent.com/4459398/137437257-330c5b49-a4ad-418e-a976-923f7a35c0cf.png) _Above: (after) the `JSON` tab displays the raw search hit_ CC @monina-n @paulewing --- .../detection_alerts/alerts_details.spec.ts | 20 +- .../detection_alerts/cti_enrichments.spec.ts | 49 ++- .../cypress/screens/alerts_details.ts | 2 + .../alert_summary_view.test.tsx.snap | 120 ++++-- .../__snapshots__/json_view.test.tsx.snap | 343 ++++++++++++++++-- .../event_details/event_details.test.tsx | 3 +- .../event_details/event_details.tsx | 6 +- .../event_details/json_view.test.tsx | 49 +-- .../components/event_details/json_view.tsx | 23 +- .../table/field_value_cell.test.tsx | 193 ++++++++++ .../event_details/table/field_value_cell.tsx | 26 +- .../public/common/mock/mock_detail_item.ts | 188 ++++++++++ .../event_details/expandable_event.tsx | 3 + .../side_panel/event_details/index.tsx | 4 +- .../timelines/containers/details/index.tsx | 7 +- .../timeline/events/details/index.ts | 1 + .../timeline/factory/events/details/index.ts | 4 + 17 files changed, 872 insertions(+), 169 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 674114188632..7b792f8d560f 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { ALERT_FLYOUT, CELL_TEXT, JSON_LINES, TABLE_ROWS } from '../../screens/alerts_details'; +import { ALERT_FLYOUT, CELL_TEXT, JSON_TEXT, TABLE_ROWS } from '../../screens/alerts_details'; import { expandFirstAlert, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; -import { openJsonView, openTable, scrollJsonViewToBottom } from '../../tasks/alerts_details'; +import { openJsonView, openTable } from '../../tasks/alerts_details'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; import { esArchiverLoad } from '../../tasks/es_archiver'; @@ -36,20 +36,14 @@ describe('Alert details with unmapped fields', () => { }); it('Displays the unmapped field on the JSON view', () => { - const expectedUnmappedField = { line: 2, text: ' "unmapped": "This is the unmapped field"' }; + const expectedUnmappedValue = 'This is the unmapped field'; openJsonView(); - scrollJsonViewToBottom(); - cy.get(ALERT_FLYOUT) - .find(JSON_LINES) - .then((elements) => { - const length = elements.length; - cy.wrap(elements) - .eq(length - expectedUnmappedField.line) - .invoke('text') - .should('include', expectedUnmappedField.text); - }); + cy.get(JSON_TEXT).then((x) => { + const parsed = JSON.parse(x.text()); + expect(parsed._source.unmapped).to.equal(expectedUnmappedValue); + }); }); it('Displays the unmapped field on the table', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index b3c6abcd8e42..f15e7adbbca4 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -10,7 +10,7 @@ import { cleanKibana, reload } from '../../tasks/common'; import { esArchiverLoad, esArchiverUnload } from '../../tasks/es_archiver'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { - JSON_LINES, + JSON_TEXT, TABLE_CELL, TABLE_ROWS, THREAT_DETAILS_VIEW, @@ -28,11 +28,7 @@ import { viewThreatIntelTab, } from '../../tasks/alerts'; import { createCustomIndicatorRule } from '../../tasks/api_calls/rules'; -import { - openJsonView, - openThreatIndicatorDetails, - scrollJsonViewToBottom, -} from '../../tasks/alerts_details'; +import { openJsonView, openThreatIndicatorDetails } from '../../tasks/alerts_details'; import { ALERTS_URL } from '../../urls/navigation'; import { addsFieldsToTimeline } from '../../tasks/rule_details'; @@ -76,26 +72,39 @@ describe('CTI Enrichment', () => { it('Displays persisted enrichments on the JSON view', () => { const expectedEnrichment = [ - { line: 4, text: ' "threat": {' }, { - line: 3, - text: ' "enrichments": "{\\"indicator\\":{\\"first_seen\\":\\"2021-03-10T08:02:14.000Z\\",\\"file\\":{\\"size\\":80280,\\"pe\\":{},\\"type\\":\\"elf\\",\\"hash\\":{\\"sha256\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"tlsh\\":\\"6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE\\",\\"ssdeep\\":\\"1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL\\",\\"md5\\":\\"9b6c3518a91d23ed77504b5416bfb5b3\\"}},\\"type\\":\\"file\\"},\\"matched\\":{\\"atomic\\":\\"a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3\\",\\"field\\":\\"myhash.mysha256\\",\\"id\\":\\"84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f\\",\\"index\\":\\"logs-ti_abusech.malware\\",\\"type\\":\\"indicator_match_rule\\"}}"', + indicator: { + first_seen: '2021-03-10T08:02:14.000Z', + file: { + size: 80280, + pe: {}, + type: 'elf', + hash: { + sha256: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + tlsh: '6D7312E017B517CC1371A8353BED205E9128223972AE35302E97528DF957703BAB2DBE', + ssdeep: + '1536:87vbq1lGAXSEYQjbChaAU2yU23M51DjZgSQAvcYkFtZTjzBht5:8D+CAXFYQChaAUk5ljnQssL', + md5: '9b6c3518a91d23ed77504b5416bfb5b3', + }, + }, + type: 'file', + }, + matched: { + atomic: 'a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3', + field: 'myhash.mysha256', + id: '84cf452c1e0375c3d4412cb550bd1783358468a3b3b777da4829d72c7d6fb74f', + index: 'logs-ti_abusech.malware', + type: 'indicator_match_rule', + }, }, - { line: 2, text: ' }' }, ]; expandFirstAlert(); openJsonView(); - scrollJsonViewToBottom(); - - cy.get(JSON_LINES).then((elements) => { - const length = elements.length; - expectedEnrichment.forEach((enrichment) => { - cy.wrap(elements) - .eq(length - enrichment.line) - .invoke('text') - .should('include', enrichment.text); - }); + + cy.get(JSON_TEXT).then((x) => { + const parsed = JSON.parse(x.text()); + expect(parsed._source.threat.enrichments).to.deep.equal(expectedEnrichment); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts index c740a669d059..584fba05452f 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts_details.ts @@ -28,6 +28,8 @@ export const JSON_LINES = '.euiCodeBlock__line'; export const JSON_VIEW_TAB = '[data-test-subj="jsonViewTab"]'; +export const JSON_TEXT = '[data-test-subj="jsonView"]'; + export const TABLE_CELL = '.euiTableRowCell'; export const TABLE_TAB = '[data-test-subj="tableTab"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap index d367c68586be..930e1282ebca 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/event_details/__snapshots__/alert_summary_view.test.tsx.snap @@ -138,12 +138,17 @@ exports[`AlertSummaryView Behavior event code renders additional summary rows 1` class="euiTableCellContent flyoutOverviewDescription euiTableCellContent--overflowingContent" >
- open +
+ open +
- xxx +
+ xxx +
- low +
+ low +
- 21 +
+ 21 +
- windows-native +
+ windows-native +
- administrator +
+ administrator +
- open +
+ open +
- xxx +
+ xxx +
- low +
+ low +
- 21 +
+ 21 +
- windows-native +
+ windows-native +
- administrator +
+ administrator +
{ - "_id": "pEMaMmkBUV60JmNWmWVi", - "_index": "filebeat-8.0.0-2019.02.19-000001", + "_index": ".ds-logs-endpoint.events.network-default-2021.09.28-000001", + "_id": "TUWyf3wBFCFU0qRJTauW", "_score": 1, - "_type": "_doc", - "@timestamp": "2019-02-28T16:50:54.621Z", - "agent": { - "ephemeral_id": "9d391ef2-a734-4787-8891-67031178c641", - "hostname": "siem-kibana", - "id": "5de03d5f-52f3-482e-91d4-853c7de073c3", - "type": "filebeat", - "version": "8.0.0" - }, - "cloud": { - "availability_zone": "projects/189716325846/zones/us-east1-b", - "instance": { - "id": "5412578377715150143", - "name": "siem-kibana" + "_source": { + "agent": { + "id": "2ac9e9b3-f6d5-4ce6-915d-8f1f8f413624", + "type": "endpoint", + "version": "8.0.0-SNAPSHOT" }, - "machine": { - "type": "projects/189716325846/machineTypes/n1-standard-1" + "process": { + "Ext": { + "ancestry": [ + "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyMzY0LTEzMjc4NjA2NTAyLjA=", + "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTEtMTMyNzA3Njg2OTIuMA==" + ] + }, + "name": "filebeat", + "pid": 22535, + "entity_id": "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyNTM1LTEzMjc4NjA2NTI4LjA=", + "executable": "/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat" }, - "project": { - "id": "elastic-beats" + "destination": { + "address": "127.0.0.1", + "port": 9200, + "ip": "127.0.0.1" }, - "provider": "gce" - }, - "destination": { - "bytes": 584, - "ip": "10.47.8.200", - "packets": 4, - "port": 902 + "source": { + "address": "127.0.0.1", + "port": 54146, + "ip": "127.0.0.1" + }, + "message": "Endpoint network event", + "network": { + "transport": "tcp", + "type": "ipv4" + }, + "@timestamp": "2021-10-14T16:45:58.0310772Z", + "ecs": { + "version": "1.11.0" + }, + "data_stream": { + "namespace": "default", + "type": "logs", + "dataset": "endpoint.events.network" + }, + "elastic": { + "agent": { + "id": "12345" + } + }, + "host": { + "hostname": "test-linux-1", + "os": { + "Ext": { + "variant": "Debian" + }, + "kernel": "4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-2 (2021-06-21)", + "name": "Linux", + "family": "debian", + "type": "linux", + "version": "10", + "platform": "debian", + "full": "Debian 10" + }, + "ip": [ + "127.0.0.1", + "::1", + "10.1.2.3", + "2001:0DB8:AC10:FE01::" + ], + "name": "test-linux-1", + "id": "76ea303129f249aa7382338e4263eac1", + "mac": [ + "aa:bb:cc:dd:ee:ff" + ], + "architecture": "x86_64" + }, + "event": { + "agent_id_status": "verified", + "sequence": 44872, + "ingested": "2021-10-14T16:46:04Z", + "created": "2021-10-14T16:45:58.0310772Z", + "kind": "event", + "module": "endpoint", + "action": "connection_attempted", + "id": "MKPXftjGeHiQzUNj++++nn6R", + "category": [ + "network" + ], + "type": [ + "start" + ], + "dataset": "endpoint.events.network", + "outcome": "unknown" + }, + "user": { + "Ext": { + "real": { + "name": "root", + "id": 0 + } + }, + "name": "root", + "id": 0 + }, + "group": { + "Ext": { + "real": { + "name": "root", + "id": 0 + } + }, + "name": "root", + "id": 0 + } }, - "event": { - "kind": "event" + "fields": { + "host.os.full.text": [ + "Debian 10" + ], + "event.category": [ + "network" + ], + "process.name.text": [ + "filebeat" + ], + "host.os.name.text": [ + "Linux" + ], + "host.os.full": [ + "Debian 10" + ], + "host.hostname": [ + "test-linux-1" + ], + "process.pid": [ + 22535 + ], + "host.mac": [ + "42:01:0a:c8:00:32" + ], + "elastic.agent.id": [ + "abcdefg-f6d5-4ce6-915d-8f1f8f413624" + ], + "host.os.version": [ + "10" + ], + "host.os.name": [ + "Linux" + ], + "source.ip": [ + "127.0.0.1" + ], + "destination.address": [ + "127.0.0.1" + ], + "host.name": [ + "test-linux-1" + ], + "event.agent_id_status": [ + "verified" + ], + "event.kind": [ + "event" + ], + "event.outcome": [ + "unknown" + ], + "group.name": [ + "root" + ], + "user.id": [ + "0" + ], + "host.os.type": [ + "linux" + ], + "process.Ext.ancestry": [ + "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyMzY0LTEzMjc4NjA2NTAyLjA=", + "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTEtMTMyNzA3Njg2OTIuMA==" + ], + "user.Ext.real.id": [ + "0" + ], + "data_stream.type": [ + "logs" + ], + "host.architecture": [ + "x86_64" + ], + "process.name": [ + "filebeat" + ], + "agent.id": [ + "2ac9e9b3-f6d5-4ce6-915d-8f1f8f413624" + ], + "source.port": [ + 54146 + ], + "ecs.version": [ + "1.11.0" + ], + "event.created": [ + "2021-10-14T16:45:58.031Z" + ], + "agent.version": [ + "8.0.0-SNAPSHOT" + ], + "host.os.family": [ + "debian" + ], + "destination.port": [ + 9200 + ], + "group.id": [ + "0" + ], + "user.name": [ + "root" + ], + "source.address": [ + "127.0.0.1" + ], + "process.entity_id": [ + "MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyNTM1LTEzMjc4NjA2NTI4LjA=" + ], + "host.ip": [ + "127.0.0.1", + "::1", + "10.1.2.3", + "2001:0DB8:AC10:FE01::" + ], + "process.executable.caseless": [ + "/opt/elastic/agent/data/elastic-agent-058c40/install/filebeat-8.0.0-snapshot-linux-x86_64/filebeat" + ], + "event.sequence": [ + 44872 + ], + "agent.type": [ + "endpoint" + ], + "process.executable.text": [ + "/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat" + ], + "group.Ext.real.name": [ + "root" + ], + "event.module": [ + "endpoint" + ], + "host.os.kernel": [ + "4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-2 (2021-06-21)" + ], + "host.os.full.caseless": [ + "debian 10" + ], + "host.id": [ + "76ea303129f249aa7382338e4263eac1" + ], + "process.name.caseless": [ + "filebeat" + ], + "network.type": [ + "ipv4" + ], + "process.executable": [ + "/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat" + ], + "user.Ext.real.name": [ + "root" + ], + "data_stream.namespace": [ + "default" + ], + "message": [ + "Endpoint network event" + ], + "destination.ip": [ + "127.0.0.1" + ], + "network.transport": [ + "tcp" + ], + "host.os.Ext.variant": [ + "Debian" + ], + "group.Ext.real.id": [ + "0" + ], + "event.ingested": [ + "2021-10-14T16:46:04.000Z" + ], + "event.action": [ + "connection_attempted" + ], + "@timestamp": [ + "2021-10-14T16:45:58.031Z" + ], + "host.os.platform": [ + "debian" + ], + "data_stream.dataset": [ + "endpoint.events.network" + ], + "event.type": [ + "start" + ], + "event.id": [ + "MKPXftjGeHiQzUNj++++nn6R" + ], + "host.os.name.caseless": [ + "linux" + ], + "event.dataset": [ + "endpoint.events.network" + ], + "user.name.text": [ + "root" + ] } } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx index a8ba536a7554..37ca3b0b897a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import '../../mock/match_media'; import '../../mock/react_beautiful_dnd'; -import { mockDetailItemData, mockDetailItemDataId, TestProviders } from '../../mock'; +import { mockDetailItemData, mockDetailItemDataId, rawEventData, TestProviders } from '../../mock'; import { EventDetails, EventsViewType } from './event_details'; import { mockBrowserFields } from '../../containers/source/mock'; @@ -48,6 +48,7 @@ describe('EventDetails', () => { timelineId: 'test', eventView: EventsViewType.summaryView, hostRisk: { fields: [], loading: true }, + rawEventData, }; const alertsProps = { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index e7092d9d6f46..a8305a635f15 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -61,6 +61,7 @@ interface Props { id: string; isAlert: boolean; isDraggable?: boolean; + rawEventData: object | undefined; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; hostRisk: HostRisk | null; @@ -106,6 +107,7 @@ const EventDetailsComponent: React.FC = ({ id, isAlert, isDraggable, + rawEventData, timelineId, timelineTabType, hostRisk, @@ -278,12 +280,12 @@ const EventDetailsComponent: React.FC = ({ <> - + ), }), - [data] + [rawEventData] ); const tabs = useMemo(() => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx index 696fac601660..b20270266602 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.test.tsx @@ -8,58 +8,15 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { mockDetailItemData } from '../../mock'; +import { rawEventData } from '../../mock'; -import { buildJsonView, JsonView } from './json_view'; +import { JsonView } from './json_view'; describe('JSON View', () => { describe('rendering', () => { test('should match snapshot', () => { - const wrapper = shallow(); + const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); }); - - describe('buildJsonView', () => { - test('should match a json', () => { - const expectedData = { - '@timestamp': '2019-02-28T16:50:54.621Z', - _id: 'pEMaMmkBUV60JmNWmWVi', - _index: 'filebeat-8.0.0-2019.02.19-000001', - _score: 1, - _type: '_doc', - agent: { - ephemeral_id: '9d391ef2-a734-4787-8891-67031178c641', - hostname: 'siem-kibana', - id: '5de03d5f-52f3-482e-91d4-853c7de073c3', - type: 'filebeat', - version: '8.0.0', - }, - cloud: { - availability_zone: 'projects/189716325846/zones/us-east1-b', - instance: { - id: '5412578377715150143', - name: 'siem-kibana', - }, - machine: { - type: 'projects/189716325846/machineTypes/n1-standard-1', - }, - project: { - id: 'elastic-beats', - }, - provider: 'gce', - }, - destination: { - bytes: 584, - ip: '10.47.8.200', - packets: 4, - port: 902, - }, - event: { - kind: 'event', - }, - }; - expect(buildJsonView(mockDetailItemData)).toEqual(expectedData); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx index 0614f131bcd1..0227d44f3230 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/json_view.tsx @@ -6,15 +6,13 @@ */ import { EuiCodeBlock } from '@elastic/eui'; -import { set } from '@elastic/safer-lodash-set/fp'; import React, { useMemo } from 'react'; import styled from 'styled-components'; -import { TimelineEventsDetailsItem } from '../../../../common/search_strategy'; import { omitTypenameAndEmpty } from '../../../timelines/components/timeline/body/helpers'; interface Props { - data: TimelineEventsDetailsItem[]; + rawEventData: object | undefined; } const EuiCodeEditorContainer = styled.div` @@ -23,15 +21,15 @@ const EuiCodeEditorContainer = styled.div` } `; -export const JsonView = React.memo(({ data }) => { +export const JsonView = React.memo(({ rawEventData }) => { const value = useMemo( () => JSON.stringify( - buildJsonView(data), + rawEventData, omitTypenameAndEmpty, 2 // indent level ), - [data] + [rawEventData] ); return ( @@ -50,16 +48,3 @@ export const JsonView = React.memo(({ data }) => { }); JsonView.displayName = 'JsonView'; - -export const buildJsonView = (data: TimelineEventsDetailsItem[]) => - data - .sort((a, b) => a.field.localeCompare(b.field)) - .reduce( - (accumulator, item) => - set( - item.field, - Array.isArray(item.originalValue) ? item.originalValue.join() : item.originalValue, - accumulator - ), - {} - ); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx new file mode 100644 index 000000000000..f6c43da2da8a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.test.tsx @@ -0,0 +1,193 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import React from 'react'; + +import { BrowserField } from '../../../containers/source'; +import { FieldValueCell } from './field_value_cell'; +import { TestProviders } from '../../../mock'; +import { EventFieldsData } from '../types'; + +const contextId = 'test'; + +const eventId = 'TUWyf3wBFCFU0qRJTauW'; + +const hostIpData: EventFieldsData = { + aggregatable: true, + ariaRowindex: 35, + category: 'host', + description: 'Host ip addresses.', + example: '127.0.0.1', + field: 'host.ip', + fields: {}, + format: '', + indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], + isObjectArray: false, + name: 'host.ip', + originalValue: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'], + searchable: true, + type: 'ip', + values: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'], +}; +const hostIpValues = ['127.0.0.1', '::1', '10.1.2.3', 'fe80::4001:aff:fec8:32']; + +describe('FieldValueCell', () => { + describe('common behavior', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it formats multiple values such that each value is displayed on a single line', () => { + expect(screen.getByTestId(`event-field-${hostIpData.field}`)).toHaveClass( + 'euiFlexGroup--directionColumn' + ); + }); + }); + + describe('when `BrowserField` metadata is NOT available', () => { + beforeEach(() => { + render( + + + + ); + }); + + test('it renders each of the expected values when `fieldFromBrowserField` is undefined', () => { + hostIpValues.forEach((value) => { + expect(screen.getByText(value)).toBeInTheDocument(); + }); + }); + + test('it renders values formatted as plain text (without `eventFieldsTable__fieldValue` formatting)', () => { + expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).not.toHaveClass( + 'eventFieldsTable__fieldValue' + ); + }); + }); + + describe('`message` field formatting', () => { + const messageData: EventFieldsData = { + aggregatable: false, + ariaRowindex: 50, + category: 'base', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.', + example: 'Hello World', + field: 'message', + fields: {}, + format: '', + indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], + isObjectArray: false, + name: 'message', + originalValue: ['Endpoint network event'], + searchable: true, + type: 'string', + values: ['Endpoint network event'], + }; + const messageValues = ['Endpoint network event']; + + const messageFieldFromBrowserField: BrowserField = { + aggregatable: false, + category: 'base', + description: + 'For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.', + example: 'Hello World', + fields: {}, + format: '', + indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], + name: 'message', + searchable: true, + type: 'string', + }; + + beforeEach(() => { + render( + + + + ); + }); + + test('it renders special formatting for the `message` field', () => { + expect(screen.getByTestId('event-field-message')).toBeInTheDocument(); + }); + + test('it renders the expected message value', () => { + messageValues.forEach((value) => { + expect(screen.getByText(value)).toBeInTheDocument(); + }); + }); + }); + + describe('when `BrowserField` metadata IS available', () => { + const hostIpFieldFromBrowserField: BrowserField = { + aggregatable: true, + category: 'host', + description: 'Host ip addresses.', + example: '127.0.0.1', + fields: {}, + format: '', + indexes: ['auditbeat-*', 'filebeat-*', 'logs-*', 'winlogbeat-*'], + name: 'host.ip', + searchable: true, + type: 'ip', + }; + + beforeEach(() => { + render( + + + + ); + }); + + test('it renders values formatted with the expected class', () => { + expect(screen.getByTestId(`event-field-${hostIpData.field}`).firstChild).toHaveClass( + 'eventFieldsTable__fieldValue' + ); + }); + + test('it renders link buttons for each of the host ip addresses', () => { + expect(screen.getAllByRole('button').length).toBe(hostIpValues.length); + }); + + test('it renders each of the expected values when `fieldFromBrowserField` is provided', () => { + hostIpValues.forEach((value) => { + expect(screen.getByText(value)).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx index fc20f84d3650..dc6c84b8138f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/table/field_value_cell.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiText } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import { BrowserField } from '../../../containers/source'; import { OverflowField } from '../../tables/helpers'; import { FormattedFieldValue } from '../../../../timelines/components/timeline/body/renderers/formatted_field'; @@ -36,18 +36,28 @@ export const FieldValueCell = React.memo( values, }: FieldValueCellProps) => { return ( -
+ {values != null && values.map((value, i) => { if (fieldFromBrowserField == null) { return ( - - {value} - + + + {value} + + ); } return ( -
+ {data.field === MESSAGE_FIELD_NAME ? ( ) : ( @@ -63,10 +73,10 @@ export const FieldValueCell = React.memo( linkValue={(getLinkValue && getLinkValue(data.field)) ?? linkValue} /> )} -
+ ); })} -
+ ); } ); diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts index 3712d389edeb..035bdbbceff8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_detail_item.ts @@ -139,3 +139,191 @@ export const generateMockDetailItemData = (): TimelineEventsDetailsItem[] => [ ]; export const mockDetailItemData: TimelineEventsDetailsItem[] = generateMockDetailItemData(); + +export const rawEventData = { + _index: '.ds-logs-endpoint.events.network-default-2021.09.28-000001', + _id: 'TUWyf3wBFCFU0qRJTauW', + _score: 1, + _source: { + agent: { + id: '2ac9e9b3-f6d5-4ce6-915d-8f1f8f413624', + type: 'endpoint', + version: '8.0.0-SNAPSHOT', + }, + process: { + Ext: { + ancestry: [ + 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyMzY0LTEzMjc4NjA2NTAyLjA=', + 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTEtMTMyNzA3Njg2OTIuMA==', + ], + }, + name: 'filebeat', + pid: 22535, + entity_id: 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyNTM1LTEzMjc4NjA2NTI4LjA=', + executable: + '/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat', + }, + destination: { + address: '127.0.0.1', + port: 9200, + ip: '127.0.0.1', + }, + source: { + address: '127.0.0.1', + port: 54146, + ip: '127.0.0.1', + }, + message: 'Endpoint network event', + network: { + transport: 'tcp', + type: 'ipv4', + }, + '@timestamp': '2021-10-14T16:45:58.0310772Z', + ecs: { + version: '1.11.0', + }, + data_stream: { + namespace: 'default', + type: 'logs', + dataset: 'endpoint.events.network', + }, + elastic: { + agent: { + id: '12345', + }, + }, + host: { + hostname: 'test-linux-1', + os: { + Ext: { + variant: 'Debian', + }, + kernel: '4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-2 (2021-06-21)', + name: 'Linux', + family: 'debian', + type: 'linux', + version: '10', + platform: 'debian', + full: 'Debian 10', + }, + ip: ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'], + name: 'test-linux-1', + id: '76ea303129f249aa7382338e4263eac1', + mac: ['aa:bb:cc:dd:ee:ff'], + architecture: 'x86_64', + }, + event: { + agent_id_status: 'verified', + sequence: 44872, + ingested: '2021-10-14T16:46:04Z', + created: '2021-10-14T16:45:58.0310772Z', + kind: 'event', + module: 'endpoint', + action: 'connection_attempted', + id: 'MKPXftjGeHiQzUNj++++nn6R', + category: ['network'], + type: ['start'], + dataset: 'endpoint.events.network', + outcome: 'unknown', + }, + user: { + Ext: { + real: { + name: 'root', + id: 0, + }, + }, + name: 'root', + id: 0, + }, + group: { + Ext: { + real: { + name: 'root', + id: 0, + }, + }, + name: 'root', + id: 0, + }, + }, + fields: { + 'host.os.full.text': ['Debian 10'], + 'event.category': ['network'], + 'process.name.text': ['filebeat'], + 'host.os.name.text': ['Linux'], + 'host.os.full': ['Debian 10'], + 'host.hostname': ['test-linux-1'], + 'process.pid': [22535], + 'host.mac': ['42:01:0a:c8:00:32'], + 'elastic.agent.id': ['abcdefg-f6d5-4ce6-915d-8f1f8f413624'], + 'host.os.version': ['10'], + 'host.os.name': ['Linux'], + 'source.ip': ['127.0.0.1'], + 'destination.address': ['127.0.0.1'], + 'host.name': ['test-linux-1'], + 'event.agent_id_status': ['verified'], + 'event.kind': ['event'], + 'event.outcome': ['unknown'], + 'group.name': ['root'], + 'user.id': ['0'], + 'host.os.type': ['linux'], + 'process.Ext.ancestry': [ + 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyMzY0LTEzMjc4NjA2NTAyLjA=', + 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTEtMTMyNzA3Njg2OTIuMA==', + ], + 'user.Ext.real.id': ['0'], + 'data_stream.type': ['logs'], + 'host.architecture': ['x86_64'], + 'process.name': ['filebeat'], + 'agent.id': ['2ac9e9b3-f6d5-4ce6-915d-8f1f8f413624'], + 'source.port': [54146], + 'ecs.version': ['1.11.0'], + 'event.created': ['2021-10-14T16:45:58.031Z'], + 'agent.version': ['8.0.0-SNAPSHOT'], + 'host.os.family': ['debian'], + 'destination.port': [9200], + 'group.id': ['0'], + 'user.name': ['root'], + 'source.address': ['127.0.0.1'], + 'process.entity_id': [ + 'MmFjOWU5YjMtZjZkNS00Y2U2LTkxNWQtOGYxZjhmNDEzNjI0LTIyNTM1LTEzMjc4NjA2NTI4LjA=', + ], + 'host.ip': ['127.0.0.1', '::1', '10.1.2.3', '2001:0DB8:AC10:FE01::'], + 'process.executable.caseless': [ + '/opt/elastic/agent/data/elastic-agent-058c40/install/filebeat-8.0.0-snapshot-linux-x86_64/filebeat', + ], + 'event.sequence': [44872], + 'agent.type': ['endpoint'], + 'process.executable.text': [ + '/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat', + ], + 'group.Ext.real.name': ['root'], + 'event.module': ['endpoint'], + 'host.os.kernel': ['4.19.0-17-cloud-amd64 #1 SMP Debian 4.19.194-2 (2021-06-21)'], + 'host.os.full.caseless': ['debian 10'], + 'host.id': ['76ea303129f249aa7382338e4263eac1'], + 'process.name.caseless': ['filebeat'], + 'network.type': ['ipv4'], + 'process.executable': [ + '/opt/Elastic/Agent/data/elastic-agent-058c40/install/filebeat-8.0.0-SNAPSHOT-linux-x86_64/filebeat', + ], + 'user.Ext.real.name': ['root'], + 'data_stream.namespace': ['default'], + message: ['Endpoint network event'], + 'destination.ip': ['127.0.0.1'], + 'network.transport': ['tcp'], + 'host.os.Ext.variant': ['Debian'], + 'group.Ext.real.id': ['0'], + 'event.ingested': ['2021-10-14T16:46:04.000Z'], + 'event.action': ['connection_attempted'], + '@timestamp': ['2021-10-14T16:45:58.031Z'], + 'host.os.platform': ['debian'], + 'data_stream.dataset': ['endpoint.events.network'], + 'event.type': ['start'], + 'event.id': ['MKPXftjGeHiQzUNj++++nn6R'], + 'host.os.name.caseless': ['linux'], + 'event.dataset': ['endpoint.events.network'], + 'user.name.text': ['root'], + }, +}; diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 17d43d80a5a9..6a7f0602c367 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -33,6 +33,7 @@ interface Props { isDraggable?: boolean; loading: boolean; messageHeight?: number; + rawEventData: object | undefined; timelineTabType: TimelineTabs | 'flyout'; timelineId: string; hostRisk: HostRisk | null; @@ -93,6 +94,7 @@ export const ExpandableEvent = React.memo( loading, detailsData, hostRisk, + rawEventData, }) => { if (!event.eventId) { return {i18n.EVENT_DETAILS_PLACEHOLDER}; @@ -111,6 +113,7 @@ export const ExpandableEvent = React.memo( id={event.eventId} isAlert={isAlert} isDraggable={isDraggable} + rawEventData={rawEventData} timelineId={timelineId} timelineTabType={timelineTabType} hostRisk={hostRisk} diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index f8786e070683..b9d7e0a8c024 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -79,7 +79,7 @@ const EventDetailsPanelComponent: React.FC = ({ tabType, timelineId, }) => { - const [loading, detailsData] = useTimelineEventsDetails({ + const [loading, detailsData, rawEventData] = useTimelineEventsDetails({ docValueFields, entityType, indexName: expandedEvent.indexName ?? '', @@ -195,6 +195,7 @@ const EventDetailsPanelComponent: React.FC = ({ isAlert={isAlert} isDraggable={isDraggable} loading={loading} + rawEventData={rawEventData} timelineId={timelineId} timelineTabType="flyout" hostRisk={hostRisk} @@ -228,6 +229,7 @@ const EventDetailsPanelComponent: React.FC = ({ isAlert={isAlert} isDraggable={isDraggable} loading={loading} + rawEventData={rawEventData} timelineId={timelineId} timelineTabType={tabType} hostRisk={hostRisk} diff --git a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx index e59eaeed4f2a..f05966bd9787 100644 --- a/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/containers/details/index.tsx @@ -42,7 +42,7 @@ export const useTimelineEventsDetails = ({ indexName, eventId, skip, -}: UseTimelineEventsDetailsProps): [boolean, EventsArgs['detailsData']] => { +}: UseTimelineEventsDetailsProps): [boolean, EventsArgs['detailsData'], object | undefined] => { const { data } = useKibana().services; const refetch = useRef(noop); const abortCtrl = useRef(new AbortController()); @@ -55,6 +55,8 @@ export const useTimelineEventsDetails = ({ const [timelineDetailsResponse, setTimelineDetailsResponse] = useState(null); + const [rawEventData, setRawEventData] = useState(undefined); + const timelineDetailsSearch = useCallback( (request: TimelineEventsDetailsRequestOptions | null) => { if (request == null || skip || isEmpty(request.eventId)) { @@ -78,6 +80,7 @@ export const useTimelineEventsDetails = ({ if (isCompleteResponse(response)) { setLoading(false); setTimelineDetailsResponse(response.data || []); + setRawEventData(response.rawResponse.hits.hits[0]); searchSubscription$.current.unsubscribe(); } else if (isErrorResponse(response)) { setLoading(false); @@ -125,5 +128,5 @@ export const useTimelineEventsDetails = ({ }; }, [timelineDetailsRequest, timelineDetailsSearch]); - return [loading, timelineDetailsResponse]; + return [loading, timelineDetailsResponse, rawEventData]; }; diff --git a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts index 5bceb3108168..f9f6a2ea5791 100644 --- a/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts +++ b/x-pack/plugins/timelines/common/search_strategy/timeline/events/details/index.ts @@ -24,6 +24,7 @@ export interface TimelineEventsDetailsItem { export interface TimelineEventsDetailsStrategyResponse extends IEsSearchResponse { data?: Maybe; inspect?: Maybe; + rawEventData?: Maybe; } export interface TimelineEventsDetailsRequestOptions diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts index c82d9af938a9..b60add2515ec 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/details/index.ts @@ -57,10 +57,14 @@ export const timelineEventsDetails: TimelineFactory Date: Tue, 19 Oct 2021 12:14:57 +0200 Subject: [PATCH 33/53] Allow elastic/fleet-server to call appropriate Fleet APIs (#113932) --- x-pack/plugins/fleet/server/mocks/index.ts | 12 +- x-pack/plugins/fleet/server/plugin.ts | 55 ++++-- .../fleet/server/routes/agent_policy/index.ts | 27 +-- .../routes/enrollment_api_key/handler.ts | 6 +- .../server/routes/enrollment_api_key/index.ts | 21 ++- .../plugins/fleet/server/routes/epm/index.ts | 39 ++-- .../fleet/server/routes/security.test.ts | 175 ++++++++++++++++++ .../plugins/fleet/server/routes/security.ts | 135 +++++++++++--- .../fleet/server/routes/setup/handlers.ts | 8 +- .../fleet/server/routes/setup/index.ts | 21 +-- .../fleet/server/types/request_context.ts | 7 + .../authorization/check_privileges.test.ts | 108 +++++++++++ .../server/authorization/check_privileges.ts | 31 +++- .../check_privileges_dynamically.test.ts | 22 ++- .../check_privileges_dynamically.ts | 14 +- .../security/server/authorization/types.ts | 26 ++- .../apis/security/privileges.ts | 2 +- .../apis/security/privileges_basic.ts | 2 +- .../apis/agents/services.ts | 26 ++- .../fleet_api_integration/apis/epm/setup.ts | 45 +++++ 20 files changed, 651 insertions(+), 131 deletions(-) create mode 100644 x-pack/plugins/fleet/server/routes/security.test.ts diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index c7f6b6fefc41..e6577426974a 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -23,7 +23,17 @@ import type { FleetAppContext } from '../plugin'; // Export all mocks from artifacts export * from '../services/artifacts/mocks'; -export const createAppContextStartContractMock = (): FleetAppContext => { +export interface MockedFleetAppContext extends FleetAppContext { + elasticsearch: ReturnType; + data: ReturnType; + encryptedSavedObjectsStart?: ReturnType; + savedObjects: ReturnType; + securitySetup?: ReturnType; + securityStart?: ReturnType; + logger: ReturnType['get']>; +} + +export const createAppContextStartContractMock = (): MockedFleetAppContext => { const config = { agents: { enabled: true, elasticsearch: {} }, enabled: true, diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index aaee24b39685..8a95065380b6 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -80,9 +80,10 @@ import { } from './services/agents'; import { registerFleetUsageCollector } from './collectors/register'; import { getInstallation, ensureInstalledPackage } from './services/epm/packages'; -import { makeRouterEnforcingSuperuser } from './routes/security'; +import { RouterWrappers } from './routes/security'; import { startFleetServerSetup } from './services/fleet_server'; import { FleetArtifactsClient } from './services/artifacts'; +import type { FleetRouter } from './types/request_context'; export interface FleetSetupDeps { licensing: LicensingPluginSetup; @@ -206,6 +207,24 @@ export class FleetPlugin category: DEFAULT_APP_CATEGORIES.management, app: [PLUGIN_ID, INTEGRATIONS_PLUGIN_ID, 'kibana'], catalogue: ['fleet'], + reserved: { + description: + 'Privilege to setup Fleet packages and configured policies. Intended for use by the elastic/fleet-server service account only.', + privileges: [ + { + id: 'fleet-setup', + privilege: { + excludeFromBasePrivileges: true, + api: ['fleet-setup'], + savedObject: { + all: [], + read: [], + }, + ui: [], + }, + }, + ], + }, privileges: { all: { api: [`${PLUGIN_ID}-read`, `${PLUGIN_ID}-all`], @@ -245,7 +264,7 @@ export class FleetPlugin }) ); - const router = core.http.createRouter(); + const router: FleetRouter = core.http.createRouter(); // Register usage collection registerFleetUsageCollector(core, config, deps.usageCollection); @@ -254,24 +273,34 @@ export class FleetPlugin registerAppRoutes(router); // Allow read-only users access to endpoints necessary for Integrations UI // Only some endpoints require superuser so we pass a raw IRouter here - registerEPMRoutes(router); // For all the routes we enforce the user to have role superuser - const routerSuperuserOnly = makeRouterEnforcingSuperuser(router); + const superuserRouter = RouterWrappers.require.superuser(router); + const fleetSetupRouter = RouterWrappers.require.fleetSetupPrivilege(router); + + // Some EPM routes use regular rbac to support integrations app + registerEPMRoutes({ rbac: router, superuser: superuserRouter }); + // Register rest of routes only if security is enabled if (deps.security) { - registerSetupRoutes(routerSuperuserOnly, config); - registerAgentPolicyRoutes(routerSuperuserOnly); - registerPackagePolicyRoutes(routerSuperuserOnly); - registerOutputRoutes(routerSuperuserOnly); - registerSettingsRoutes(routerSuperuserOnly); - registerDataStreamRoutes(routerSuperuserOnly); - registerPreconfigurationRoutes(routerSuperuserOnly); + registerSetupRoutes(fleetSetupRouter, config); + registerAgentPolicyRoutes({ + fleetSetup: fleetSetupRouter, + superuser: superuserRouter, + }); + registerPackagePolicyRoutes(superuserRouter); + registerOutputRoutes(superuserRouter); + registerSettingsRoutes(superuserRouter); + registerDataStreamRoutes(superuserRouter); + registerPreconfigurationRoutes(superuserRouter); // Conditional config routes if (config.agents.enabled) { - registerAgentAPIRoutes(routerSuperuserOnly, config); - registerEnrollmentApiKeyRoutes(routerSuperuserOnly); + registerAgentAPIRoutes(superuserRouter, config); + registerEnrollmentApiKeyRoutes({ + fleetSetup: fleetSetupRouter, + superuser: superuserRouter, + }); } } } diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts index a66a9ab9cadc..4c20358e1508 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/index.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { IRouter } from 'src/core/server'; - import { PLUGIN_ID, AGENT_POLICY_API_ROUTES } from '../../constants'; import { GetAgentPoliciesRequestSchema, @@ -17,6 +15,7 @@ import { DeleteAgentPolicyRequestSchema, GetFullAgentPolicyRequestSchema, } from '../../types'; +import type { FleetRouter } from '../../types/request_context'; import { getAgentPoliciesHandler, @@ -29,19 +28,21 @@ import { downloadFullAgentPolicy, } from './handlers'; -export const registerRoutes = (router: IRouter) => { - // List - router.get( +export const registerRoutes = (routers: { superuser: FleetRouter; fleetSetup: FleetRouter }) => { + // List - Fleet Server needs access to run setup + routers.fleetSetup.get( { path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, validate: GetAgentPoliciesRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. + // options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getAgentPoliciesHandler ); // Get one - router.get( + routers.superuser.get( { path: AGENT_POLICY_API_ROUTES.INFO_PATTERN, validate: GetOneAgentPolicyRequestSchema, @@ -51,7 +52,7 @@ export const registerRoutes = (router: IRouter) => { ); // Create - router.post( + routers.superuser.post( { path: AGENT_POLICY_API_ROUTES.CREATE_PATTERN, validate: CreateAgentPolicyRequestSchema, @@ -61,7 +62,7 @@ export const registerRoutes = (router: IRouter) => { ); // Update - router.put( + routers.superuser.put( { path: AGENT_POLICY_API_ROUTES.UPDATE_PATTERN, validate: UpdateAgentPolicyRequestSchema, @@ -71,7 +72,7 @@ export const registerRoutes = (router: IRouter) => { ); // Copy - router.post( + routers.superuser.post( { path: AGENT_POLICY_API_ROUTES.COPY_PATTERN, validate: CopyAgentPolicyRequestSchema, @@ -81,7 +82,7 @@ export const registerRoutes = (router: IRouter) => { ); // Delete - router.post( + routers.superuser.post( { path: AGENT_POLICY_API_ROUTES.DELETE_PATTERN, validate: DeleteAgentPolicyRequestSchema, @@ -91,7 +92,7 @@ export const registerRoutes = (router: IRouter) => { ); // Get one full agent policy - router.get( + routers.superuser.get( { path: AGENT_POLICY_API_ROUTES.FULL_INFO_PATTERN, validate: GetFullAgentPolicyRequestSchema, @@ -101,7 +102,7 @@ export const registerRoutes = (router: IRouter) => { ); // Download one full agent policy - router.get( + routers.superuser.get( { path: AGENT_POLICY_API_ROUTES.FULL_INFO_DOWNLOAD_PATTERN, validate: GetFullAgentPolicyRequestSchema, diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts index 0959a9a88704..9cb07a9050f8 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/handler.ts @@ -27,7 +27,8 @@ export const getEnrollmentApiKeysHandler: RequestHandler< undefined, TypeOf > = async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + // Use kibana_system and depend on authz checks on HTTP layer to prevent abuse + const esClient = context.core.elasticsearch.client.asInternalUser; try { const { items, total, page, perPage } = await APIKeyService.listEnrollmentApiKeys(esClient, { @@ -87,7 +88,8 @@ export const deleteEnrollmentApiKeyHandler: RequestHandler< export const getOneEnrollmentApiKeyHandler: RequestHandler< TypeOf > = async (context, request, response) => { - const esClient = context.core.elasticsearch.client.asCurrentUser; + // Use kibana_system and depend on authz checks on HTTP layer to prevent abuse + const esClient = context.core.elasticsearch.client.asInternalUser; try { const apiKey = await APIKeyService.getEnrollmentAPIKey(esClient, request.params.keyId); const body: GetOneEnrollmentAPIKeyResponse = { item: apiKey }; diff --git a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts index b37a88e70e08..6429d4d29d5c 100644 --- a/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts +++ b/x-pack/plugins/fleet/server/routes/enrollment_api_key/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { IRouter } from 'src/core/server'; - import { PLUGIN_ID, ENROLLMENT_API_KEY_ROUTES } from '../../constants'; import { GetEnrollmentAPIKeysRequestSchema, @@ -14,6 +12,7 @@ import { DeleteEnrollmentAPIKeyRequestSchema, PostEnrollmentAPIKeyRequestSchema, } from '../../types'; +import type { FleetRouter } from '../../types/request_context'; import { getEnrollmentApiKeysHandler, @@ -22,17 +21,19 @@ import { postEnrollmentApiKeyHandler, } from './handler'; -export const registerRoutes = (router: IRouter) => { - router.get( +export const registerRoutes = (routers: { superuser: FleetRouter; fleetSetup: FleetRouter }) => { + routers.fleetSetup.get( { path: ENROLLMENT_API_KEY_ROUTES.INFO_PATTERN, validate: GetOneEnrollmentAPIKeyRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. + // options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getOneEnrollmentApiKeyHandler ); - router.delete( + routers.superuser.delete( { path: ENROLLMENT_API_KEY_ROUTES.DELETE_PATTERN, validate: DeleteEnrollmentAPIKeyRequestSchema, @@ -41,16 +42,18 @@ export const registerRoutes = (router: IRouter) => { deleteEnrollmentApiKeyHandler ); - router.get( + routers.fleetSetup.get( { path: ENROLLMENT_API_KEY_ROUTES.LIST_PATTERN, validate: GetEnrollmentAPIKeysRequestSchema, - options: { tags: [`access:${PLUGIN_ID}-read`] }, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. + // options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getEnrollmentApiKeysHandler ); - router.post( + routers.superuser.post( { path: ENROLLMENT_API_KEY_ROUTES.CREATE_PATTERN, validate: PostEnrollmentAPIKeyRequestSchema, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 360f2ec1d446..a2f2df4a00c5 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -5,10 +5,7 @@ * 2.0. */ -import type { IRouter } from 'src/core/server'; - import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; -import type { FleetRequestHandlerContext } from '../../types'; import { GetCategoriesRequestSchema, GetPackagesRequestSchema, @@ -21,7 +18,7 @@ import { GetStatsRequestSchema, UpdatePackageRequestSchema, } from '../../types'; -import { enforceSuperUser } from '../security'; +import type { FleetRouter } from '../../types/request_context'; import { getCategoriesHandler, @@ -39,8 +36,8 @@ import { const MAX_FILE_SIZE_BYTES = 104857600; // 100MB -export const registerRoutes = (router: IRouter) => { - router.get( +export const registerRoutes = (routers: { rbac: FleetRouter; superuser: FleetRouter }) => { + routers.rbac.get( { path: EPM_API_ROUTES.CATEGORIES_PATTERN, validate: GetCategoriesRequestSchema, @@ -49,7 +46,7 @@ export const registerRoutes = (router: IRouter) => { getCategoriesHandler ); - router.get( + routers.rbac.get( { path: EPM_API_ROUTES.LIST_PATTERN, validate: GetPackagesRequestSchema, @@ -58,7 +55,7 @@ export const registerRoutes = (router: IRouter) => { getListHandler ); - router.get( + routers.rbac.get( { path: EPM_API_ROUTES.LIMITED_LIST_PATTERN, validate: false, @@ -67,7 +64,7 @@ export const registerRoutes = (router: IRouter) => { getLimitedListHandler ); - router.get( + routers.rbac.get( { path: EPM_API_ROUTES.STATS_PATTERN, validate: GetStatsRequestSchema, @@ -76,7 +73,7 @@ export const registerRoutes = (router: IRouter) => { getStatsHandler ); - router.get( + routers.rbac.get( { path: EPM_API_ROUTES.FILEPATH_PATTERN, validate: GetFileRequestSchema, @@ -85,7 +82,7 @@ export const registerRoutes = (router: IRouter) => { getFileHandler ); - router.get( + routers.rbac.get( { path: EPM_API_ROUTES.INFO_PATTERN, validate: GetInfoRequestSchema, @@ -94,34 +91,34 @@ export const registerRoutes = (router: IRouter) => { getInfoHandler ); - router.put( + routers.superuser.put( { path: EPM_API_ROUTES.INFO_PATTERN, validate: UpdatePackageRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - enforceSuperUser(updatePackageHandler) + updatePackageHandler ); - router.post( + routers.superuser.post( { path: EPM_API_ROUTES.INSTALL_FROM_REGISTRY_PATTERN, validate: InstallPackageFromRegistryRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - enforceSuperUser(installPackageFromRegistryHandler) + installPackageFromRegistryHandler ); - router.post( + routers.superuser.post( { path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, validate: BulkUpgradePackagesFromRegistryRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - enforceSuperUser(bulkInstallPackagesFromRegistryHandler) + bulkInstallPackagesFromRegistryHandler ); - router.post( + routers.superuser.post( { path: EPM_API_ROUTES.INSTALL_BY_UPLOAD_PATTERN, validate: InstallPackageByUploadRequestSchema, @@ -134,15 +131,15 @@ export const registerRoutes = (router: IRouter) => { }, }, }, - enforceSuperUser(installPackageByUploadHandler) + installPackageByUploadHandler ); - router.delete( + routers.superuser.delete( { path: EPM_API_ROUTES.DELETE_PATTERN, validate: DeletePackageRequestSchema, options: { tags: [`access:${PLUGIN_ID}-all`] }, }, - enforceSuperUser(deletePackageHandler) + deletePackageHandler ); }; diff --git a/x-pack/plugins/fleet/server/routes/security.test.ts b/x-pack/plugins/fleet/server/routes/security.test.ts new file mode 100644 index 000000000000..80ea14254153 --- /dev/null +++ b/x-pack/plugins/fleet/server/routes/security.test.ts @@ -0,0 +1,175 @@ +/* + * 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 { IRouter, RequestHandler, RouteConfig } from '../../../../../src/core/server'; +import { coreMock } from '../../../../../src/core/server/mocks'; +import type { AuthenticatedUser } from '../../../security/server'; +import type { CheckPrivilegesDynamically } from '../../../security/server/authorization/check_privileges_dynamically'; +import { createAppContextStartContractMock } from '../mocks'; +import { appContextService } from '../services'; + +import type { RouterWrapper } from './security'; +import { RouterWrappers } from './security'; + +describe('RouterWrappers', () => { + const runTest = async ({ + wrapper, + security: { + roles = [], + pluginEnabled = true, + licenseEnabled = true, + checkPrivilegesDynamically, + } = {}, + }: { + wrapper: RouterWrapper; + security?: { + roles?: string[]; + pluginEnabled?: boolean; + licenseEnabled?: boolean; + checkPrivilegesDynamically?: CheckPrivilegesDynamically; + }; + }) => { + const fakeRouter = { + get: jest.fn(), + } as unknown as jest.Mocked; + const fakeHandler: RequestHandler = jest.fn((ctx, req, res) => res.ok()); + + const mockContext = createAppContextStartContractMock(); + // @ts-expect-error type doesn't properly respect deeply mocked keys + mockContext.securityStart?.authz.actions.api.get.mockImplementation((priv) => `api:${priv}`); + + if (!pluginEnabled) { + mockContext.securitySetup = undefined; + mockContext.securityStart = undefined; + } else { + mockContext.securityStart?.authc.getCurrentUser.mockReturnValue({ + username: 'foo', + roles, + } as unknown as AuthenticatedUser); + + mockContext.securitySetup?.license.isEnabled.mockReturnValue(licenseEnabled); + if (licenseEnabled) { + mockContext.securityStart?.authz.mode.useRbacForRequest.mockReturnValue(true); + } + + if (checkPrivilegesDynamically) { + mockContext.securityStart?.authz.checkPrivilegesDynamicallyWithRequest.mockReturnValue( + checkPrivilegesDynamically + ); + } + } + + appContextService.start(mockContext); + + const wrappedRouter = wrapper(fakeRouter); + wrappedRouter.get({} as RouteConfig, fakeHandler); + const wrappedHandler = fakeRouter.get.mock.calls[0][1]; + const resFactory = { forbidden: jest.fn(() => 'forbidden'), ok: jest.fn(() => 'ok') }; + const res = await wrappedHandler( + { core: coreMock.createRequestHandlerContext() }, + {} as any, + resFactory as any + ); + + return res as unknown as 'forbidden' | 'ok'; + }; + + describe('require.superuser', () => { + it('allow users with the superuser role', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.superuser, + security: { roles: ['superuser'] }, + }) + ).toEqual('ok'); + }); + + it('does not allow users without the superuser role', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.superuser, + security: { roles: ['foo'] }, + }) + ).toEqual('forbidden'); + }); + + it('does not allow security plugin to be disabled', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.superuser, + security: { pluginEnabled: false }, + }) + ).toEqual('forbidden'); + }); + + it('does not allow security license to be disabled', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.superuser, + security: { licenseEnabled: false }, + }) + ).toEqual('forbidden'); + }); + }); + + describe('require.fleetSetupPrivilege', () => { + const mockCheckPrivileges: jest.Mock< + ReturnType, + Parameters + > = jest.fn().mockResolvedValue({ hasAllRequested: true }); + + it('executes custom authz check', async () => { + await runTest({ + wrapper: RouterWrappers.require.fleetSetupPrivilege, + security: { checkPrivilegesDynamically: mockCheckPrivileges }, + }); + expect(mockCheckPrivileges).toHaveBeenCalledWith( + { kibana: ['api:fleet-setup'] }, + { + requireLoginAction: false, + } + ); + }); + + it('allow users with required privileges', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.fleetSetupPrivilege, + security: { checkPrivilegesDynamically: mockCheckPrivileges }, + }) + ).toEqual('ok'); + }); + + it('does not allow users without required privileges', async () => { + mockCheckPrivileges.mockResolvedValueOnce({ hasAllRequested: false } as any); + expect( + await runTest({ + wrapper: RouterWrappers.require.fleetSetupPrivilege, + security: { checkPrivilegesDynamically: mockCheckPrivileges }, + }) + ).toEqual('forbidden'); + }); + + it('does not allow security plugin to be disabled', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.fleetSetupPrivilege, + security: { pluginEnabled: false }, + }) + ).toEqual('forbidden'); + }); + + it('does not allow security license to be disabled', async () => { + expect( + await runTest({ + wrapper: RouterWrappers.require.fleetSetupPrivilege, + security: { licenseEnabled: false }, + }) + ).toEqual('forbidden'); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 33a510c27f04..8a67a7066742 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -5,56 +5,137 @@ * 2.0. */ -import type { IRouter, RequestHandler, RequestHandlerContext } from 'src/core/server'; +import type { + IRouter, + KibanaRequest, + RequestHandler, + RequestHandlerContext, +} from 'src/core/server'; import { appContextService } from '../services'; -export function enforceSuperUser( +const SUPERUSER_AUTHZ_MESSAGE = + 'Access to Fleet API requires the superuser role and for stack security features to be enabled.'; + +function checkSecurityEnabled() { + return appContextService.hasSecurity() && appContextService.getSecurityLicense().isEnabled(); +} + +function checkSuperuser(req: KibanaRequest) { + if (!checkSecurityEnabled()) { + return false; + } + + const security = appContextService.getSecurity(); + const user = security.authc.getCurrentUser(req); + if (!user) { + return false; + } + + const userRoles = user.roles || []; + if (!userRoles.includes('superuser')) { + return false; + } + + return true; +} + +function enforceSuperuser( handler: RequestHandler ): RequestHandler { return function enforceSuperHandler(context, req, res) { - if (!appContextService.hasSecurity() || !appContextService.getSecurityLicense().isEnabled()) { + const isSuperuser = checkSuperuser(req); + if (!isSuperuser) { return res.forbidden({ body: { - message: `Access to this API requires that security is enabled`, + message: SUPERUSER_AUTHZ_MESSAGE, }, }); } - const security = appContextService.getSecurity(); - const user = security.authc.getCurrentUser(req); - if (!user) { - return res.forbidden({ - body: { - message: - 'Access to Fleet API require the superuser role, and for stack security features to be enabled.', - }, - }); - } + return handler(context, req, res); + }; +} - const userRoles = user.roles || []; - if (!userRoles.includes('superuser')) { - return res.forbidden({ - body: { - message: 'Access to Fleet API require the superuser role.', - }, - }); +function makeRouterEnforcingSuperuser( + router: IRouter +): IRouter { + return { + get: (options, handler) => router.get(options, enforceSuperuser(handler)), + delete: (options, handler) => router.delete(options, enforceSuperuser(handler)), + post: (options, handler) => router.post(options, enforceSuperuser(handler)), + put: (options, handler) => router.put(options, enforceSuperuser(handler)), + patch: (options, handler) => router.patch(options, enforceSuperuser(handler)), + handleLegacyErrors: (handler) => router.handleLegacyErrors(handler), + getRoutes: () => router.getRoutes(), + routerPath: router.routerPath, + }; +} + +async function checkFleetSetupPrivilege(req: KibanaRequest) { + if (!checkSecurityEnabled()) { + return false; + } + + const security = appContextService.getSecurity(); + + if (security.authz.mode.useRbacForRequest(req)) { + const checkPrivileges = security.authz.checkPrivilegesDynamicallyWithRequest(req); + const { hasAllRequested } = await checkPrivileges( + { kibana: [security.authz.actions.api.get('fleet-setup')] }, + { requireLoginAction: false } // exclude login access requirement + ); + + return !!hasAllRequested; + } + + return true; +} + +function enforceFleetSetupPrivilege( + handler: RequestHandler +): RequestHandler { + return async (context, req, res) => { + const hasFleetSetupPrivilege = await checkFleetSetupPrivilege(req); + if (!hasFleetSetupPrivilege) { + return res.forbidden({ body: { message: SUPERUSER_AUTHZ_MESSAGE } }); } + return handler(context, req, res); }; } -export function makeRouterEnforcingSuperuser( +function makeRouterEnforcingFleetSetupPrivilege( router: IRouter ): IRouter { return { - get: (options, handler) => router.get(options, enforceSuperUser(handler)), - delete: (options, handler) => router.delete(options, enforceSuperUser(handler)), - post: (options, handler) => router.post(options, enforceSuperUser(handler)), - put: (options, handler) => router.put(options, enforceSuperUser(handler)), - patch: (options, handler) => router.patch(options, enforceSuperUser(handler)), + get: (options, handler) => router.get(options, enforceFleetSetupPrivilege(handler)), + delete: (options, handler) => router.delete(options, enforceFleetSetupPrivilege(handler)), + post: (options, handler) => router.post(options, enforceFleetSetupPrivilege(handler)), + put: (options, handler) => router.put(options, enforceFleetSetupPrivilege(handler)), + patch: (options, handler) => router.patch(options, enforceFleetSetupPrivilege(handler)), handleLegacyErrors: (handler) => router.handleLegacyErrors(handler), getRoutes: () => router.getRoutes(), routerPath: router.routerPath, }; } + +export type RouterWrapper = (route: IRouter) => IRouter; + +interface RouterWrappersSetup { + require: { + superuser: RouterWrapper; + fleetSetupPrivilege: RouterWrapper; + }; +} + +export const RouterWrappers: RouterWrappersSetup = { + require: { + superuser: (router) => { + return makeRouterEnforcingSuperuser(router); + }, + fleetSetupPrivilege: (router) => { + return makeRouterEnforcingFleetSetupPrivilege(router); + }, + }, +}; diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index c5b2ef0ade26..fad5d93c3f5d 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -5,8 +5,6 @@ * 2.0. */ -import type { RequestHandler } from 'src/core/server'; - import { appContextService } from '../../services'; import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../common'; import { setupFleet } from '../../services/setup'; @@ -14,12 +12,14 @@ import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; import type { FleetRequestHandler } from '../../types'; -export const getFleetStatusHandler: RequestHandler = async (context, request, response) => { +export const getFleetStatusHandler: FleetRequestHandler = async (context, request, response) => { try { const isApiKeysEnabled = await appContextService .getSecurity() .authc.apiKeys.areAPIKeysEnabled(); - const isFleetServerSetup = await hasFleetServers(appContextService.getInternalUserESClient()); + const isFleetServerSetup = await hasFleetServers( + context.core.elasticsearch.client.asInternalUser + ); const missingRequirements: GetFleetStatusResponse['missing_requirements'] = []; if (!isApiKeysEnabled) { diff --git a/x-pack/plugins/fleet/server/routes/setup/index.ts b/x-pack/plugins/fleet/server/routes/setup/index.ts index 591b9c832172..d191f1b78e9a 100644 --- a/x-pack/plugins/fleet/server/routes/setup/index.ts +++ b/x-pack/plugins/fleet/server/routes/setup/index.ts @@ -5,55 +5,48 @@ * 2.0. */ -import type { IRouter } from 'src/core/server'; - import { PLUGIN_ID, AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import type { FleetConfigType } from '../../../common'; -import type { FleetRequestHandlerContext } from '../../types/request_context'; +import type { FleetRouter } from '../../types/request_context'; import { getFleetStatusHandler, fleetSetupHandler } from './handlers'; -export const registerFleetSetupRoute = (router: IRouter) => { +export const registerFleetSetupRoute = (router: FleetRouter) => { router.post( { path: SETUP_API_ROUTE, validate: false, - // if this route is set to `-all`, a read-only user get a 404 for this route - // and will see `Unable to initialize Ingest Manager` in the UI - options: { tags: [`access:${PLUGIN_ID}-read`] }, }, fleetSetupHandler ); }; // That route is used by agent to setup Fleet -export const registerCreateFleetSetupRoute = (router: IRouter) => { +export const registerCreateFleetSetupRoute = (router: FleetRouter) => { router.post( { path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN, validate: false, - options: { tags: [`access:${PLUGIN_ID}-all`] }, }, fleetSetupHandler ); }; -export const registerGetFleetStatusRoute = (router: IRouter) => { +export const registerGetFleetStatusRoute = (router: FleetRouter) => { router.get( { path: AGENTS_SETUP_API_ROUTES.INFO_PATTERN, validate: false, + // Disable this tag and the automatic RBAC support until elastic/fleet-server access is removed in 8.0 + // Required to allow elastic/fleet-server to access this API. options: { tags: [`access:${PLUGIN_ID}-read`] }, }, getFleetStatusHandler ); }; -export const registerRoutes = ( - router: IRouter, - config: FleetConfigType -) => { +export const registerRoutes = (router: FleetRouter, config: FleetConfigType) => { // Ingest manager setup registerFleetSetupRoute(router); diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts index a3b414119b68..0d0da9145f07 100644 --- a/x-pack/plugins/fleet/server/types/request_context.ts +++ b/x-pack/plugins/fleet/server/types/request_context.ts @@ -11,6 +11,7 @@ import type { RequestHandlerContext, RouteMethod, SavedObjectsClientContract, + IRouter, } from '../../../../../src/core/server'; /** @internal */ @@ -37,3 +38,9 @@ export type FleetRequestHandler< Method extends RouteMethod = any, ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory > = RequestHandler; + +/** + * Convenience type for routers in Fleet that includes the FleetRequestHandlerContext type + * @internal + */ +export type FleetRouter = IRouter; diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index 75c8229bb37d..d8906d91f152 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -878,6 +878,42 @@ describe('#atSpace', () => { `); }); }); + + test('omits login privilege when requireLoginAction: false', async () => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({ + has_all_requested: true, + username: 'foo-username', + index: {}, + application: { + [application]: { + 'space:space_1': { + [mockActions.version]: true, + }, + }, + }, + }); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + () => Promise.resolve(mockClusterClient), + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + await checkPrivileges.atSpace('space_1', {}, { requireLoginAction: false }); + + expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ + body: { + index: [], + application: [ + { + application, + resources: [`space:space_1`], + privileges: [mockActions.version], + }, + ], + }, + }); + }); }); describe('#atSpaces', () => { @@ -2083,6 +2119,42 @@ describe('#atSpaces', () => { `); }); }); + + test('omits login privilege when requireLoginAction: false', async () => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({ + has_all_requested: true, + username: 'foo-username', + index: {}, + application: { + [application]: { + 'space:space_1': { + [mockActions.version]: true, + }, + }, + }, + }); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + () => Promise.resolve(mockClusterClient), + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + await checkPrivileges.atSpaces(['space_1'], {}, { requireLoginAction: false }); + + expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ + body: { + index: [], + application: [ + { + application, + resources: [`space:space_1`], + privileges: [mockActions.version], + }, + ], + }, + }); + }); }); describe('#globally', () => { @@ -2937,4 +3009,40 @@ describe('#globally', () => { `); }); }); + + test('omits login privilege when requireLoginAction: false', async () => { + const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient({ + has_all_requested: true, + username: 'foo-username', + index: {}, + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.version]: true, + }, + }, + }, + }); + const checkPrivilegesWithRequest = checkPrivilegesWithRequestFactory( + mockActions, + () => Promise.resolve(mockClusterClient), + application + ); + const request = httpServerMock.createKibanaRequest(); + const checkPrivileges = checkPrivilegesWithRequest(request); + await checkPrivileges.globally({}, { requireLoginAction: false }); + + expect(mockScopedClusterClient.asCurrentUser.security.hasPrivileges).toHaveBeenCalledWith({ + body: { + index: [], + application: [ + { + application, + resources: [GLOBAL_RESOURCE], + privileges: [mockActions.version], + }, + ], + }, + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3a35cf164ad8..36c364f1ff7d 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -13,6 +13,7 @@ import { GLOBAL_RESOURCE } from '../../common/constants'; import { ResourceSerializer } from './resource_serializer'; import type { CheckPrivileges, + CheckPrivilegesOptions, CheckPrivilegesPayload, CheckPrivilegesResponse, HasPrivilegesResponse, @@ -41,14 +42,20 @@ export function checkPrivilegesWithRequestFactory( return function checkPrivilegesWithRequest(request: KibanaRequest): CheckPrivileges { const checkPrivilegesAtResources = async ( resources: string[], - privileges: CheckPrivilegesPayload + privileges: CheckPrivilegesPayload, + { requireLoginAction = true }: CheckPrivilegesOptions = {} ): Promise => { const kibanaPrivileges = Array.isArray(privileges.kibana) ? privileges.kibana : privileges.kibana ? [privileges.kibana] : []; - const allApplicationPrivileges = uniq([actions.version, actions.login, ...kibanaPrivileges]); + + const allApplicationPrivileges = uniq([ + actions.version, + ...(requireLoginAction ? [actions.login] : []), + ...kibanaPrivileges, + ]); const clusterClient = await getClusterClient(); const { body } = await clusterClient.asScoped(request).asCurrentUser.security.hasPrivileges({ @@ -135,18 +142,26 @@ export function checkPrivilegesWithRequestFactory( }; return { - async atSpace(spaceId: string, privileges: CheckPrivilegesPayload) { + async atSpace( + spaceId: string, + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions + ) { const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResources([spaceResource], privileges); + return await checkPrivilegesAtResources([spaceResource], privileges, options); }, - async atSpaces(spaceIds: string[], privileges: CheckPrivilegesPayload) { + async atSpaces( + spaceIds: string[], + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions + ) { const spaceResources = spaceIds.map((spaceId) => ResourceSerializer.serializeSpaceResource(spaceId) ); - return await checkPrivilegesAtResources(spaceResources, privileges); + return await checkPrivilegesAtResources(spaceResources, privileges, options); }, - async globally(privileges: CheckPrivilegesPayload) { - return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges); + async globally(privileges: CheckPrivilegesPayload, options?: CheckPrivilegesOptions) { + return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges, options); }, }; }; diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts index 547782bbd1ba..9fd14c6d2980 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts @@ -8,6 +8,7 @@ import { httpServerMock } from 'src/core/server/mocks'; import { checkPrivilegesDynamicallyWithRequestFactory } from './check_privileges_dynamically'; +import type { CheckPrivilegesOptions } from './types'; test(`checkPrivileges.atSpace when spaces is enabled`, async () => { const expectedResult = Symbol(); @@ -25,13 +26,18 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { namespaceToSpaceId: jest.fn(), }) )(request); - const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); + const options: CheckPrivilegesOptions = { requireLoginAction: true }; + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }, options); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, { - kibana: privilegeOrPrivileges, - }); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith( + spaceId, + { + kibana: privilegeOrPrivileges, + }, + options + ); }); test(`checkPrivileges.globally when spaces is disabled`, async () => { @@ -46,9 +52,13 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { mockCheckPrivilegesWithRequest, () => undefined )(request); - const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); + const options: CheckPrivilegesOptions = { requireLoginAction: true }; + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }, options); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: privilegeOrPrivileges }); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith( + { kibana: privilegeOrPrivileges }, + options + ); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 4ce59c870627..d4e335ba0405 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -9,13 +9,15 @@ import type { KibanaRequest } from 'src/core/server'; import type { SpacesService } from '../plugin'; import type { + CheckPrivilegesOptions, CheckPrivilegesPayload, CheckPrivilegesResponse, CheckPrivilegesWithRequest, } from './types'; export type CheckPrivilegesDynamically = ( - privileges: CheckPrivilegesPayload + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions ) => Promise; export type CheckPrivilegesDynamicallyWithRequest = ( @@ -28,11 +30,15 @@ export function checkPrivilegesDynamicallyWithRequestFactory( ): CheckPrivilegesDynamicallyWithRequest { return function checkPrivilegesDynamicallyWithRequest(request: KibanaRequest) { const checkPrivileges = checkPrivilegesWithRequest(request); - return async function checkPrivilegesDynamically(privileges: CheckPrivilegesPayload) { + + return async function checkPrivilegesDynamically( + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions + ) { const spacesService = getSpacesService(); return spacesService - ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges) - : await checkPrivileges.globally(privileges); + ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges, options) + : await checkPrivileges.globally(privileges, options); }; }; } diff --git a/x-pack/plugins/security/server/authorization/types.ts b/x-pack/plugins/security/server/authorization/types.ts index 8bfe89284063..aee059fb8bec 100644 --- a/x-pack/plugins/security/server/authorization/types.ts +++ b/x-pack/plugins/security/server/authorization/types.ts @@ -29,6 +29,18 @@ export interface HasPrivilegesResponse { }; } +/** + * Options to influce the privilege checks. + */ +export interface CheckPrivilegesOptions { + /** + * Whether or not the `login` action should be required (default: true). + * Setting this to false is not advised except for special circumstances, when you do not require + * the request to belong to a user capable of logging into Kibana. + */ + requireLoginAction?: boolean; +} + export interface CheckPrivilegesResponse { hasAllRequested: boolean; username: string; @@ -59,12 +71,20 @@ export interface CheckPrivilegesResponse { export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; export interface CheckPrivileges { - atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise; + atSpace( + spaceId: string, + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions + ): Promise; atSpaces( spaceIds: string[], - privileges: CheckPrivilegesPayload + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions + ): Promise; + globally( + privileges: CheckPrivilegesPayload, + options?: CheckPrivilegesOptions ): Promise; - globally(privileges: CheckPrivilegesPayload): Promise; } export interface CheckPrivilegesPayload { diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 762fc1642a87..f234855b84e1 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -73,7 +73,7 @@ export default function ({ getService }: FtrProviderContext) { 'packs_read', ], }, - reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], + reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; await supertest diff --git a/x-pack/test/api_integration/apis/security/privileges_basic.ts b/x-pack/test/api_integration/apis/security/privileges_basic.ts index 0efaa25ee57d..ac69bfcd9d5d 100644 --- a/x-pack/test/api_integration/apis/security/privileges_basic.ts +++ b/x-pack/test/api_integration/apis/security/privileges_basic.ts @@ -45,7 +45,7 @@ export default function ({ getService }: FtrProviderContext) { }, global: ['all', 'read'], space: ['all', 'read'], - reserved: ['ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], + reserved: ['fleet-setup', 'ml_user', 'ml_admin', 'ml_apm_user', 'monitoring'], }; await supertest diff --git a/x-pack/test/fleet_api_integration/apis/agents/services.ts b/x-pack/test/fleet_api_integration/apis/agents/services.ts index be5d2d438f76..0e28ad647bbc 100644 --- a/x-pack/test/fleet_api_integration/apis/agents/services.ts +++ b/x-pack/test/fleet_api_integration/apis/agents/services.ts @@ -32,12 +32,30 @@ export function getEsClientForAPIKey({ getService }: FtrProviderContext, esApiKe }); } -export function setupFleetAndAgents({ getService }: FtrProviderContext) { +export function setupFleetAndAgents(providerContext: FtrProviderContext) { before(async () => { - await getService('supertest').post(`/api/fleet/setup`).set('kbn-xsrf', 'xxx').send(); - await getService('supertest') + // Use elastic/fleet-server service account to execute setup to verify privilege configuration + const es = providerContext.getService('es'); + const { + body: { token }, + // @ts-expect-error SecurityCreateServiceTokenRequest should not require `name` + } = await es.security.createServiceToken({ + namespace: 'elastic', + service: 'fleet-server', + }); + const supetestWithoutAuth = getSupertestWithoutAuth(providerContext); + + await supetestWithoutAuth + .post(`/api/fleet/setup`) + .set('kbn-xsrf', 'xxx') + .set('Authorization', `Bearer ${token.value}`) + .send() + .expect(200); + await supetestWithoutAuth .post(`/api/fleet/agents/setup`) .set('kbn-xsrf', 'xxx') - .send({ forceRecreate: true }); + .set('Authorization', `Bearer ${token.value}`) + .send({ forceRecreate: true }) + .expect(200); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/setup.ts b/x-pack/test/fleet_api_integration/apis/epm/setup.ts index 8567cf8069c5..051636ad11f5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/setup.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/setup.ts @@ -14,7 +14,9 @@ import { setupFleetAndAgents } from '../agents/services'; export default function (providerContext: FtrProviderContext) { const { getService } = providerContext; const supertest = getService('supertest'); + const supertestWithoutAuth = getService('supertestWithoutAuth'); const log = getService('log'); + const es = getService('es'); describe('setup api', async () => { skipIfNoDockerRegistry(providerContext); @@ -47,5 +49,48 @@ export default function (providerContext: FtrProviderContext) { ); }); }); + + it('allows elastic/fleet-server user to call required APIs', async () => { + const { + body: { token }, + // @ts-expect-error SecurityCreateServiceTokenRequest should not require `name` + } = await es.security.createServiceToken({ + namespace: 'elastic', + service: 'fleet-server', + }); + + // elastic/fleet-server needs access to these APIs: + // POST /api/fleet/setup + // POST /api/fleet/agents/setup + // GET /api/fleet/agent_policies + // GET /api/fleet/enrollment-api-keys + // GET /api/fleet/enrollment-api-keys/ + await supertestWithoutAuth + .post('/api/fleet/setup') + .set('Authorization', `Bearer ${token.value}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + await supertestWithoutAuth + .post('/api/fleet/agents/setup') + .set('Authorization', `Bearer ${token.value}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + await supertestWithoutAuth + .get('/api/fleet/agent_policies') + .set('Authorization', `Bearer ${token.value}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + const response = await supertestWithoutAuth + .get('/api/fleet/enrollment-api-keys') + .set('Authorization', `Bearer ${token.value}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + const enrollmentApiKeyId = response.body.list[0].id; + await supertestWithoutAuth + .get(`/api/fleet/enrollment-api-keys/${enrollmentApiKeyId}`) + .set('Authorization', `Bearer ${token.value}`) + .set('kbn-xsrf', 'xxx') + .expect(200); + }); }); } From f6a9afea6165c6072bd0c3fdf00439b7a98de1fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Tue, 19 Oct 2021 11:33:57 +0100 Subject: [PATCH 34/53] [Stack management apps] Deprecate "enabled" Kibana setting (#114768) --- docs/dev-tools/console/console.asciidoc | 9 + docs/setup/settings.asciidoc | 34 +++ src/plugins/console/public/index.ts | 7 +- src/plugins/console/public/plugin.ts | 122 ++++++----- src/plugins/console/public/types/config.ts | 13 ++ src/plugins/console/public/types/index.ts | 2 + src/plugins/console/public/types/locator.ts | 12 ++ .../public/types/plugin_dependencies.ts | 8 +- src/plugins/console/server/config.ts | 199 ++++++++++++++---- src/plugins/console/server/index.ts | 1 + src/plugins/console/server/plugin.ts | 10 +- .../components/manage_data/manage_data.tsx | 3 +- .../components/details/req_code_viewer.tsx | 12 +- .../common/constants/index.ts | 2 + .../server/config.ts | 96 ++++++++- .../cross_cluster_replication/server/index.ts | 13 +- .../common/constants/index.ts | 2 + .../server/config.ts | 96 ++++++++- .../server/index.ts | 13 +- .../plugins/index_management/public/plugin.ts | 50 +++-- .../plugins/index_management/public/types.ts | 6 + .../plugins/index_management/server/config.ts | 89 +++++++- .../plugins/index_management/server/index.ts | 10 +- .../common/constants/index.ts | 2 +- .../common/constants/plugin.ts | 2 + .../license_management/server/config.ts | 90 +++++++- .../license_management/server/index.ts | 13 +- .../remote_clusters/common/constants.ts | 2 + .../plugins/remote_clusters/server/config.ts | 89 +++++++- .../plugins/remote_clusters/server/plugin.ts | 4 +- x-pack/plugins/rollup/common/index.ts | 2 + x-pack/plugins/rollup/public/index.ts | 3 +- x-pack/plugins/rollup/public/plugin.ts | 57 ++--- x-pack/plugins/rollup/public/types.ts | 12 ++ x-pack/plugins/rollup/server/config.ts | 89 +++++++- x-pack/plugins/rollup/server/index.ts | 10 +- .../snapshot_restore/common/constants.ts | 2 + .../plugins/snapshot_restore/public/plugin.ts | 84 ++++---- .../plugins/snapshot_restore/public/types.ts | 1 + .../plugins/snapshot_restore/server/config.ts | 98 ++++++++- .../plugins/snapshot_restore/server/index.ts | 13 +- .../plugins/snapshot_restore/server/plugin.ts | 9 +- .../client_integration/helpers/index.ts | 2 +- .../helpers/setup_environment.tsx | 12 +- .../overview/overview.test.tsx | 5 +- .../upgrade_assistant/common/config.ts | 20 -- .../upgrade_assistant/common/constants.ts | 6 +- .../reindex/flyout/warning_step.test.tsx | 18 +- .../upgrade_assistant/public/plugin.ts | 88 ++++---- .../plugins/upgrade_assistant/public/types.ts | 7 + .../upgrade_assistant/server/config.ts | 107 ++++++++++ .../plugins/upgrade_assistant/server/index.ts | 13 +- .../server/lib/__fixtures__/version.ts | 8 +- .../server/lib/es_version_precheck.test.ts | 4 +- .../lib/reindexing/index_settings.test.ts | 8 +- .../lib/reindexing/reindex_actions.test.ts | 4 +- .../lib/reindexing/reindex_service.test.ts | 4 +- 57 files changed, 1279 insertions(+), 418 deletions(-) create mode 100644 src/plugins/console/public/types/config.ts create mode 100644 src/plugins/console/public/types/locator.ts create mode 100644 x-pack/plugins/rollup/public/types.ts delete mode 100644 x-pack/plugins/upgrade_assistant/common/config.ts create mode 100644 x-pack/plugins/upgrade_assistant/server/config.ts diff --git a/docs/dev-tools/console/console.asciidoc b/docs/dev-tools/console/console.asciidoc index 48fe936dd2db..21334c31011f 100644 --- a/docs/dev-tools/console/console.asciidoc +++ b/docs/dev-tools/console/console.asciidoc @@ -129,3 +129,12 @@ image::dev-tools/console/images/console-settings.png["Console Settings", width=6 For a list of available keyboard shortcuts, click *Help*. + +[float] +[[console-settings]] +=== Disable Console + +If you don’t want to use *Console*, you can disable it by setting `console.ui.enabled` +to `false` in your `kibana.yml` configuration file. Changing this setting +causes the server to regenerate assets on the next startup, +which might cause a delay before pages start being served. \ No newline at end of file diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 4802a4da8182..af22ad4ad157 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -20,6 +20,11 @@ configuration using `${MY_ENV_VAR}` syntax. [cols="2*<"] |=== +| `console.ui.enabled:` +Toggling this causes the server to regenerate assets on the next startup, +which may cause a delay before pages start being served. +Set to `false` to disable Console. *Default: `true`* + | `csp.rules:` | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] A https://w3c.github.io/webappsec-csp/[Content Security Policy] template @@ -681,6 +686,10 @@ out through *Advanced Settings*. *Default: `true`* | Set this value to true to allow Vega to use any URL to access external data sources and images. When false, Vega can only get data from {es}. *Default: `false`* +| `xpack.ccr.ui.enabled` +Set this value to false to disable the Cross-Cluster Replication UI. +*Default: `true`* + |[[settings-explore-data-in-context]] `xpack.discoverEnhanced.actions.` `exploreDataInContextMenu.enabled` | Enables the *Explore underlying data* option that allows you to open *Discover* from a dashboard panel and view the panel data. *Default: `false`* @@ -689,6 +698,31 @@ sources and images. When false, Vega can only get data from {es}. *Default: `fal `exploreDataInChart.enabled` | Enables you to view the underlying documents in a data series from a dashboard panel. *Default: `false`* +| `xpack.ilm.ui.enabled` +Set this value to false to disable the Index Lifecycle Policies UI. +*Default: `true`* + +| `xpack.index_management.ui.enabled` +Set this value to false to disable the Index Management UI. +*Default: `true`* + +| `xpack.license_management.ui.enabled` +Set this value to false to disable the License Management UI. +*Default: `true`* + +| `xpack.remote_clusters.ui.enabled` +Set this value to false to disable the Remote Clusters UI. +*Default: `true`* + +| `xpack.rollup.ui.enabled:` +Set this value to false to disable the Rollup Jobs UI. *Default: true* + +| `xpack.snapshot_restore.ui.enabled:` +Set this value to false to disable the Snapshot and Restore UI. *Default: true* + +| `xpack.upgrade_assistant.ui.enabled:` +Set this value to false to disable the Upgrade Assistant UI. *Default: true* + | `i18n.locale` {ess-icon} | Set this value to change the {kib} interface language. Valid locales are: `en`, `zh-CN`, `ja-JP`. *Default: `en`* diff --git a/src/plugins/console/public/index.ts b/src/plugins/console/public/index.ts index 8c4a10710856..9a9c5896cd26 100644 --- a/src/plugins/console/public/index.ts +++ b/src/plugins/console/public/index.ts @@ -7,13 +7,14 @@ */ import './index.scss'; +import { PluginInitializerContext } from 'src/core/public'; import { ConsoleUIPlugin } from './plugin'; -export type { ConsoleUILocatorParams } from './plugin'; +export type { ConsoleUILocatorParams, ConsolePluginSetup } from './types'; export { ConsoleUIPlugin as Plugin }; -export function plugin() { - return new ConsoleUIPlugin(); +export function plugin(ctx: PluginInitializerContext) { + return new ConsoleUIPlugin(ctx); } diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index e3791df6a2db..d61769c23dfe 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -7,77 +7,87 @@ */ import { i18n } from '@kbn/i18n'; -import { SerializableRecord } from '@kbn/utility-types'; -import { Plugin, CoreSetup } from 'src/core/public'; +import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../home/public'; -import { AppSetupUIPluginDependencies } from './types'; - -export interface ConsoleUILocatorParams extends SerializableRecord { - loadFrom?: string; -} +import { + AppSetupUIPluginDependencies, + ClientConfigType, + ConsolePluginSetup, + ConsoleUILocatorParams, +} from './types'; export class ConsoleUIPlugin implements Plugin { + constructor(private ctx: PluginInitializerContext) {} + public setup( { notifications, getStartServices, http }: CoreSetup, { devTools, home, share, usageCollection }: AppSetupUIPluginDependencies - ) { - if (home) { - home.featureCatalogue.register({ + ): ConsolePluginSetup { + const { + ui: { enabled: isConsoleUiEnabled }, + } = this.ctx.config.get(); + + if (isConsoleUiEnabled) { + if (home) { + home.featureCatalogue.register({ + id: 'console', + title: i18n.translate('console.devToolsTitle', { + defaultMessage: 'Interact with the Elasticsearch API', + }), + description: i18n.translate('console.devToolsDescription', { + defaultMessage: 'Skip cURL and use a JSON interface to work with your data in Console.', + }), + icon: 'consoleApp', + path: '/app/dev_tools#/console', + showOnHomePage: false, + category: FeatureCatalogueCategory.ADMIN, + }); + } + + devTools.register({ id: 'console', - title: i18n.translate('console.devToolsTitle', { - defaultMessage: 'Interact with the Elasticsearch API', - }), - description: i18n.translate('console.devToolsDescription', { - defaultMessage: 'Skip cURL and use a JSON interface to work with your data in Console.', + order: 1, + title: i18n.translate('console.consoleDisplayName', { + defaultMessage: 'Console', }), - icon: 'consoleApp', - path: '/app/dev_tools#/console', - showOnHomePage: false, - category: FeatureCatalogueCategory.ADMIN, - }); - } + enableRouting: false, + mount: async ({ element }) => { + const [core] = await getStartServices(); - devTools.register({ - id: 'console', - order: 1, - title: i18n.translate('console.consoleDisplayName', { - defaultMessage: 'Console', - }), - enableRouting: false, - mount: async ({ element }) => { - const [core] = await getStartServices(); + const { + i18n: { Context: I18nContext }, + docLinks: { DOC_LINK_VERSION }, + } = core; - const { - i18n: { Context: I18nContext }, - docLinks: { DOC_LINK_VERSION }, - } = core; + const { renderApp } = await import('./application'); - const { renderApp } = await import('./application'); + return renderApp({ + http, + docLinkVersion: DOC_LINK_VERSION, + I18nContext, + notifications, + usageCollection, + element, + }); + }, + }); - return renderApp({ - http, - docLinkVersion: DOC_LINK_VERSION, - I18nContext, - notifications, - usageCollection, - element, - }); - }, - }); + const locator = share.url.locators.create({ + id: 'CONSOLE_APP_LOCATOR', + getLocation: async ({ loadFrom }) => { + return { + app: 'dev_tools', + path: `#/console${loadFrom ? `?load_from=${loadFrom}` : ''}`, + state: { loadFrom }, + }; + }, + }); - const locator = share.url.locators.create({ - id: 'CONSOLE_APP_LOCATOR', - getLocation: async ({ loadFrom }) => { - return { - app: 'dev_tools', - path: `#/console${loadFrom ? `?load_from=${loadFrom}` : ''}`, - state: { loadFrom }, - }; - }, - }); + return { locator }; + } - return { locator }; + return {}; } public start() {} diff --git a/src/plugins/console/public/types/config.ts b/src/plugins/console/public/types/config.ts new file mode 100644 index 000000000000..da41eef6f548 --- /dev/null +++ b/src/plugins/console/public/types/config.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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. + */ + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/src/plugins/console/public/types/index.ts b/src/plugins/console/public/types/index.ts index b98adbf5610c..d8b6aaf7b12c 100644 --- a/src/plugins/console/public/types/index.ts +++ b/src/plugins/console/public/types/index.ts @@ -11,3 +11,5 @@ export * from './core_editor'; export * from './token'; export * from './tokens_provider'; export * from './common'; +export { ClientConfigType } from './config'; +export { ConsoleUILocatorParams } from './locator'; diff --git a/src/plugins/console/public/types/locator.ts b/src/plugins/console/public/types/locator.ts new file mode 100644 index 000000000000..f3a42338aaad --- /dev/null +++ b/src/plugins/console/public/types/locator.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { SerializableRecord } from '@kbn/utility-types'; + +export interface ConsoleUILocatorParams extends SerializableRecord { + loadFrom?: string; +} diff --git a/src/plugins/console/public/types/plugin_dependencies.ts b/src/plugins/console/public/types/plugin_dependencies.ts index 444776f47ea1..afc49f9a5a98 100644 --- a/src/plugins/console/public/types/plugin_dependencies.ts +++ b/src/plugins/console/public/types/plugin_dependencies.ts @@ -9,7 +9,9 @@ import { HomePublicPluginSetup } from '../../../home/public'; import { DevToolsSetup } from '../../../dev_tools/public'; import { UsageCollectionSetup } from '../../../usage_collection/public'; -import { SharePluginSetup } from '../../../share/public'; +import { SharePluginSetup, LocatorPublic } from '../../../share/public'; + +import { ConsoleUILocatorParams } from './locator'; export interface AppSetupUIPluginDependencies { home?: HomePublicPluginSetup; @@ -17,3 +19,7 @@ export interface AppSetupUIPluginDependencies { share: SharePluginSetup; usageCollection?: UsageCollectionSetup; } + +export interface ConsolePluginSetup { + locator?: LocatorPublic; +} diff --git a/src/plugins/console/server/config.ts b/src/plugins/console/server/config.ts index 6d667fed081e..024777aa8d25 100644 --- a/src/plugins/console/server/config.ts +++ b/src/plugins/console/server/config.ts @@ -7,6 +7,8 @@ */ import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; import { PluginConfigDescriptor } from 'kibana/server'; @@ -14,62 +16,171 @@ import { MAJOR_VERSION } from '../common/constants'; const kibanaVersion = new SemVer(MAJOR_VERSION); -const baseSettings = { - enabled: schema.boolean({ defaultValue: true }), - ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), -}; - -// Settings only available in 7.x -const deprecatedSettings = { - proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), - proxyConfig: schema.arrayOf( - schema.object({ - match: schema.object({ - protocol: schema.string({ defaultValue: '*' }), - host: schema.string({ defaultValue: '*' }), - port: schema.string({ defaultValue: '*' }), - path: schema.string({ defaultValue: '*' }), - }), - - timeout: schema.number(), - ssl: schema.object( - { - verify: schema.boolean(), - ca: schema.arrayOf(schema.string()), - cert: schema.string(), - key: schema.string(), - }, - { defaultValue: undefined } - ), - }), - { defaultValue: [] } - ), -}; - -const configSchema = schema.object( +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( { - ...baseSettings, + ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), }, { defaultValue: undefined } ); -const configSchema7x = schema.object( +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; + +export type ConsoleConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( { - ...baseSettings, - ...deprecatedSettings, + enabled: schema.boolean({ defaultValue: true }), + proxyFilter: schema.arrayOf(schema.string(), { defaultValue: ['.*'] }), + proxyConfig: schema.arrayOf( + schema.object({ + match: schema.object({ + protocol: schema.string({ defaultValue: '*' }), + host: schema.string({ defaultValue: '*' }), + port: schema.string({ defaultValue: '*' }), + path: schema.string({ defaultValue: '*' }), + }), + + timeout: schema.number(), + ssl: schema.object( + { + verify: schema.boolean(), + ca: schema.arrayOf(schema.string()), + cert: schema.string(), + key: schema.string(), + }, + { defaultValue: undefined } + ), + }), + { defaultValue: [] } + ), + ssl: schema.object({ verify: schema.boolean({ defaultValue: false }) }, {}), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), }, { defaultValue: undefined } ); -export type ConfigType = TypeOf; -export type ConfigType7x = TypeOf; +export type ConsoleConfig7x = TypeOf; -export const config: PluginConfigDescriptor = { - schema: kibanaVersion.major < 8 ? configSchema7x : configSchema, +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, deprecations: ({ deprecate, unused }) => [ - deprecate('enabled', '8.0.0'), - deprecate('proxyFilter', '8.0.0'), - deprecate('proxyConfig', '8.0.0'), unused('ssl'), + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'console.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'console.enabled', + level: 'critical', + title: i18n.translate('console.deprecations.enabledTitle', { + defaultMessage: 'Setting "console.enabled" is deprecated', + }), + message: i18n.translate('console.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Console UI, use the "console.ui.enabled" setting instead of "console.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('console.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('console.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: 'Change the "console.enabled" setting to "console.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'console.proxyConfig') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'console.proxyConfig', + level: 'critical', + title: i18n.translate('console.deprecations.proxyConfigTitle', { + defaultMessage: 'Setting "console.proxyConfig" is deprecated', + }), + message: i18n.translate('console.deprecations.proxyConfigMessage', { + defaultMessage: + 'Configuring "console.proxyConfig" is deprecated and will be removed in 8.0.0. To secure your connection between Kibana and Elasticsearch use the standard "server.ssl.*" settings instead.', + }), + documentationUrl: 'https://ela.st/encrypt-kibana-browser', + correctiveActions: { + manualSteps: [ + i18n.translate('console.deprecations.proxyConfig.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('console.deprecations.proxyConfig.manualStepTwoMessage', { + defaultMessage: 'Remove the "console.proxyConfig" setting.', + }), + i18n.translate('console.deprecations.proxyConfig.manualStepThreeMessage', { + defaultMessage: + 'Configure the secure connection between Kibana and Elasticsearch using the "server.ssl.*" settings.', + }), + ], + }, + }); + return completeConfig; + }, + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'console.proxyFilter') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'console.proxyFilter', + level: 'critical', + title: i18n.translate('console.deprecations.proxyFilterTitle', { + defaultMessage: 'Setting "console.proxyFilter" is deprecated', + }), + message: i18n.translate('console.deprecations.proxyFilterMessage', { + defaultMessage: + 'Configuring "console.proxyFilter" is deprecated and will be removed in 8.0.0. To secure your connection between Kibana and Elasticsearch use the standard "server.ssl.*" settings instead.', + }), + documentationUrl: 'https://ela.st/encrypt-kibana-browser', + correctiveActions: { + manualSteps: [ + i18n.translate('console.deprecations.proxyFilter.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('console.deprecations.proxyFilter.manualStepTwoMessage', { + defaultMessage: 'Remove the "console.proxyFilter" setting.', + }), + i18n.translate('console.deprecations.proxyFilter.manualStepThreeMessage', { + defaultMessage: + 'Configure the secure connection between Kibana and Elasticsearch using the "server.ssl.*" settings.', + }), + ], + }, + }); + return completeConfig; + }, ], }; + +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/src/plugins/console/server/index.ts b/src/plugins/console/server/index.ts index 6ae518f5dc79..b270b89a3d45 100644 --- a/src/plugins/console/server/index.ts +++ b/src/plugins/console/server/index.ts @@ -11,6 +11,7 @@ import { PluginInitializerContext } from 'kibana/server'; import { ConsoleServerPlugin } from './plugin'; export { ConsoleSetup, ConsoleStart } from './types'; + export { config } from './config'; export const plugin = (ctx: PluginInitializerContext) => new ConsoleServerPlugin(ctx); diff --git a/src/plugins/console/server/plugin.ts b/src/plugins/console/server/plugin.ts index 613337b286fb..5543c40d03cb 100644 --- a/src/plugins/console/server/plugin.ts +++ b/src/plugins/console/server/plugin.ts @@ -11,7 +11,7 @@ import { SemVer } from 'semver'; import { ProxyConfigCollection } from './lib'; import { SpecDefinitionsService, EsLegacyConfigService } from './services'; -import { ConfigType, ConfigType7x } from './config'; +import { ConsoleConfig, ConsoleConfig7x } from './config'; import { registerRoutes } from './routes'; @@ -24,11 +24,11 @@ export class ConsoleServerPlugin implements Plugin { esLegacyConfigService = new EsLegacyConfigService(); - constructor(private readonly ctx: PluginInitializerContext) { + constructor(private readonly ctx: PluginInitializerContext) { this.log = this.ctx.logger.get(); } - setup({ http, capabilities, getStartServices, elasticsearch }: CoreSetup) { + setup({ http, capabilities, elasticsearch }: CoreSetup) { capabilities.registerProvider(() => ({ dev_tools: { show: true, @@ -43,8 +43,8 @@ export class ConsoleServerPlugin implements Plugin { let proxyConfigCollection: ProxyConfigCollection | undefined; if (kibanaVersion.major < 8) { // "pathFilters" and "proxyConfig" are only used in 7.x - pathFilters = (config as ConfigType7x).proxyFilter.map((str: string) => new RegExp(str)); - proxyConfigCollection = new ProxyConfigCollection((config as ConfigType7x).proxyConfig); + pathFilters = (config as ConsoleConfig7x).proxyFilter.map((str: string) => new RegExp(str)); + proxyConfigCollection = new ProxyConfigCollection((config as ConsoleConfig7x).proxyConfig); } this.esLegacyConfigService.setup(elasticsearch.legacy.config$); diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.tsx index b374bdd2e161..0f465dfcf965 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.tsx @@ -61,7 +61,8 @@ export const ManageData: FC = ({ addBasePath, application, features }) => {isDevToolsEnabled || isManagementEnabled ? ( - {isDevToolsEnabled ? ( + {/* Check if both the Dev Tools UI and the Console UI are enabled. */} + {isDevToolsEnabled && consoleHref !== undefined ? ( (); const navigateToUrl = services.application?.navigateToUrl; - const canShowDevTools = services.application?.capabilities?.dev_tools.show; const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`); - const devToolsHref = services.share.url.locators + const consoleHref = services.share.url.locators .get('CONSOLE_APP_LOCATOR') ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); + // Check if both the Dev Tools UI and the Console UI are enabled. + const canShowDevTools = + services.application?.capabilities?.dev_tools.show && consoleHref !== undefined; const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools); const handleDevToolsLinkClick = useCallback( - () => devToolsHref && navigateToUrl && navigateToUrl(devToolsHref), - [devToolsHref, navigateToUrl] + () => consoleHref && navigateToUrl && navigateToUrl(consoleHref), + [consoleHref, navigateToUrl] ); return ( @@ -79,7 +81,7 @@ export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps size="xs" flush="right" iconType="wrench" - href={devToolsHref} + href={consoleHref} onClick={handleDevToolsLinkClick} data-test-subj="inspectorRequestOpenInConsoleButton" > diff --git a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts index f1b327aed638..a800afcf77ae 100644 --- a/x-pack/plugins/cross_cluster_replication/common/constants/index.ts +++ b/x-pack/plugins/cross_cluster_replication/common/constants/index.ts @@ -19,6 +19,8 @@ export const PLUGIN = { minimumLicenseType: platinumLicense, }; +export const MAJOR_VERSION = '8.0.0'; + export const APPS = { CCR_APP: 'ccr', REMOTE_CLUSTER_APP: 'remote_cluster', diff --git a/x-pack/plugins/cross_cluster_replication/server/config.ts b/x-pack/plugins/cross_cluster_replication/server/config.ts index 50cca903f8a2..732137e308a0 100644 --- a/x-pack/plugins/cross_cluster_replication/server/config.ts +++ b/x-pack/plugins/cross_cluster_replication/server/config.ts @@ -4,14 +4,96 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - ui: schema.object({ +export type CrossClusterReplicationConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { enabled: schema.boolean({ defaultValue: true }), - }), -}); + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +export type CrossClusterReplicationConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.ccr.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.ccr.enabled', + level: 'critical', + title: i18n.translate('xpack.crossClusterReplication.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.ccr.enabled" is deprecated', + }), + message: i18n.translate('xpack.crossClusterReplication.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Cross-Cluster Replication UI, use the "xpack.ccr.ui.enabled" setting instead of "xpack.ccr.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate( + 'xpack.crossClusterReplication.deprecations.enabled.manualStepOneMessage', + { + defaultMessage: 'Open the kibana.yml config file.', + } + ), + i18n.translate( + 'xpack.crossClusterReplication.deprecations.enabled.manualStepTwoMessage', + { + defaultMessage: 'Change the "xpack.ccr.enabled" setting to "xpack.ccr.ui.enabled".', + } + ), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type CrossClusterReplicationConfig = TypeOf; +export const config: PluginConfigDescriptor< + CrossClusterReplicationConfig | CrossClusterReplicationConfig7x +> = kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/cross_cluster_replication/server/index.ts b/x-pack/plugins/cross_cluster_replication/server/index.ts index a6a3ec0fe575..7a0984a6117b 100644 --- a/x-pack/plugins/cross_cluster_replication/server/index.ts +++ b/x-pack/plugins/cross_cluster_replication/server/index.ts @@ -5,17 +5,10 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { CrossClusterReplicationServerPlugin } from './plugin'; -import { configSchema, CrossClusterReplicationConfig } from './config'; + +export { config } from './config'; export const plugin = (pluginInitializerContext: PluginInitializerContext) => new CrossClusterReplicationServerPlugin(pluginInitializerContext); - -export const config: PluginConfigDescriptor = { - schema: configSchema, - exposeToBrowser: { - ui: true, - }, - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], -}; diff --git a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts index 7107489f4e2b..329f479e128e 100644 --- a/x-pack/plugins/index_lifecycle_management/common/constants/index.ts +++ b/x-pack/plugins/index_lifecycle_management/common/constants/index.ts @@ -19,6 +19,8 @@ export const PLUGIN = { }), }; +export const MAJOR_VERSION = '8.0.0'; + export const API_BASE_PATH = '/api/index_lifecycle_management'; export { MIN_SEARCHABLE_SNAPSHOT_LICENSE, MIN_PLUGIN_LICENSE }; diff --git a/x-pack/plugins/index_lifecycle_management/server/config.ts b/x-pack/plugins/index_lifecycle_management/server/config.ts index f3fdf59cec55..691cc06708bb 100644 --- a/x-pack/plugins/index_lifecycle_management/server/config.ts +++ b/x-pack/plugins/index_lifecycle_management/server/config.ts @@ -4,16 +4,94 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + // Cloud requires the ability to hide internal node attributes from users. + filteredNodeAttributes: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - ui: schema.object({ +export type IndexLifecycleManagementConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { enabled: schema.boolean({ defaultValue: true }), - }), - // Cloud requires the ability to hide internal node attributes from users. - filteredNodeAttributes: schema.arrayOf(schema.string(), { defaultValue: [] }), -}); + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + // Cloud requires the ability to hide internal node attributes from users. + filteredNodeAttributes: schema.arrayOf(schema.string(), { defaultValue: [] }), + }, + { defaultValue: undefined } +); + +export type IndexLifecycleManagementConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.ilm.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.ilm.enabled', + level: 'critical', + title: i18n.translate('xpack.indexLifecycleMgmt.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.ilm.enabled" is deprecated', + }), + message: i18n.translate('xpack.indexLifecycleMgmt.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Index Lifecycle Policies UI, use the "xpack.ilm.ui.enabled" setting instead of "xpack.ilm.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.indexLifecycleMgmt.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.indexLifecycleMgmt.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: 'Change the "xpack.ilm.enabled" setting to "xpack.ilm.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type IndexLifecycleManagementConfig = TypeOf; +export const config: PluginConfigDescriptor< + IndexLifecycleManagementConfig | IndexLifecycleManagementConfig7x +> = kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/index_lifecycle_management/server/index.ts b/x-pack/plugins/index_lifecycle_management/server/index.ts index 1f8b01913fd3..6a74b4c80b2d 100644 --- a/x-pack/plugins/index_lifecycle_management/server/index.ts +++ b/x-pack/plugins/index_lifecycle_management/server/index.ts @@ -5,17 +5,10 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; +import { PluginInitializerContext } from 'kibana/server'; import { IndexLifecycleManagementServerPlugin } from './plugin'; -import { configSchema, IndexLifecycleManagementConfig } from './config'; + +export { config } from './config'; export const plugin = (ctx: PluginInitializerContext) => new IndexLifecycleManagementServerPlugin(ctx); - -export const config: PluginConfigDescriptor = { - schema: configSchema, - exposeToBrowser: { - ui: true, - }, - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], -}; diff --git a/x-pack/plugins/index_management/public/plugin.ts b/x-pack/plugins/index_management/public/plugin.ts index 4e123b6f474f..2394167ca61b 100644 --- a/x-pack/plugins/index_management/public/plugin.ts +++ b/x-pack/plugins/index_management/public/plugin.ts @@ -13,7 +13,12 @@ import { setExtensionsService } from './application/store/selectors/extension_se import { ExtensionsService } from './services'; -import { IndexManagementPluginSetup, SetupDependencies, StartDependencies } from './types'; +import { + IndexManagementPluginSetup, + SetupDependencies, + StartDependencies, + ClientConfigType, +} from './types'; // avoid import from index files in plugin.ts, use specific import paths import { PLUGIN } from '../common/constants/plugin'; @@ -31,25 +36,30 @@ export class IndexMgmtUIPlugin { coreSetup: CoreSetup, plugins: SetupDependencies ): IndexManagementPluginSetup { - const { fleet, usageCollection, management } = plugins; - const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); - - management.sections.section.data.registerApp({ - id: PLUGIN.id, - title: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }), - order: 0, - mount: async (params) => { - const { mountManagementSection } = await import('./application/mount_management_section'); - return mountManagementSection( - coreSetup, - usageCollection, - params, - this.extensionsService, - Boolean(fleet), - kibanaVersion - ); - }, - }); + const { + ui: { enabled: isIndexManagementUiEnabled }, + } = this.ctx.config.get(); + + if (isIndexManagementUiEnabled) { + const { fleet, usageCollection, management } = plugins; + const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); + management.sections.section.data.registerApp({ + id: PLUGIN.id, + title: i18n.translate('xpack.idxMgmt.appTitle', { defaultMessage: 'Index Management' }), + order: 0, + mount: async (params) => { + const { mountManagementSection } = await import('./application/mount_management_section'); + return mountManagementSection( + coreSetup, + usageCollection, + params, + this.extensionsService, + Boolean(fleet), + kibanaVersion + ); + }, + }); + } return { extensionsService: this.extensionsService.setup(), diff --git a/x-pack/plugins/index_management/public/types.ts b/x-pack/plugins/index_management/public/types.ts index 05c486e299c7..e0af6b160cf1 100644 --- a/x-pack/plugins/index_management/public/types.ts +++ b/x-pack/plugins/index_management/public/types.ts @@ -23,3 +23,9 @@ export interface SetupDependencies { export interface StartDependencies { share: SharePluginStart; } + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/index_management/server/config.ts b/x-pack/plugins/index_management/server/config.ts index 0a314c7654b1..88a714db5edc 100644 --- a/x-pack/plugins/index_management/server/config.ts +++ b/x-pack/plugins/index_management/server/config.ts @@ -4,11 +4,90 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; + +export type IndexManagementConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +export type IndexManagementConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.index_management.enabled') === undefined) { + return completeConfig; + } -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); + addDeprecation({ + configPath: 'xpack.index_management.enabled', + level: 'critical', + title: i18n.translate('xpack.idxMgmt.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.index_management.enabled" is deprecated', + }), + message: i18n.translate('xpack.idxMgmt.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Index Management UI, use the "xpack.index_management.ui.enabled" setting instead of "xpack.index_management.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.idxMgmt.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.idxMgmt.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.index_management.enabled" setting to "xpack.index_management.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type IndexManagementConfig = TypeOf; +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/index_management/server/index.ts b/x-pack/plugins/index_management/server/index.ts index 14b67e2ffd58..29291116e44f 100644 --- a/x-pack/plugins/index_management/server/index.ts +++ b/x-pack/plugins/index_management/server/index.ts @@ -5,17 +5,13 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { IndexMgmtServerPlugin } from './plugin'; -import { configSchema } from './config'; -export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context); +export { config } from './config'; -export const config: PluginConfigDescriptor = { - schema: configSchema, - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], -}; +export const plugin = (context: PluginInitializerContext) => new IndexMgmtServerPlugin(context); /** @public */ export { Dependencies } from './types'; diff --git a/x-pack/plugins/license_management/common/constants/index.ts b/x-pack/plugins/license_management/common/constants/index.ts index 0567b0008f0c..9735eabeb1e4 100644 --- a/x-pack/plugins/license_management/common/constants/index.ts +++ b/x-pack/plugins/license_management/common/constants/index.ts @@ -5,7 +5,7 @@ * 2.0. */ -export { PLUGIN } from './plugin'; +export { PLUGIN, MAJOR_VERSION } from './plugin'; export { API_BASE_PATH } from './base_path'; export { EXTERNAL_LINKS } from './external_links'; export { APP_PERMISSION } from './permissions'; diff --git a/x-pack/plugins/license_management/common/constants/plugin.ts b/x-pack/plugins/license_management/common/constants/plugin.ts index ae7fd0f6e8a2..76f4d94a0188 100644 --- a/x-pack/plugins/license_management/common/constants/plugin.ts +++ b/x-pack/plugins/license_management/common/constants/plugin.ts @@ -13,3 +13,5 @@ export const PLUGIN = { }), id: 'license_management', }; + +export const MAJOR_VERSION = '8.0.0'; diff --git a/x-pack/plugins/license_management/server/config.ts b/x-pack/plugins/license_management/server/config.ts index 0e4de29b718b..e378a1019168 100644 --- a/x-pack/plugins/license_management/server/config.ts +++ b/x-pack/plugins/license_management/server/config.ts @@ -4,14 +4,90 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - ui: schema.object({ +export type LicenseManagementConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { enabled: schema.boolean({ defaultValue: true }), - }), -}); + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +export type LicenseManagementConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.license_management.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.license_management.enabled', + level: 'critical', + title: i18n.translate('xpack.licenseMgmt.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.license_management.enabled" is deprecated', + }), + message: i18n.translate('xpack.licenseMgmt.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the License Management UI, use the "xpack.license_management.ui.enabled" setting instead of "xpack.license_management.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.licenseMgmt.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.licenseMgmt.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.license_management.enabled" setting to "xpack.license_management.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type LicenseManagementConfig = TypeOf; +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/license_management/server/index.ts b/x-pack/plugins/license_management/server/index.ts index e78ffe07b50c..7aa6bfb06d54 100644 --- a/x-pack/plugins/license_management/server/index.ts +++ b/x-pack/plugins/license_management/server/index.ts @@ -5,17 +5,10 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { LicenseManagementServerPlugin } from './plugin'; -import { configSchema, LicenseManagementConfig } from './config'; -export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementServerPlugin(); +export { config } from './config'; -export const config: PluginConfigDescriptor = { - schema: configSchema, - exposeToBrowser: { - ui: true, - }, - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], -}; +export const plugin = (ctx: PluginInitializerContext) => new LicenseManagementServerPlugin(); diff --git a/x-pack/plugins/remote_clusters/common/constants.ts b/x-pack/plugins/remote_clusters/common/constants.ts index b11292141672..5a36924b2643 100644 --- a/x-pack/plugins/remote_clusters/common/constants.ts +++ b/x-pack/plugins/remote_clusters/common/constants.ts @@ -20,6 +20,8 @@ export const PLUGIN = { }, }; +export const MAJOR_VERSION = '8.0.0'; + export const API_BASE_PATH = '/api/remote_clusters'; export const SNIFF_MODE = 'sniff'; diff --git a/x-pack/plugins/remote_clusters/server/config.ts b/x-pack/plugins/remote_clusters/server/config.ts index 8f379ec5613c..5b4972f0a525 100644 --- a/x-pack/plugins/remote_clusters/server/config.ts +++ b/x-pack/plugins/remote_clusters/server/config.ts @@ -4,23 +4,90 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; -import { PluginConfigDescriptor } from 'kibana/server'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; + +export type RemoteClustersConfig = TypeOf; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - ui: schema.object({ +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { enabled: schema.boolean({ defaultValue: true }), - }), -}); + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); -export type ConfigType = TypeOf; +export type RemoteClustersConfig7x = TypeOf; -export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], - schema: configSchema, +const config7x: PluginConfigDescriptor = { exposeToBrowser: { ui: true, }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.remote_clusters.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.remote_clusters.enabled', + level: 'critical', + title: i18n.translate('xpack.remoteClusters.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.remote_clusters.enabled" is deprecated', + }), + message: i18n.translate('xpack.remoteClusters.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Remote Clusters UI, use the "xpack.remote_clusters.ui.enabled" setting instead of "xpack.remote_clusters.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.remoteClusters.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.remoteClusters.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.remote_clusters.enabled" setting to "xpack.remote_clusters.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], }; + +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index b13773c27034..fde71677b844 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -11,7 +11,7 @@ import { CoreSetup, Logger, Plugin, PluginInitializerContext } from 'src/core/se import { PLUGIN } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; -import { ConfigType } from './config'; +import { RemoteClustersConfig, RemoteClustersConfig7x } from './config'; import { registerGetRoute, registerAddRoute, @@ -30,7 +30,7 @@ export class RemoteClustersServerPlugin { licenseStatus: LicenseStatus; log: Logger; - config: ConfigType; + config: RemoteClustersConfig | RemoteClustersConfig7x; constructor({ logger, config }: PluginInitializerContext) { this.log = logger.get(); diff --git a/x-pack/plugins/rollup/common/index.ts b/x-pack/plugins/rollup/common/index.ts index dffbfbd18209..c912a905d061 100644 --- a/x-pack/plugins/rollup/common/index.ts +++ b/x-pack/plugins/rollup/common/index.ts @@ -14,6 +14,8 @@ export const PLUGIN = { minimumLicenseType: basicLicense, }; +export const MAJOR_VERSION = '8.0.0'; + export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns'; export const API_BASE_PATH = '/api/rollup'; diff --git a/x-pack/plugins/rollup/public/index.ts b/x-pack/plugins/rollup/public/index.ts index b70ce8649338..f740971b4bcb 100644 --- a/x-pack/plugins/rollup/public/index.ts +++ b/x-pack/plugins/rollup/public/index.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { PluginInitializerContext } from 'src/core/public'; import { RollupPlugin } from './plugin'; -export const plugin = () => new RollupPlugin(); +export const plugin = (ctx: PluginInitializerContext) => new RollupPlugin(ctx); diff --git a/x-pack/plugins/rollup/public/plugin.ts b/x-pack/plugins/rollup/public/plugin.ts index 0d345e326193..e458a13ee0e0 100644 --- a/x-pack/plugins/rollup/public/plugin.ts +++ b/x-pack/plugins/rollup/public/plugin.ts @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import { CoreSetup, CoreStart, Plugin } from 'kibana/public'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'kibana/public'; import { rollupBadgeExtension, rollupToggleExtension } from './extend_index_management'; // @ts-ignore import { RollupIndexPatternCreationConfig } from './index_pattern_creation/rollup_index_pattern_creation_config'; @@ -23,6 +23,7 @@ import { IndexManagementPluginSetup } from '../../index_management/public'; import { setHttp, init as initDocumentation } from './crud_app/services/index'; import { setNotifications, setFatalErrors, setUiStatsReporter } from './kibana_services'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; +import { ClientConfigType } from './types'; export interface RollupPluginSetupDependencies { home?: HomePublicPluginSetup; @@ -32,10 +33,16 @@ export interface RollupPluginSetupDependencies { } export class RollupPlugin implements Plugin { + constructor(private ctx: PluginInitializerContext) {} + setup( core: CoreSetup, { home, management, indexManagement, usageCollection }: RollupPluginSetupDependencies ) { + const { + ui: { enabled: isRollupUiEnabled }, + } = this.ctx.config.get(); + setFatalErrors(core.fatalErrors); if (usageCollection) { setUiStatsReporter(usageCollection.reportUiCounter.bind(usageCollection, UIM_APP_NAME)); @@ -46,7 +53,7 @@ export class RollupPlugin implements Plugin { indexManagement.extensionsService.addToggle(rollupToggleExtension); } - if (home) { + if (home && isRollupUiEnabled) { home.featureCatalogue.register({ id: 'rollup_jobs', title: 'Rollups', @@ -61,33 +68,35 @@ export class RollupPlugin implements Plugin { }); } - const pluginName = i18n.translate('xpack.rollupJobs.appTitle', { - defaultMessage: 'Rollup Jobs', - }); + if (isRollupUiEnabled) { + const pluginName = i18n.translate('xpack.rollupJobs.appTitle', { + defaultMessage: 'Rollup Jobs', + }); - management.sections.section.data.registerApp({ - id: 'rollup_jobs', - title: pluginName, - order: 4, - async mount(params) { - const [coreStart] = await core.getStartServices(); + management.sections.section.data.registerApp({ + id: 'rollup_jobs', + title: pluginName, + order: 4, + async mount(params) { + const [coreStart] = await core.getStartServices(); - const { - chrome: { docTitle }, - } = coreStart; + const { + chrome: { docTitle }, + } = coreStart; - docTitle.change(pluginName); - params.setBreadcrumbs([{ text: pluginName }]); + docTitle.change(pluginName); + params.setBreadcrumbs([{ text: pluginName }]); - const { renderApp } = await import('./application'); - const unmountAppCallback = await renderApp(core, params); + const { renderApp } = await import('./application'); + const unmountAppCallback = await renderApp(core, params); - return () => { - docTitle.reset(); - unmountAppCallback(); - }; - }, - }); + return () => { + docTitle.reset(); + unmountAppCallback(); + }; + }, + }); + } } start(core: CoreStart) { diff --git a/x-pack/plugins/rollup/public/types.ts b/x-pack/plugins/rollup/public/types.ts new file mode 100644 index 000000000000..dc5e55e9268f --- /dev/null +++ b/x-pack/plugins/rollup/public/types.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export interface ClientConfigType { + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/rollup/server/config.ts b/x-pack/plugins/rollup/server/config.ts index d20b31742210..c0cca4bbb4d3 100644 --- a/x-pack/plugins/rollup/server/config.ts +++ b/x-pack/plugins/rollup/server/config.ts @@ -4,11 +4,90 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; + +export type RollupConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +export type RollupConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.rollup.enabled') === undefined) { + return completeConfig; + } -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), -}); + addDeprecation({ + configPath: 'xpack.rollup.enabled', + level: 'critical', + title: i18n.translate('xpack.rollupJobs.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.rollup.enabled" is deprecated', + }), + message: i18n.translate('xpack.rollupJobs.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Rollup Jobs UI, use the "xpack.rollup.ui.enabled" setting instead of "xpack.rollup.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.rollupJobs.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.rollupJobs.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.rollup.enabled" setting to "xpack.rollup.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type RollupConfig = TypeOf; +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/rollup/server/index.ts b/x-pack/plugins/rollup/server/index.ts index e77e0e6f15d7..6ae1d9f24b8b 100644 --- a/x-pack/plugins/rollup/server/index.ts +++ b/x-pack/plugins/rollup/server/index.ts @@ -5,14 +5,10 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { RollupPlugin } from './plugin'; -import { configSchema, RollupConfig } from './config'; + +export { config } from './config'; export const plugin = (pluginInitializerContext: PluginInitializerContext) => new RollupPlugin(pluginInitializerContext); - -export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], - schema: configSchema, -}; diff --git a/x-pack/plugins/snapshot_restore/common/constants.ts b/x-pack/plugins/snapshot_restore/common/constants.ts index b18e118dc5ff..df13bd4c2f1f 100644 --- a/x-pack/plugins/snapshot_restore/common/constants.ts +++ b/x-pack/plugins/snapshot_restore/common/constants.ts @@ -20,6 +20,8 @@ export const PLUGIN = { }, }; +export const MAJOR_VERSION = '8.0.0'; + export const API_BASE_PATH = '/api/snapshot_restore/'; export enum REPOSITORY_TYPES { diff --git a/x-pack/plugins/snapshot_restore/public/plugin.ts b/x-pack/plugins/snapshot_restore/public/plugin.ts index bb091a1fd183..0351716fad5b 100644 --- a/x-pack/plugins/snapshot_restore/public/plugin.ts +++ b/x-pack/plugins/snapshot_restore/public/plugin.ts @@ -42,52 +42,58 @@ export class SnapshotRestoreUIPlugin { public setup(coreSetup: CoreSetup, plugins: PluginsDependencies): void { const config = this.initializerContext.config.get(); - const { http } = coreSetup; - const { home, management, usageCollection } = plugins; + const { + ui: { enabled: isSnapshotRestoreUiEnabled }, + } = config; - // Initialize services - this.uiMetricService.setup(usageCollection); - textService.setup(i18n); - httpService.setup(http); + if (isSnapshotRestoreUiEnabled) { + const { http } = coreSetup; + const { home, management, usageCollection } = plugins; - management.sections.section.data.registerApp({ - id: PLUGIN.id, - title: i18n.translate('xpack.snapshotRestore.appTitle', { - defaultMessage: 'Snapshot and Restore', - }), - order: 3, - mount: async (params) => { - const { mountManagementSection } = await import('./application/mount_management_section'); - const services = { - uiMetricService: this.uiMetricService, - }; - return await mountManagementSection(coreSetup, services, config, params); - }, - }); + // Initialize services + this.uiMetricService.setup(usageCollection); + textService.setup(i18n); + httpService.setup(http); - if (home) { - home.featureCatalogue.register({ + management.sections.section.data.registerApp({ id: PLUGIN.id, - title: i18n.translate('xpack.snapshotRestore.featureCatalogueTitle', { - defaultMessage: 'Back up and restore', + title: i18n.translate('xpack.snapshotRestore.appTitle', { + defaultMessage: 'Snapshot and Restore', }), - description: i18n.translate('xpack.snapshotRestore.featureCatalogueDescription', { - defaultMessage: - 'Save snapshots to a backup repository, and restore to recover index and cluster state.', - }), - icon: 'storage', - path: '/app/management/data/snapshot_restore', - showOnHomePage: true, - category: FeatureCatalogueCategory.ADMIN, - order: 630, + order: 3, + mount: async (params) => { + const { mountManagementSection } = await import('./application/mount_management_section'); + const services = { + uiMetricService: this.uiMetricService, + }; + return await mountManagementSection(coreSetup, services, config, params); + }, }); - } - plugins.share.url.locators.create( - new SnapshotRestoreLocatorDefinition({ - managementAppLocator: plugins.management.locator, - }) - ); + if (home) { + home.featureCatalogue.register({ + id: PLUGIN.id, + title: i18n.translate('xpack.snapshotRestore.featureCatalogueTitle', { + defaultMessage: 'Back up and restore', + }), + description: i18n.translate('xpack.snapshotRestore.featureCatalogueDescription', { + defaultMessage: + 'Save snapshots to a backup repository, and restore to recover index and cluster state.', + }), + icon: 'storage', + path: '/app/management/data/snapshot_restore', + showOnHomePage: true, + category: FeatureCatalogueCategory.ADMIN, + order: 630, + }); + } + + plugins.share.url.locators.create( + new SnapshotRestoreLocatorDefinition({ + managementAppLocator: plugins.management.locator, + }) + ); + } } public start() {} diff --git a/x-pack/plugins/snapshot_restore/public/types.ts b/x-pack/plugins/snapshot_restore/public/types.ts index b73170ad9d57..c58c942b4bc1 100644 --- a/x-pack/plugins/snapshot_restore/public/types.ts +++ b/x-pack/plugins/snapshot_restore/public/types.ts @@ -7,4 +7,5 @@ export interface ClientConfigType { slm_ui: { enabled: boolean }; + ui: { enabled: boolean }; } diff --git a/x-pack/plugins/snapshot_restore/server/config.ts b/x-pack/plugins/snapshot_restore/server/config.ts index f0ca416ef203..cc430f475661 100644 --- a/x-pack/plugins/snapshot_restore/server/config.ts +++ b/x-pack/plugins/snapshot_restore/server/config.ts @@ -4,14 +4,98 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { SemVer } from 'semver'; +import { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + slm_ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + slm_ui: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - slm_ui: schema.object({ +export type SnapshotRestoreConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { enabled: schema.boolean({ defaultValue: true }), - }), -}); + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + slm_ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + }, + { defaultValue: undefined } +); + +export type SnapshotRestoreConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + slm_ui: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.snapshot_restore.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.snapshot_restore.enabled', + level: 'critical', + title: i18n.translate('xpack.snapshotRestore.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.snapshot_restore.enabled" is deprecated', + }), + message: i18n.translate('xpack.snapshotRestore.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Snapshot and Restore UI, use the "xpack.snapshot_restore.ui.enabled" setting instead of "xpack.snapshot_restore.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.snapshotRestore.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.snapshotRestore.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.snapshot_restore.enabled" setting to "xpack.snapshot_restore.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; -export type SnapshotRestoreConfig = TypeOf; +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/snapshot_restore/server/index.ts b/x-pack/plugins/snapshot_restore/server/index.ts index e10bffd6073d..1e9d2b55aa20 100644 --- a/x-pack/plugins/snapshot_restore/server/index.ts +++ b/x-pack/plugins/snapshot_restore/server/index.ts @@ -5,16 +5,9 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'kibana/server'; +import { PluginInitializerContext } from 'kibana/server'; import { SnapshotRestoreServerPlugin } from './plugin'; -import { configSchema, SnapshotRestoreConfig } from './config'; -export const plugin = (ctx: PluginInitializerContext) => new SnapshotRestoreServerPlugin(ctx); +export { config } from './config'; -export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], - schema: configSchema, - exposeToBrowser: { - slm_ui: true, - }, -}; +export const plugin = (ctx: PluginInitializerContext) => new SnapshotRestoreServerPlugin(ctx); diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index 4414e3735959..d737807ec8da 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -28,16 +28,9 @@ export class SnapshotRestoreServerPlugin implements Plugin this.license = new License(); } - public setup( - { http, getStartServices }: CoreSetup, - { licensing, features, security, cloud }: Dependencies - ): void { + public setup({ http }: CoreSetup, { licensing, features, security, cloud }: Dependencies): void { const pluginConfig = this.context.config.get(); - if (!pluginConfig.enabled) { - return; - } - const router = http.createRouter(); this.license.setup( diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts index b19c8b3d0f08..b2a1c4e80ec7 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/index.ts @@ -9,4 +9,4 @@ export { setup as setupOverviewPage, OverviewTestBed } from './overview.helpers' export { setup as setupElasticsearchPage, ElasticsearchTestBed } from './elasticsearch.helpers'; export { setup as setupKibanaPage, KibanaTestBed } from './kibana.helpers'; -export { setupEnvironment } from './setup_environment'; +export { setupEnvironment, kibanaVersion } from './setup_environment'; diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx index a1cdfaa3446c..fbbbc0e07853 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/helpers/setup_environment.tsx @@ -9,7 +9,7 @@ import React from 'react'; import axios from 'axios'; // @ts-ignore import axiosXhrAdapter from 'axios/lib/adapters/xhr'; - +import SemVer from 'semver/classes/semver'; import { deprecationsServiceMock, docLinksServiceMock, @@ -19,7 +19,7 @@ import { import { HttpSetup } from 'src/core/public'; import { KibanaContextProvider } from '../../../public/shared_imports'; -import { mockKibanaSemverVersion } from '../../../common/constants'; +import { MAJOR_VERSION } from '../../../common/constants'; import { AppContextProvider } from '../../../public/application/app_context'; import { apiService } from '../../../public/application/lib/api'; import { breadcrumbService } from '../../../public/application/lib/breadcrumbs'; @@ -31,6 +31,8 @@ const { GlobalFlyoutProvider } = GlobalFlyout; const mockHttpClient = axios.create({ adapter: axiosXhrAdapter }); +export const kibanaVersion = new SemVer(MAJOR_VERSION); + export const WithAppDependencies = (Comp: any, overrides: Record = {}) => (props: Record) => { @@ -41,9 +43,9 @@ export const WithAppDependencies = http: mockHttpClient as unknown as HttpSetup, docLinks: docLinksServiceMock.createStartContract(), kibanaVersionInfo: { - currentMajor: mockKibanaSemverVersion.major, - prevMajor: mockKibanaSemverVersion.major - 1, - nextMajor: mockKibanaSemverVersion.major + 1, + currentMajor: kibanaVersion.major, + prevMajor: kibanaVersion.major - 1, + nextMajor: kibanaVersion.major + 1, }, notifications: notificationServiceMock.createStartContract(), isReadOnlyMode: false, diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.test.tsx b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.test.tsx index 0acf5ae65c6c..7831ab0110e4 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.test.tsx +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/overview.test.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import { mockKibanaSemverVersion } from '../../../common/constants'; -import { OverviewTestBed, setupOverviewPage, setupEnvironment } from '../helpers'; +import { OverviewTestBed, setupOverviewPage, setupEnvironment, kibanaVersion } from '../helpers'; describe('Overview Page', () => { let testBed: OverviewTestBed; @@ -24,7 +23,7 @@ describe('Overview Page', () => { describe('Documentation links', () => { test('Has a whatsNew link and it references nextMajor version', () => { const { exists, find } = testBed; - const nextMajor = mockKibanaSemverVersion.major + 1; + const nextMajor = kibanaVersion.major + 1; expect(exists('whatsNewLink')).toBe(true); expect(find('whatsNewLink').text()).toContain(`${nextMajor}.0`); diff --git a/x-pack/plugins/upgrade_assistant/common/config.ts b/x-pack/plugins/upgrade_assistant/common/config.ts deleted file mode 100644 index e74fe5cc1bf1..000000000000 --- a/x-pack/plugins/upgrade_assistant/common/config.ts +++ /dev/null @@ -1,20 +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 { schema, TypeOf } from '@kbn/config-schema'; - -export const configSchema = schema.object({ - enabled: schema.boolean({ defaultValue: true }), - /* - * This will default to true up until the last minor before the next major. - * In readonly mode, the user will not be able to perform any actions in the UI - * and will be presented with a message indicating as such. - */ - readonly: schema.boolean({ defaultValue: true }), -}); - -export type Config = TypeOf; diff --git a/x-pack/plugins/upgrade_assistant/common/constants.ts b/x-pack/plugins/upgrade_assistant/common/constants.ts index 893d61d32953..68a6b9e9cdb8 100644 --- a/x-pack/plugins/upgrade_assistant/common/constants.ts +++ b/x-pack/plugins/upgrade_assistant/common/constants.ts @@ -4,15 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import SemVer from 'semver/classes/semver'; - /* - * These constants are used only in tests to add conditional logic based on Kibana version * On master, the version should represent the next major version (e.g., master --> 8.0.0) * The release branch should match the release version (e.g., 7.x --> 7.0.0) */ -export const mockKibanaVersion = '8.0.0'; -export const mockKibanaSemverVersion = new SemVer(mockKibanaVersion); +export const MAJOR_VERSION = '8.0.0'; /* * Map of 7.0 --> 8.0 index setting deprecation log messages and associated settings diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx index ff11b9f1a845..d2cafd69e94e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/reindex/flyout/warning_step.test.tsx @@ -8,12 +8,20 @@ import { I18nProvider } from '@kbn/i18n/react'; import { mount, shallow } from 'enzyme'; import React from 'react'; +import SemVer from 'semver/classes/semver'; import { ReindexWarning } from '../../../../../../../common/types'; -import { mockKibanaSemverVersion } from '../../../../../../../common/constants'; +import { MAJOR_VERSION } from '../../../../../../../common/constants'; import { idForWarning, WarningsFlyoutStep } from './warnings_step'; +const kibanaVersion = new SemVer(MAJOR_VERSION); +const mockKibanaVersionInfo = { + currentMajor: kibanaVersion.major, + prevMajor: kibanaVersion.major - 1, + nextMajor: kibanaVersion.major + 1, +}; + jest.mock('../../../../../app_context', () => { const { docLinksServiceMock } = jest.requireActual( '../../../../../../../../../../src/core/public/doc_links/doc_links_service.mock' @@ -23,11 +31,7 @@ jest.mock('../../../../../app_context', () => { useAppContext: () => { return { docLinks: docLinksServiceMock.createStartContract(), - kibanaVersionInfo: { - currentMajor: mockKibanaSemverVersion.major, - prevMajor: mockKibanaSemverVersion.major - 1, - nextMajor: mockKibanaSemverVersion.major + 1, - }, + kibanaVersionInfo: mockKibanaVersionInfo, }; }, }; @@ -45,7 +49,7 @@ describe('WarningsFlyoutStep', () => { expect(shallow()).toMatchSnapshot(); }); - if (mockKibanaSemverVersion.major === 7) { + if (kibanaVersion.major === 7) { it('does not allow proceeding until all are checked', () => { const defaultPropsWithWarnings = { ...defaultProps, diff --git a/x-pack/plugins/upgrade_assistant/public/plugin.ts b/x-pack/plugins/upgrade_assistant/public/plugin.ts index 5edb638e1bc5..32e825fbdc20 100644 --- a/x-pack/plugins/upgrade_assistant/public/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/public/plugin.ts @@ -9,59 +9,69 @@ import SemVer from 'semver/classes/semver'; import { i18n } from '@kbn/i18n'; import { Plugin, CoreSetup, PluginInitializerContext } from 'src/core/public'; -import { SetupDependencies, StartDependencies, AppServicesContext } from './types'; -import { Config } from '../common/config'; +import { + SetupDependencies, + StartDependencies, + AppServicesContext, + ClientConfigType, +} from './types'; export class UpgradeAssistantUIPlugin implements Plugin { constructor(private ctx: PluginInitializerContext) {} + setup(coreSetup: CoreSetup, { management, cloud }: SetupDependencies) { - const { readonly } = this.ctx.config.get(); + const { + readonly, + ui: { enabled: isUpgradeAssistantUiEnabled }, + } = this.ctx.config.get(); - const appRegistrar = management.sections.section.stack; - const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); + if (isUpgradeAssistantUiEnabled) { + const appRegistrar = management.sections.section.stack; + const kibanaVersion = new SemVer(this.ctx.env.packageInfo.version); - const kibanaVersionInfo = { - currentMajor: kibanaVersion.major, - prevMajor: kibanaVersion.major - 1, - nextMajor: kibanaVersion.major + 1, - }; + const kibanaVersionInfo = { + currentMajor: kibanaVersion.major, + prevMajor: kibanaVersion.major - 1, + nextMajor: kibanaVersion.major + 1, + }; - const pluginName = i18n.translate('xpack.upgradeAssistant.appTitle', { - defaultMessage: '{version} Upgrade Assistant', - values: { version: `${kibanaVersionInfo.nextMajor}.0` }, - }); + const pluginName = i18n.translate('xpack.upgradeAssistant.appTitle', { + defaultMessage: '{version} Upgrade Assistant', + values: { version: `${kibanaVersionInfo.nextMajor}.0` }, + }); - appRegistrar.registerApp({ - id: 'upgrade_assistant', - title: pluginName, - order: 1, - async mount(params) { - const [coreStart, { discover, data }] = await coreSetup.getStartServices(); - const services: AppServicesContext = { discover, data, cloud }; + appRegistrar.registerApp({ + id: 'upgrade_assistant', + title: pluginName, + order: 1, + async mount(params) { + const [coreStart, { discover, data }] = await coreSetup.getStartServices(); + const services: AppServicesContext = { discover, data, cloud }; - const { - chrome: { docTitle }, - } = coreStart; + const { + chrome: { docTitle }, + } = coreStart; - docTitle.change(pluginName); + docTitle.change(pluginName); - const { mountManagementSection } = await import('./application/mount_management_section'); - const unmountAppCallback = await mountManagementSection( - coreSetup, - params, - kibanaVersionInfo, - readonly, - services - ); + const { mountManagementSection } = await import('./application/mount_management_section'); + const unmountAppCallback = await mountManagementSection( + coreSetup, + params, + kibanaVersionInfo, + readonly, + services + ); - return () => { - docTitle.reset(); - unmountAppCallback(); - }; - }, - }); + return () => { + docTitle.reset(); + unmountAppCallback(); + }; + }, + }); + } } start() {} diff --git a/x-pack/plugins/upgrade_assistant/public/types.ts b/x-pack/plugins/upgrade_assistant/public/types.ts index a2b49305c32d..cbeaf22bb095 100644 --- a/x-pack/plugins/upgrade_assistant/public/types.ts +++ b/x-pack/plugins/upgrade_assistant/public/types.ts @@ -26,3 +26,10 @@ export interface StartDependencies { discover: DiscoverStart; data: DataPublicPluginStart; } + +export interface ClientConfigType { + readonly: boolean; + ui: { + enabled: boolean; + }; +} diff --git a/x-pack/plugins/upgrade_assistant/server/config.ts b/x-pack/plugins/upgrade_assistant/server/config.ts new file mode 100644 index 000000000000..4183ea337def --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/server/config.ts @@ -0,0 +1,107 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { get } from 'lodash'; +import { schema, TypeOf } from '@kbn/config-schema'; +import { PluginConfigDescriptor } from 'src/core/server'; + +import { MAJOR_VERSION } from '../common/constants'; + +const kibanaVersion = new SemVer(MAJOR_VERSION); + +// ------------------------------- +// >= 8.x +// ------------------------------- +const schemaLatest = schema.object( + { + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + /* + * This will default to true up until the last minor before the next major. + * In readonly mode, the user will not be able to perform any actions in the UI + * and will be presented with a message indicating as such. + */ + readonly: schema.boolean({ defaultValue: true }), + }, + { defaultValue: undefined } +); + +const configLatest: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + readonly: true, + }, + schema: schemaLatest, + deprecations: () => [], +}; + +export type UpgradeAssistantConfig = TypeOf; + +// ------------------------------- +// 7.x +// ------------------------------- +const schema7x = schema.object( + { + enabled: schema.boolean({ defaultValue: true }), + ui: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + }), + /* + * This will default to true up until the last minor before the next major. + * In readonly mode, the user will not be able to perform any actions in the UI + * and will be presented with a message indicating as such. + */ + readonly: schema.boolean({ defaultValue: true }), + }, + { defaultValue: undefined } +); + +export type UpgradeAssistantConfig7x = TypeOf; + +const config7x: PluginConfigDescriptor = { + exposeToBrowser: { + ui: true, + readonly: true, + }, + schema: schema7x, + deprecations: () => [ + (completeConfig, rootPath, addDeprecation) => { + if (get(completeConfig, 'xpack.upgrade_assistant.enabled') === undefined) { + return completeConfig; + } + + addDeprecation({ + configPath: 'xpack.upgrade_assistant.enabled', + level: 'critical', + title: i18n.translate('xpack.upgradeAssistant.deprecations.enabledTitle', { + defaultMessage: 'Setting "xpack.upgrade_assistant.enabled" is deprecated', + }), + message: i18n.translate('xpack.upgradeAssistant.deprecations.enabledMessage', { + defaultMessage: + 'To disallow users from accessing the Upgrade Assistant UI, use the "xpack.upgrade_assistant.ui.enabled" setting instead of "xpack.upgrade_assistant.enabled".', + }), + correctiveActions: { + manualSteps: [ + i18n.translate('xpack.upgradeAssistant.deprecations.enabled.manualStepOneMessage', { + defaultMessage: 'Open the kibana.yml config file.', + }), + i18n.translate('xpack.upgradeAssistant.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: + 'Change the "xpack.upgrade_assistant.enabled" setting to "xpack.upgrade_assistant.ui.enabled".', + }), + ], + }, + }); + return completeConfig; + }, + ], +}; + +export const config: PluginConfigDescriptor = + kibanaVersion.major < 8 ? config7x : configLatest; diff --git a/x-pack/plugins/upgrade_assistant/server/index.ts b/x-pack/plugins/upgrade_assistant/server/index.ts index 5591276b2fa3..660aa107292e 100644 --- a/x-pack/plugins/upgrade_assistant/server/index.ts +++ b/x-pack/plugins/upgrade_assistant/server/index.ts @@ -5,18 +5,11 @@ * 2.0. */ -import { PluginInitializerContext, PluginConfigDescriptor } from 'src/core/server'; +import { PluginInitializerContext } from 'src/core/server'; import { UpgradeAssistantServerPlugin } from './plugin'; -import { configSchema, Config } from '../common/config'; + +export { config } from './config'; export const plugin = (ctx: PluginInitializerContext) => { return new UpgradeAssistantServerPlugin(ctx); }; - -export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], - schema: configSchema, - exposeToBrowser: { - readonly: true, - }, -}; diff --git a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts index d93fe7920f1d..5f39e902c75d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/__fixtures__/version.ts @@ -4,14 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { SemVer } from 'semver'; +import { MAJOR_VERSION } from '../../../common/constants'; -import { mockKibanaSemverVersion } from '../../../common/constants'; +const kibanaVersion = new SemVer(MAJOR_VERSION); export const getMockVersionInfo = () => { - const currentMajor = mockKibanaSemverVersion.major; + const currentMajor = kibanaVersion.major; return { - currentVersion: mockKibanaSemverVersion, + currentVersion: kibanaVersion, currentMajor, prevMajor: currentMajor - 1, nextMajor: currentMajor + 1, diff --git a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts index e1817ef63927..1785491e5da4 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/es_version_precheck.test.ts @@ -9,7 +9,7 @@ import { SemVer } from 'semver'; import { IScopedClusterClient, kibanaResponseFactory } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { licensingMock } from '../../../../plugins/licensing/server/mocks'; -import { mockKibanaVersion } from '../../common/constants'; +import { MAJOR_VERSION } from '../../common/constants'; import { getMockVersionInfo } from './__fixtures__/version'; import { @@ -98,7 +98,7 @@ describe('verifyAllMatchKibanaVersion', () => { describe('EsVersionPrecheck', () => { beforeEach(() => { - versionService.setup(mockKibanaVersion); + versionService.setup(MAJOR_VERSION); }); it('returns a 403 when callCluster fails with a 403', async () => { diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts index 30093a9fb6e5..957198cde8da 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/index_settings.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { mockKibanaSemverVersion, mockKibanaVersion } from '../../../common/constants'; +import { MAJOR_VERSION } from '../../../common/constants'; import { versionService } from '../version'; import { getMockVersionInfo } from '../__fixtures__/version'; @@ -131,7 +131,7 @@ describe('transformFlatSettings', () => { describe('sourceNameForIndex', () => { beforeEach(() => { - versionService.setup(mockKibanaVersion); + versionService.setup(MAJOR_VERSION); }); it('parses internal indices', () => { @@ -152,7 +152,7 @@ describe('transformFlatSettings', () => { describe('generateNewIndexName', () => { beforeEach(() => { - versionService.setup(mockKibanaVersion); + versionService.setup(MAJOR_VERSION); }); it('parses internal indices', () => { @@ -186,7 +186,7 @@ describe('transformFlatSettings', () => { ).toEqual([]); }); - if (mockKibanaSemverVersion.major === 7) { + if (currentMajor === 7) { describe('[7.x] customTypeName warning', () => { it('returns customTypeName warning for non-_doc mapping types', () => { expect( diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts index 3cfdb1fdd316..ce1e8e11eb2d 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_actions.test.ts @@ -19,7 +19,7 @@ import { ReindexStatus, ReindexStep, } from '../../../common/types'; -import { mockKibanaVersion } from '../../../common/constants'; +import { MAJOR_VERSION } from '../../../common/constants'; import { versionService } from '../version'; import { LOCK_WINDOW, ReindexActions, reindexActionsFactory } from './reindex_actions'; import { getMockVersionInfo } from '../__fixtures__/version'; @@ -54,7 +54,7 @@ describe('ReindexActions', () => { describe('createReindexOp', () => { beforeEach(() => { - versionService.setup(mockKibanaVersion); + versionService.setup(MAJOR_VERSION); client.create.mockResolvedValue(); }); diff --git a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts index 7a5bf1c18769..6017691a9328 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/reindexing/reindex_service.test.ts @@ -20,7 +20,7 @@ import { ReindexStatus, ReindexStep, } from '../../../common/types'; -import { mockKibanaVersion } from '../../../common/constants'; +import { MAJOR_VERSION } from '../../../common/constants'; import { licensingMock } from '../../../../licensing/server/mocks'; import { LicensingPluginSetup } from '../../../../licensing/server'; @@ -89,7 +89,7 @@ describe('reindexService', () => { licensingPluginSetup ); - versionService.setup(mockKibanaVersion); + versionService.setup(MAJOR_VERSION); }); describe('hasRequiredPrivileges', () => { From c1b0565acdbbcf7432a46a0664a91c34f299dab3 Mon Sep 17 00:00:00 2001 From: Tre Date: Tue, 19 Oct 2021 06:56:35 -0400 Subject: [PATCH 35/53] [QA][refactor] Use ui settings - sample data (#114530) --- test/functional/apps/home/_sample_data.ts | 21 +++++++++------------ test/functional/page_objects/common_page.ts | 9 +++++++++ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/test/functional/apps/home/_sample_data.ts b/test/functional/apps/home/_sample_data.ts index 3cf387133bc9..e0a96940337e 100644 --- a/test/functional/apps/home/_sample_data.ts +++ b/test/functional/apps/home/_sample_data.ts @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { after(async () => { await security.testUser.restoreDefaults(); + await PageObjects.common.unsetTime(); }); it('should display registered flights sample data sets', async () => { @@ -74,6 +75,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('dashboard', () => { beforeEach(async () => { + await time(); await PageObjects.common.navigateToUrl('home', '/tutorial_directory/sampleData', { useActualUrl: true, }); @@ -84,10 +86,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.home.launchSampleDashboard('flights'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const todayYearMonthDay = moment().format('MMM D, YYYY'); - const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; - const toTime = `${todayYearMonthDay} @ 23:59:59.999`; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(17); }); @@ -112,10 +110,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.home.launchSampleDashboard('logs'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const todayYearMonthDay = moment().format('MMM D, YYYY'); - const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; - const toTime = `${todayYearMonthDay} @ 23:59:59.999`; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(13); }); @@ -124,10 +118,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.home.launchSampleDashboard('ecommerce'); await PageObjects.header.waitUntilLoadingHasFinished(); await renderable.waitForRender(); - const todayYearMonthDay = moment().format('MMM D, YYYY'); - const fromTime = `${todayYearMonthDay} @ 00:00:00.000`; - const toTime = `${todayYearMonthDay} @ 23:59:59.999`; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); const panelCount = await PageObjects.dashboard.getPanelCount(); expect(panelCount).to.be(15); }); @@ -160,5 +150,12 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(isInstalled).to.be(false); }); }); + + async function time() { + const today = moment().format('MMM D, YYYY'); + const from = `${today} @ 00:00:00.000`; + const to = `${today} @ 23:59:59.999`; + await PageObjects.common.setTime({ from, to }); + } }); } diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 64fb184f40e4..a40465b00dbe 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -30,6 +30,7 @@ export class CommonPageObject extends FtrService { private readonly globalNav = this.ctx.getService('globalNav'); private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly loginPage = this.ctx.getPageObject('login'); + private readonly kibanaServer = this.ctx.getService('kibanaServer'); private readonly defaultTryTimeout = this.config.get('timeouts.try'); private readonly defaultFindTimeout = this.config.get('timeouts.find'); @@ -500,4 +501,12 @@ export class CommonPageObject extends FtrService { await this.testSubjects.exists(validator); } } + + async setTime(time: { from: string; to: string }) { + await this.kibanaServer.uiSettings.replace({ 'timepicker:timeDefaults': JSON.stringify(time) }); + } + + async unsetTime() { + await this.kibanaServer.uiSettings.unset('timepicker:timeDefaults'); + } } From f8041e6005a10b73fd771b9b8e2c8d9a22bfce84 Mon Sep 17 00:00:00 2001 From: Pete Harverson Date: Tue, 19 Oct 2021 11:57:10 +0100 Subject: [PATCH 36/53] [ML] Delete annotation directly from the index it is stored in (#115328) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ml/common/constants/index_patterns.ts | 1 - .../ml/server/lib/check_annotations/index.ts | 11 ++----- .../annotation_service/annotation.test.ts | 3 +- .../models/annotation_service/annotation.ts | 33 ++++++++++++++++--- 4 files changed, 32 insertions(+), 16 deletions(-) diff --git a/x-pack/plugins/ml/common/constants/index_patterns.ts b/x-pack/plugins/ml/common/constants/index_patterns.ts index d7d6c343e282..9a8e5c1b8ae7 100644 --- a/x-pack/plugins/ml/common/constants/index_patterns.ts +++ b/x-pack/plugins/ml/common/constants/index_patterns.ts @@ -7,7 +7,6 @@ export const ML_ANNOTATIONS_INDEX_ALIAS_READ = '.ml-annotations-read'; export const ML_ANNOTATIONS_INDEX_ALIAS_WRITE = '.ml-annotations-write'; -export const ML_ANNOTATIONS_INDEX_PATTERN = '.ml-annotations-6'; export const ML_RESULTS_INDEX_PATTERN = '.ml-anomalies-*'; export const ML_NOTIFICATION_INDEX_PATTERN = '.ml-notifications*'; diff --git a/x-pack/plugins/ml/server/lib/check_annotations/index.ts b/x-pack/plugins/ml/server/lib/check_annotations/index.ts index a388a24d082a..e64b4658588c 100644 --- a/x-pack/plugins/ml/server/lib/check_annotations/index.ts +++ b/x-pack/plugins/ml/server/lib/check_annotations/index.ts @@ -11,22 +11,15 @@ import { mlLog } from '../../lib/log'; import { ML_ANNOTATIONS_INDEX_ALIAS_READ, ML_ANNOTATIONS_INDEX_ALIAS_WRITE, - ML_ANNOTATIONS_INDEX_PATTERN, } from '../../../common/constants/index_patterns'; // Annotations Feature is available if: -// - ML_ANNOTATIONS_INDEX_PATTERN index is present // - ML_ANNOTATIONS_INDEX_ALIAS_READ alias is present // - ML_ANNOTATIONS_INDEX_ALIAS_WRITE alias is present +// Note there is no need to check for the existence of the indices themselves as aliases are stored +// in the metadata of the indices they point to, so it's impossible to have an alias that doesn't point to any index. export async function isAnnotationsFeatureAvailable({ asInternalUser }: IScopedClusterClient) { try { - const indexParams = { index: ML_ANNOTATIONS_INDEX_PATTERN }; - - const { body: annotationsIndexExists } = await asInternalUser.indices.exists(indexParams); - if (!annotationsIndexExists) { - return false; - } - const { body: annotationsReadAliasExists } = await asInternalUser.indices.existsAlias({ index: ML_ANNOTATIONS_INDEX_ALIAS_READ, name: ML_ANNOTATIONS_INDEX_ALIAS_READ, diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts index 725e0ac49494..975070e92a7e 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.test.ts @@ -9,7 +9,6 @@ import getAnnotationsRequestMock from './__mocks__/get_annotations_request.json' import getAnnotationsResponseMock from './__mocks__/get_annotations_response.json'; import { ANNOTATION_TYPE } from '../../../common/constants/annotations'; -import { ML_ANNOTATIONS_INDEX_ALIAS_WRITE } from '../../../common/constants/index_patterns'; import { Annotation, isAnnotations } from '../../../common/types/annotations'; import { DeleteParams, GetResponse, IndexAnnotationArgs } from './annotation'; @@ -42,7 +41,7 @@ describe('annotation_service', () => { const annotationMockId = 'mockId'; const deleteParamsMock: DeleteParams = { - index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + index: '.ml-annotations-6', id: annotationMockId, refresh: 'wait_for', }; diff --git a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts index c6ed72de18d0..5807d181cc56 100644 --- a/x-pack/plugins/ml/server/models/annotation_service/annotation.ts +++ b/x-pack/plugins/ml/server/models/annotation_service/annotation.ts @@ -71,6 +71,7 @@ export interface IndexParams { index: string; body: Annotation; refresh: boolean | 'wait_for' | undefined; + require_alias?: boolean; id?: string; } @@ -99,6 +100,7 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) { index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, body: annotation, refresh: 'wait_for', + require_alias: true, }; if (typeof annotation._id !== 'undefined') { @@ -407,14 +409,37 @@ export function annotationProvider({ asInternalUser }: IScopedClusterClient) { } async function deleteAnnotation(id: string) { - const params: DeleteParams = { - index: ML_ANNOTATIONS_INDEX_ALIAS_WRITE, + // Find the index the annotation is stored in. + const searchParams: estypes.SearchRequest = { + index: ML_ANNOTATIONS_INDEX_ALIAS_READ, + size: 1, + body: { + query: { + ids: { + values: [id], + }, + }, + }, + }; + + const { body } = await asInternalUser.search(searchParams); + const totalCount = + typeof body.hits.total === 'number' ? body.hits.total : body.hits.total.value; + + if (totalCount === 0) { + throw Boom.notFound(`Cannot find annotation with ID ${id}`); + } + + const index = body.hits.hits[0]._index; + + const deleteParams: DeleteParams = { + index, id, refresh: 'wait_for', }; - const { body } = await asInternalUser.delete(params); - return body; + const { body: deleteResponse } = await asInternalUser.delete(deleteParams); + return deleteResponse; } return { From 0e5f2524b46da0fe147b4726b741c38283789ed7 Mon Sep 17 00:00:00 2001 From: Diana Derevyankina <54894989+DziyanaDzeraviankina@users.noreply.github.com> Date: Tue, 19 Oct 2021 15:08:05 +0300 Subject: [PATCH 37/53] Respect external URL allow list in TSVB (#114093) * Respect external URL allow list in TSVB * Remove showExternalUrlErrorModal and onContextMenu handler for table * Update modal message Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../lib/external_url_error_modal.tsx | 60 +++++++++++++++++++ .../components/vis_types/table/vis.js | 57 ++++++++++++++---- .../components/vis_types/top_n/vis.js | 21 ++++++- 3 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx diff --git a/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx b/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx new file mode 100644 index 000000000000..ebb806387d9c --- /dev/null +++ b/src/plugins/vis_types/timeseries/public/application/components/lib/external_url_error_modal.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiTextColor, +} from '@elastic/eui'; + +interface ExternalUrlErrorModalProps { + url: string; + handleClose: () => void; +} + +export const ExternalUrlErrorModal = ({ url, handleClose }: ExternalUrlErrorModalProps) => ( + + + + + + + + + {url} + + ), + externalUrlPolicy: 'externalUrl.policy', + kibanaConfigFileName: 'kibana.yml', + }} + /> + + + + + + + +); diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js index 7b1db4b36264..b3a48a997b30 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/table/vis.js @@ -17,6 +17,7 @@ import { createFieldFormatter } from '../../lib/create_field_formatter'; import { isSortable } from './is_sortable'; import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { replaceVars } from '../../lib/replace_vars'; +import { ExternalUrlErrorModal } from '../../lib/external_url_error_modal'; import { FIELD_FORMAT_IDS } from '../../../../../../../../plugins/field_formats/common'; import { FormattedMessage } from '@kbn/i18n/react'; import { getFieldFormats, getCoreStart } from '../../../../services'; @@ -53,12 +54,26 @@ class TableVis extends Component { const DateFormat = fieldFormatsService.getType(FIELD_FORMAT_IDS.DATE); this.dateFormatter = new DateFormat({}, this.props.getConfig); + + this.state = { + accessDeniedDrilldownUrl: null, + }; } get visibleSeries() { return get(this.props, 'model.series', []).filter((series) => !series.hidden); } + createDrilldownUrlClickHandler = (url) => (event) => { + const validatedUrl = getCoreStart().http.externalUrl.validateUrl(url); + if (validatedUrl) { + this.setState({ accessDeniedDrilldownUrl: null }); + } else { + event.preventDefault(); + this.setState({ accessDeniedDrilldownUrl: url }); + } + }; + renderRow = (row) => { const { model, fieldFormatMap, getConfig } = this.props; @@ -74,7 +89,16 @@ class TableVis extends Component { if (model.drilldown_url) { const url = replaceVars(model.drilldown_url, {}, { key: row.key }); - rowDisplay = {rowDisplay}; + const handleDrilldownUrlClick = this.createDrilldownUrlClickHandler(url); + rowDisplay = ( + + {rowDisplay} + + ); } const columns = row.series @@ -213,8 +237,11 @@ class TableVis extends Component { ); } + closeExternalUrlErrorModal = () => this.setState({ accessDeniedDrilldownUrl: null }); + render() { const { visData, model } = this.props; + const { accessDeniedDrilldownUrl } = this.state; const header = this.renderHeader(); let rows; @@ -239,16 +266,24 @@ class TableVis extends Component { ); } return ( - - - {header} - {rows} -
-
+ <> + + + {header} + {rows} +
+
+ {accessDeniedDrilldownUrl && ( + + )} + ); } } diff --git a/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js index 8176f6ece280..5eb850a75338 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_types/timeseries/public/application/components/vis_types/top_n/vis.js @@ -15,10 +15,11 @@ import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; -import React from 'react'; +import React, { useState, useCallback } from 'react'; import { sortBy, first, get } from 'lodash'; import { DATA_FORMATTERS } from '../../../../../common/enums'; import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; +import { ExternalUrlErrorModal } from '../../lib/external_url_error_modal'; function sortByDirection(data, direction, fn) { if (direction === 'desc') { @@ -41,6 +42,8 @@ function sortSeries(visData, model) { } function TopNVisualization(props) { + const [accessDeniedDrilldownUrl, setAccessDeniedDrilldownUrl] = useState(null); + const coreStart = getCoreStart(); const { backgroundColor, model, visData, fieldFormatMap, getConfig } = props; const series = sortSeries(visData, model).map((item) => { @@ -83,13 +86,27 @@ function TopNVisualization(props) { if (model.drilldown_url) { params.onClick = (item) => { const url = replaceVars(model.drilldown_url, {}, { key: item.label }); - getCoreStart().application.navigateToUrl(url); + const validatedUrl = coreStart.http.externalUrl.validateUrl(url); + if (validatedUrl) { + setAccessDeniedDrilldownUrl(null); + coreStart.application.navigateToUrl(url); + } else { + setAccessDeniedDrilldownUrl(url); + } }; } + const closeExternalUrlErrorModal = useCallback(() => setAccessDeniedDrilldownUrl(null), []); + return (
+ {accessDeniedDrilldownUrl && ( + + )}
); } From 340271fba271dab844d0c535d2dd685233535705 Mon Sep 17 00:00:00 2001 From: Kevin Qualters <56408403+kqualters-elastic@users.noreply.github.com> Date: Tue, 19 Oct 2021 08:34:15 -0400 Subject: [PATCH 38/53] [Security Solution] Analyze event moved outside of overflow popover (#115478) --- .../timeline_actions/alert_context_menu.tsx | 16 +----- .../timeline/body/actions/index.test.tsx | 29 ++++++++++ .../timeline/body/actions/index.tsx | 54 ++++++++++++++++++- .../__snapshots__/index.test.tsx.snap | 2 +- .../timeline/body/control_columns/index.tsx | 2 +- .../components/timeline/body/helpers.tsx | 6 ++- 6 files changed, 90 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index 06d61b3f0b28..a9b6eabecff8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -34,7 +34,6 @@ import { useExceptionActions } from './use_add_exception_actions'; import { useEventFilterModal } from './use_event_filter_modal'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { useKibana } from '../../../../common/lib/kibana'; -import { useInvestigateInResolverContextItem } from './investigate_in_resolver'; import { ATTACH_ALERT_TO_CASE_FOR_ROW } from '../../../../timelines/components/timeline/body/translations'; import { useEventFilterAction } from './use_event_filter_action'; import { useAddToCaseActions } from './use_add_to_case_actions'; @@ -163,30 +162,19 @@ const AlertContextMenuComponent: React.FC !isEvent && ruleId - ? [ - ...investigateInResolverActionItems, - ...addToCaseActionItems, - ...statusActionItems, - ...exceptionActionItems, - ] - : [...investigateInResolverActionItems, ...addToCaseActionItems, ...eventFilterActionItems], + ? [...addToCaseActionItems, ...statusActionItems, ...exceptionActionItems] + : [...addToCaseActionItems, ...eventFilterActionItems], [ statusActionItems, addToCaseActionItems, eventFilterActionItems, exceptionActionItems, - investigateInResolverActionItems, isEvent, ruleId, ] diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 5ed9398a621e..1da09bcf4e25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -185,5 +185,34 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(false); }); + test('it shows the analyze event button when the event is from an endpoint', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['endpoint'] }, + process: { entity_id: ['1'] }, + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(true); + }); + test('it does not render the analyze event button when the event is from an unsupported source', () => { + const ecsData = { + ...mockTimelineData[0].ecs, + event: { kind: ['alert'] }, + agent: { type: ['notendpoint'] }, + }; + const wrapper = mount( + + + + ); + + expect(wrapper.find('[data-test-subj="view-in-analyzer"]').exists()).toBe(false); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 73650bd320f3..c4dae739cb25 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -21,9 +21,23 @@ import { EventsTdContent } from '../../styles'; import * as i18n from '../translations'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../helpers'; import { useShallowEqualSelector } from '../../../../../common/hooks/use_selector'; -import { TimelineId, ActionProps, OnPinEvent } from '../../../../../../common/types/timeline'; +import { + setActiveTabTimeline, + updateTimelineGraphEventId, +} from '../../../../store/timeline/actions'; +import { + useGlobalFullScreen, + useTimelineFullScreen, +} from '../../../../../common/containers/use_full_screen'; +import { + TimelineId, + ActionProps, + OnPinEvent, + TimelineTabs, +} from '../../../../../../common/types/timeline'; import { timelineActions, timelineSelectors } from '../../../../store/timeline'; import { timelineDefaults } from '../../../../store/timeline/defaults'; +import { isInvestigateInResolverActionEnabled } from '../../../../../detections/components/alerts_table/timeline_actions/investigate_in_resolver'; const ActionsContainer = styled.div` align-items: center; @@ -100,6 +114,24 @@ const ActionsComponent: React.FC = ({ [eventType, ecsData.event?.kind, ecsData.agent?.type] ); + const isDisabled = useMemo(() => !isInvestigateInResolverActionEnabled(ecsData), [ecsData]); + const { setGlobalFullScreen } = useGlobalFullScreen(); + const { setTimelineFullScreen } = useTimelineFullScreen(); + const handleClick = useCallback(() => { + const dataGridIsFullScreen = document.querySelector('.euiDataGrid--fullScreen'); + dispatch(updateTimelineGraphEventId({ id: timelineId, graphEventId: ecsData._id })); + if (timelineId === TimelineId.active) { + if (dataGridIsFullScreen) { + setTimelineFullScreen(true); + } + dispatch(setActiveTabTimeline({ id: timelineId, activeTab: TimelineTabs.graph })); + } else { + if (dataGridIsFullScreen) { + setGlobalFullScreen(true); + } + } + }, [dispatch, ecsData._id, timelineId, setGlobalFullScreen, setTimelineFullScreen]); + return ( {showCheckboxes && !tGridEnabled && ( @@ -171,6 +203,26 @@ const ActionsComponent: React.FC = ({ refetch={refetch ?? noop} onRuleChange={onRuleChange} /> + {isDisabled === false ? ( +
+ + + + + +
+ ) : null}
); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap index 6bc2dc089494..25d5104a98d9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/column_headers/__snapshots__/index.test.tsx.snap @@ -521,7 +521,7 @@ exports[`ColumnHeaders rendering renders correctly against snapshot 1`] = ` "compare": null, "type": [Function], }, - "width": 108, + "width": 140, }, ] } diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx index d38bf2136513..2cdc8d5f4e28 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/control_columns/index.tsx @@ -9,7 +9,7 @@ import { ControlColumnProps } from '../../../../../../common/types/timeline'; import { Actions } from '../actions'; import { HeaderActions } from '../actions/header_actions'; -const DEFAULT_CONTROL_COLUMN_WIDTH = 108; +const DEFAULT_CONTROL_COLUMN_WIDTH = 140; export const defaultControlColumn: ControlColumnProps = { id: 'default-timeline-control-column', diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx index 5b993110d38b..7032319b5933 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/helpers.tsx @@ -17,8 +17,10 @@ import { import { OnPinEvent, OnUnPinEvent } from '../events'; import * as i18n from './translations'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export const omitTypenameAndEmpty = (k: string, v: any): any | undefined => +export const omitTypenameAndEmpty = ( + k: string, + v: string | object | Array +): string | object | Array | undefined => k !== '__typename' && v != null ? v : undefined; export const stringifyEvent = (ecs: Ecs): string => JSON.stringify(ecs, omitTypenameAndEmpty, 2); From b402bea0658a82d028a3e5c55df4a907a18ab326 Mon Sep 17 00:00:00 2001 From: Robert Austin Date: Tue, 19 Oct 2021 09:33:46 -0400 Subject: [PATCH 39/53] Disable the experimental `metrics_entities` plugin by default. (#115460) This was default disabled in 7.15, but we needed a code change to maintain that (consistent) behavior. --- x-pack/plugins/metrics_entities/server/index.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/metrics_entities/server/index.ts b/x-pack/plugins/metrics_entities/server/index.ts index e61dc8b7dc64..bb80ac8e8be7 100644 --- a/x-pack/plugins/metrics_entities/server/index.ts +++ b/x-pack/plugins/metrics_entities/server/index.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { schema } from '@kbn/config-schema'; + import { PluginInitializerContext } from '../../../../src/core/server'; import { MetricsEntitiesPlugin } from './plugin'; @@ -17,3 +19,10 @@ export const plugin = (initializerContext: PluginInitializerContext): MetricsEnt }; export { MetricsEntitiesPluginSetup, MetricsEntitiesPluginStart } from './types'; + +export const config = { + schema: schema.object({ + // This plugin is experimental and should be disabled by default. + enabled: schema.boolean({ defaultValue: false }), + }), +}; From e5a918dc7dad47e32bd9abf6b056e99ffaf0db18 Mon Sep 17 00:00:00 2001 From: Garrett Spong Date: Tue, 19 Oct 2021 07:43:43 -0600 Subject: [PATCH 40/53] [SecuritySolution][Detections] Enables Index Action and Connector for Detection Actions (#111813) ## Summary This PR enables the [Index Connector and Action](https://www.elastic.co/guide/en/kibana/master/index-action-type.html) for the detection engine, addressing https://github.com/elastic/kibana/issues/110550.
Action type available in list:

No Connector UI:

Create Connector UI:

Connector Template:

``` json { "rule_id": "{{context.rule.id}}", "rule_name": "{{context.rule.name}}", "alert_id": "{{alert.id}}", "context_message": "Threshold Results: {{#context.alerts}}{{#signal.threshold_result.terms}}{{value}}, {{/signal.threshold_result.terms}}{{/context.alerts}}" } ```

Documents successfully written:

--- If wanting to store the alert index timestamp, create index first with `timestamp` field and use `Define timefield for each document` option: ``` PUT .homemade-alerts-index { "mappings" : { "dynamic": "true", "properties" : { "@timestamp": { "type": "date" } } } } ```

### Checklist Delete any items that are not applicable to this PR. - [ ] [Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html) was added for features that require explanation or tutorials (will need to update documentation if we proceed with this PR) --- x-pack/plugins/security_solution/common/constants.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 5a7e19e2cdd0..6e8d574c1586 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -296,15 +296,16 @@ export const ML_GROUP_IDS = [ML_GROUP_ID, LEGACY_ML_GROUP_ID]; */ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', - '.slack', + '.index', + '.jira', '.pagerduty', - '.swimlane', - '.webhook', + '.resilient', '.servicenow', '.servicenow-sir', - '.jira', - '.resilient', + '.slack', + '.swimlane', '.teams', + '.webhook', ]; if (ENABLE_CASE_CONNECTOR) { From 5a0002fae834965e7924b448b1036ff91cbc698d Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 19 Oct 2021 09:51:55 -0400 Subject: [PATCH 41/53] [App Search] Update "overrides" badge (#115437) --- .../components/curations/components/suggestions_table.test.tsx | 2 +- .../components/curations/components/suggestions_table.tsx | 2 +- .../applications/app_search/components/curations/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx index 28c368d942c1..439f9dabadee 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.test.tsx @@ -88,7 +88,7 @@ describe('SuggestionsTable', () => { wrapper = renderColumn(0)('test', {}); expect(wrapper.find(EuiBadge)).toHaveLength(0); - wrapper = renderColumn(0)('test', { override_curation_id: '1-2-3' }); + wrapper = renderColumn(0)('test', { override_manual_curation: true }); expect(wrapper.find(EuiBadge).prop('children')).toEqual('Overrides'); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx index b7a731c1654c..c91468e67529 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/components/suggestions_table.tsx @@ -42,7 +42,7 @@ const columns: Array> = [ render: (query: string, curation: CurationSuggestion) => ( {query} - {curation.override_curation_id && ( + {curation.override_manual_curation && ( <> {i18n.translate( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts index f00da5deec7e..7479505ea86d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/types.ts @@ -15,7 +15,7 @@ export interface CurationSuggestion { status: 'pending' | 'applied' | 'automated' | 'rejected' | 'disabled'; curation_id?: string; operation: 'create' | 'update' | 'delete'; - override_curation_id?: string; + override_manual_curation?: boolean; } export interface Curation { From 44d0150ae19ed3f01dca913285f834eccede3dd1 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 19 Oct 2021 15:58:59 +0200 Subject: [PATCH 42/53] :bug: Fix single percentile case when ES is returning no buckets (#115214) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/aggs/metrics/single_percentile.test.ts | 5 +++++ .../data/common/search/aggs/metrics/single_percentile.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts index c2ba6ee1a403..967e1b1f624a 100644 --- a/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.test.ts @@ -73,6 +73,11 @@ describe('AggTypeMetricSinglePercentileProvider class', () => { ).toEqual(123); }); + it('should not throw error for empty buckets', () => { + const agg = aggConfigs.getResponseAggs()[0]; + expect(agg.getValue({})).toEqual(NaN); + }); + it('produces the expected expression ast', () => { const agg = aggConfigs.getResponseAggs()[0]; expect(agg.toExpressionAst()).toMatchInlineSnapshot(` diff --git a/src/plugins/data/common/search/aggs/metrics/single_percentile.ts b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts index 4bdafcae327c..954576e2bbe1 100644 --- a/src/plugins/data/common/search/aggs/metrics/single_percentile.ts +++ b/src/plugins/data/common/search/aggs/metrics/single_percentile.ts @@ -57,7 +57,9 @@ export const getSinglePercentileMetricAgg = () => { if (Number.isInteger(agg.params.percentile)) { valueKey += '.0'; } - return bucket[agg.id].values[valueKey]; + const { values } = bucket[agg.id] ?? {}; + + return values ? values[valueKey] : NaN; }, }); }; From 7420cc228c3462b9b5b3ba34fd18166e54718d8f Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 19 Oct 2021 09:59:29 -0400 Subject: [PATCH 43/53] [Fleet] Add telemetry for integration cards (#115413) --- x-pack/plugins/fleet/kibana.json | 2 +- .../applications/integrations/index.tsx | 23 +++++---- .../sections/epm/components/package_card.tsx | 47 +++++++++++-------- .../epm/screens/home/available_packages.tsx | 2 +- .../sections/epm/screens/home/index.tsx | 2 +- x-pack/plugins/fleet/public/plugin.ts | 16 ++++++- 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/x-pack/plugins/fleet/kibana.json b/x-pack/plugins/fleet/kibana.json index 9de538ee91b8..1ca88cac1cc1 100644 --- a/x-pack/plugins/fleet/kibana.json +++ b/x-pack/plugins/fleet/kibana.json @@ -11,5 +11,5 @@ "requiredPlugins": ["licensing", "data", "encryptedSavedObjects", "navigation", "customIntegrations", "share"], "optionalPlugins": ["security", "features", "cloud", "usageCollection", "home", "globalSearch"], "extraPublicDirs": ["common"], - "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils"] + "requiredBundles": ["kibanaReact", "esUiShared", "home", "infra", "kibanaUtils", "usageCollection"] } diff --git a/x-pack/plugins/fleet/public/applications/integrations/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/index.tsx index 0abb78f85007..4099879538af 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/index.tsx @@ -70,18 +70,21 @@ export function renderApp( { element, appBasePath, history, setHeaderActionMenu }: AppMountParameters, config: FleetConfigType, kibanaVersion: string, - extensions: UIExtensionsStorage + extensions: UIExtensionsStorage, + UsageTracker: React.FC ) { ReactDOM.render( - , + + + , element ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 091eb4c97183..7181241776dd 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -9,6 +9,8 @@ import React from 'react'; import styled from 'styled-components'; import { EuiCard, EuiFlexItem, EuiBadge, EuiToolTip, EuiSpacer } from '@elastic/eui'; +import { TrackApplicationView } from '../../../../../../../../../src/plugins/usage_collection/public'; + import { CardIcon } from '../../../../../components/package_icon'; import type { IntegrationCardItem } from '../../../../../../common/types/models/epm'; @@ -31,6 +33,7 @@ export function PackageCard({ integration, url, release, + id, }: PackageCardProps) { let releaseBadge: React.ReactNode | null = null; @@ -47,26 +50,30 @@ export function PackageCard({ ); } + const testid = `integration-card:${id}`; return ( - - } - href={url} - target={url.startsWith('http') || url.startsWith('https') ? '_blank' : undefined} - > - {releaseBadge} - + + + } + href={url} + target={url.startsWith('http') || url.startsWith('https') ? '_blank' : undefined} + > + {releaseBadge} + + ); } diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index f5c521ebacf1..73de0e51bea6 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -79,7 +79,7 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { const allCategories = [...topCategories, ...categories]; return { ...restOfPackage, - id: `${restOfPackage}-${name}`, + id: `${restOfPackage.id}-${name}`, integration: name, title, description, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 4270d360b929..e3fc5e15488e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -74,7 +74,7 @@ export const mapToCard = ( } return { - id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}-${item.id}`, + id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}:${item.id}`, description: item.description, icons: !item.icons || !item.icons.length ? [] : item.icons, title: item.title, diff --git a/x-pack/plugins/fleet/public/plugin.ts b/x-pack/plugins/fleet/public/plugin.ts index 4a2a6900cc78..b0e4e56aa344 100644 --- a/x-pack/plugins/fleet/public/plugin.ts +++ b/x-pack/plugins/fleet/public/plugin.ts @@ -5,6 +5,7 @@ * 2.0. */ +import React from 'react'; import type { AppMountParameters, CoreSetup, @@ -23,6 +24,8 @@ import type { import type { SharePluginStart } from 'src/plugins/share/public'; +import type { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; + import { DEFAULT_APP_CATEGORIES, AppNavLinkStatus } from '../../../../src/core/public'; import type { @@ -73,6 +76,7 @@ export interface FleetSetupDeps { cloud?: CloudSetup; globalSearch?: GlobalSearchPluginSetup; customIntegrations: CustomIntegrationsSetup; + usageCollection?: UsageCollectionSetup; } export interface FleetStartDeps { @@ -137,7 +141,17 @@ export class FleetPlugin implements Plugin { unmount(); From 1a917674a4569c2f61b567d2096827c385000bca Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 19 Oct 2021 10:07:45 -0400 Subject: [PATCH 44/53] [Security Solution] [Platform] Migrate legacy actions whenever user interacts with the rule (#115101) Migrate legacy actions whenever user interacts with the rule (#115101) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../rules/add_prepackaged_rules_route.ts | 1 + .../routes/rules/import_rules_route.ts | 9 ++- .../routes/rules/patch_rules_bulk_route.ts | 10 ++- .../routes/rules/patch_rules_route.ts | 10 ++- .../rules/update_rules_bulk_route.test.ts | 2 + .../routes/rules/update_rules_bulk_route.ts | 16 +++++ .../routes/rules/update_rules_route.test.ts | 2 +- .../routes/rules/update_rules_route.ts | 16 +++++ .../rules/patch_rules.mock.ts | 3 + .../lib/detection_engine/rules/patch_rules.ts | 7 ++- .../lib/detection_engine/rules/types.ts | 17 ++++- .../rules/update_prepacked_rules.test.ts | 4 ++ .../rules/update_prepacked_rules.ts | 14 ++++- .../rules/update_rules.mock.ts | 3 + .../detection_engine/rules/update_rules.ts | 20 +++++- .../lib/detection_engine/rules/utils.ts | 62 +++++++++++++++++++ 16 files changed, 183 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts index fed34743e220..ddf4e956beac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/add_prepackaged_rules_route.ts @@ -171,6 +171,7 @@ export const createPrepackagedRules = async ( ); await updatePrepackagedRules( rulesClient, + savedObjectsClient, context.securitySolution.getSpaceId(), ruleStatusClient, rulesToUpdate, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 8269fe8b3613..b09ef1a21574 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -40,6 +40,7 @@ import { } from '../utils'; import { patchRules } from '../../rules/patch_rules'; +import { legacyMigrate } from '../../rules/utils'; import { getTupleDuplicateErrorsAndUniqueRules } from './utils'; import { createRulesStreamFromNdJson } from '../../rules/create_rules_stream_from_ndjson'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; @@ -271,8 +272,14 @@ export const importRulesRoute = ( status_code: 200, }); } else if (rule != null && request.query.overwrite) { + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule, + }); await patchRules({ rulesClient, + savedObjectsClient, author, buildingBlockType, spaceId: context.securitySolution.getSpaceId(), @@ -291,7 +298,7 @@ export const importRulesRoute = ( timelineTitle, meta, filters, - rule, + rule: migratedRule, index, interval, maxSignals, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts index 67d68221d846..2b514ba91109 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_bulk_route.ts @@ -24,6 +24,7 @@ import { transformValidateBulkError } from './validate'; import { patchRules } from '../../rules/patch_rules'; import { readRules } from '../../rules/read_rules'; import { PartialFilter } from '../../types'; +import { legacyMigrate } from '../../rules/utils'; export const patchRulesBulkRoute = ( router: SecuritySolutionPluginRouter, @@ -133,9 +134,16 @@ export const patchRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); } - const rule = await patchRules({ + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, rule: existingRule, + }); + + const rule = await patchRules({ + rule: migratedRule, rulesClient, + savedObjectsClient, author, buildingBlockType, description, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts index cf140f22289d..0096cd2e3818 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/patch_rules_route.ts @@ -24,6 +24,7 @@ import { buildSiemResponse } from '../utils'; import { getIdError } from './utils'; import { transformValidate } from './validate'; import { readRules } from '../../rules/read_rules'; +import { legacyMigrate } from '../../rules/utils'; import { PartialFilter } from '../../types'; export const patchRulesRoute = ( @@ -134,8 +135,15 @@ export const patchRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(existingRule?.params.type)); } + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule: existingRule, + }); + const rule = await patchRules({ rulesClient, + savedObjectsClient, author, buildingBlockType, description, @@ -154,7 +162,7 @@ export const patchRulesRoute = ( timelineTitle, meta, filters, - rule: existingRule, + rule: migratedRule, index, interval, maxSignals, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts index f7bef76944a9..22e8f6543eb7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.test.ts @@ -41,6 +41,8 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); + clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); + updateRulesBulkRoute(server.router, ml, isRuleRegistryEnabled); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts index 6138690070b6..d8b7e8cb2b72 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_bulk_route.ts @@ -19,6 +19,8 @@ import { getIdBulkError } from './utils'; import { transformValidateBulkError } from './validate'; import { transformBulkError, buildSiemResponse, createBulkErrorObject } from '../utils'; import { updateRules } from '../../rules/update_rules'; +import { legacyMigrate } from '../../rules/utils'; +import { readRules } from '../../rules/read_rules'; export const updateRulesBulkRoute = ( router: SecuritySolutionPluginRouter, @@ -69,10 +71,24 @@ export const updateRulesBulkRoute = ( throwHttpError(await mlAuthz.validateRuleType(payloadRule.type)); + const existingRule = await readRules({ + isRuleRegistryEnabled, + rulesClient, + ruleId: payloadRule.rule_id, + id: payloadRule.id, + }); + + await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule: existingRule, + }); + const rule = await updateRules({ spaceId: context.securitySolution.getSpaceId(), rulesClient, ruleStatusClient, + savedObjectsClient, defaultOutputIndex: siemClient.getSignalsIndex(), ruleUpdate: payloadRule, isRuleRegistryEnabled, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts index 7d611f3cccbf..37df792b421b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.test.ts @@ -44,7 +44,7 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); // successful update clients.ruleExecutionLogClient.find.mockResolvedValue([]); // successful transform: ; - + clients.appClient.getSignalsIndex.mockReturnValue('.siem-signals-test-index'); updateRulesRoute(server.router, ml, isRuleRegistryEnabled); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts index 7cfe83093a54..cf443e329351 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/update_rules_route.ts @@ -19,6 +19,8 @@ import { getIdError } from './utils'; import { transformValidate } from './validate'; import { updateRules } from '../../rules/update_rules'; import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; +import { legacyMigrate } from '../../rules/utils'; +import { readRules } from '../../rules/read_rules'; export const updateRulesRoute = ( router: SecuritySolutionPluginRouter, @@ -59,11 +61,25 @@ export const updateRulesRoute = ( throwHttpError(await mlAuthz.validateRuleType(request.body.type)); const ruleStatusClient = context.securitySolution.getExecutionLogClient(); + + const existingRule = await readRules({ + isRuleRegistryEnabled, + rulesClient, + ruleId: request.body.rule_id, + id: request.body.id, + }); + + await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule: existingRule, + }); const rule = await updateRules({ defaultOutputIndex: siemClient.getSignalsIndex(), isRuleRegistryEnabled, rulesClient, ruleStatusClient, + savedObjectsClient, ruleUpdate: request.body, spaceId: context.securitySolution.getSpaceId(), }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts index 1d09e4ca5c50..3626bcd5f127 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.mock.ts @@ -7,6 +7,7 @@ import { PatchRulesOptions } from './types'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { getAlertMock } from '../routes/__mocks__/request_responses'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; @@ -15,6 +16,7 @@ export const getPatchRulesOptionsMock = (isRuleRegistryEnabled: boolean): PatchR author: ['Elastic'], buildingBlockType: undefined, rulesClient: rulesClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), spaceId: 'default', ruleStatusClient: ruleExecutionLogClientMock.create(), anomalyThreshold: undefined, @@ -68,6 +70,7 @@ export const getPatchMlRulesOptionsMock = (isRuleRegistryEnabled: boolean): Patc author: ['Elastic'], buildingBlockType: undefined, rulesClient: rulesClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), spaceId: 'default', ruleStatusClient: ruleExecutionLogClientMock.create(), anomalyThreshold: 55, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts index c3b7e7288dc5..fd48cd4eebc2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/patch_rules.ts @@ -37,6 +37,7 @@ class PatchError extends Error { export const patchRules = async ({ rulesClient, + savedObjectsClient, author, buildingBlockType, ruleStatusClient, @@ -191,14 +192,14 @@ export const patchRules = async ({ const newRule = { tags: addTags(tags ?? rule.tags, rule.params.ruleId, rule.params.immutable), - throttle: throttle !== undefined ? transformToAlertThrottle(throttle) : rule.throttle, - notifyWhen: throttle !== undefined ? transformToNotifyWhen(throttle) : rule.notifyWhen, name: calculateName({ updatedName: name, originalName: rule.name }), schedule: { interval: calculateInterval(interval, rule.schedule.interval), }, - actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, params: removeUndefined(nextParams), + actions: actions?.map(transformRuleToAlertAction) ?? rule.actions, + throttle: throttle !== undefined ? transformToAlertThrottle(throttle) : rule.throttle, + notifyWhen: throttle !== undefined ? transformToNotifyWhen(throttle) : rule.notifyWhen, }; const [validated, errors] = validate(newRule, internalRuleUpdate); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index 53a83d61da78..a4ef08115401 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -8,7 +8,12 @@ import { get } from 'lodash/fp'; import { Readable } from 'stream'; -import { SavedObject, SavedObjectAttributes, SavedObjectsFindResult } from 'kibana/server'; +import { + SavedObject, + SavedObjectAttributes, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from 'kibana/server'; import type { MachineLearningJobIdOrUndefined, From, @@ -271,12 +276,14 @@ export interface UpdateRulesOptions { rulesClient: RulesClient; defaultOutputIndex: string; ruleUpdate: UpdateRulesSchema; + savedObjectsClient: SavedObjectsClientContract; } export interface PatchRulesOptions { spaceId: string; ruleStatusClient: IRuleExecutionLogClient; rulesClient: RulesClient; + savedObjectsClient: SavedObjectsClientContract; anomalyThreshold: AnomalyThresholdOrUndefined; author: AuthorOrUndefined; buildingBlockType: BuildingBlockTypeOrUndefined; @@ -323,7 +330,7 @@ export interface PatchRulesOptions { version: VersionOrUndefined; exceptionsList: ListArrayOrUndefined; actions: RuleAlertAction[] | undefined; - rule: SanitizedAlert | null; + rule: SanitizedAlert | null | undefined; namespace?: NamespaceOrUndefined; } @@ -351,3 +358,9 @@ export interface FindRuleOptions { fields: FieldsOrUndefined; sortOrder: SortOrderOrUndefined; } + +export interface LegacyMigrateParams { + rulesClient: RulesClient; + savedObjectsClient: SavedObjectsClientContract; + rule: SanitizedAlert | null | undefined; +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts index 7c9f0c9ec67a..9bd0fe3cef59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.test.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '../../../../../alerting/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; import { updatePrepackagedRules } from './update_prepacked_rules'; import { patchRules } from './patch_rules'; @@ -19,10 +20,12 @@ describe.each([ ])('updatePrepackagedRules - %s', (_, isRuleRegistryEnabled) => { let rulesClient: ReturnType; let ruleStatusClient: ReturnType; + let savedObjectsClient: ReturnType; beforeEach(() => { rulesClient = rulesClientMock.create(); ruleStatusClient = ruleExecutionLogClientMock.create(); + savedObjectsClient = savedObjectsClientMock.create(); }); it('should omit actions and enabled when calling patchRules', async () => { @@ -40,6 +43,7 @@ describe.each([ await updatePrepackagedRules( rulesClient, + savedObjectsClient, 'default', ruleStatusClient, [{ ...prepackagedRule, actions }], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts index d9c2ecd1b573..dcf43d41e8d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_prepacked_rules.ts @@ -6,6 +6,7 @@ */ import { chunk } from 'lodash/fp'; +import { SavedObjectsClientContract } from 'kibana/server'; import { AddPrepackagedRulesSchemaDecoded } from '../../../../common/detection_engine/schemas/request/add_prepackaged_rules_schema'; import { RulesClient, PartialAlert } from '../../../../../alerting/server'; import { patchRules } from './patch_rules'; @@ -13,6 +14,7 @@ import { readRules } from './read_rules'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; +import { legacyMigrate } from './utils'; /** * How many rules to update at a time is set to 50 from errors coming from @@ -51,6 +53,7 @@ export const UPDATE_CHUNK_SIZE = 50; */ export const updatePrepackagedRules = async ( rulesClient: RulesClient, + savedObjectsClient: SavedObjectsClientContract, spaceId: string, ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], @@ -61,6 +64,7 @@ export const updatePrepackagedRules = async ( for (const ruleChunk of ruleChunks) { const rulePromises = createPromises( rulesClient, + savedObjectsClient, spaceId, ruleStatusClient, ruleChunk, @@ -82,6 +86,7 @@ export const updatePrepackagedRules = async ( */ export const createPromises = ( rulesClient: RulesClient, + savedObjectsClient: SavedObjectsClientContract, spaceId: string, ruleStatusClient: IRuleExecutionLogClient, rules: AddPrepackagedRulesSchemaDecoded[], @@ -146,10 +151,17 @@ export const createPromises = ( // TODO: Fix these either with an is conversion or by better typing them within io-ts const filters: PartialFilter[] | undefined = filtersObject as PartialFilter[]; + const migratedRule = await legacyMigrate({ + rulesClient, + savedObjectsClient, + rule: existingRule, + }); + // Note: we do not pass down enabled as we do not want to suddenly disable // or enable rules on the user when they were not expecting it if a rule updates return patchRules({ rulesClient, + savedObjectsClient, author, buildingBlockType, description, @@ -160,7 +172,7 @@ export const createPromises = ( language, license, outputIndex, - rule: existingRule, + rule: migratedRule, savedId, spaceId, ruleStatusClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts index 58d6cf1fd5e6..9a7711fcc898 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.mock.ts @@ -6,6 +6,7 @@ */ import { rulesClientMock } from '../../../../../alerting/server/mocks'; +import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; import { getUpdateMachineLearningSchemaMock, getUpdateRulesSchemaMock, @@ -16,6 +17,7 @@ export const getUpdateRulesOptionsMock = (isRuleRegistryEnabled: boolean) => ({ spaceId: 'default', rulesClient: rulesClientMock.create(), ruleStatusClient: ruleExecutionLogClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateRulesSchemaMock(), isRuleRegistryEnabled, @@ -25,6 +27,7 @@ export const getUpdateMlRulesOptionsMock = (isRuleRegistryEnabled: boolean) => ( spaceId: 'default', rulesClient: rulesClientMock.create(), ruleStatusClient: ruleExecutionLogClientMock.create(), + savedObjectsClient: savedObjectsClientMock.create(), defaultOutputIndex: '.siem-signals-default', ruleUpdate: getUpdateMachineLearningSchemaMock(), isRuleRegistryEnabled, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts index f4060f7f831a..4268ed901406 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.ts @@ -6,7 +6,7 @@ */ /* eslint-disable complexity */ - +import { validate } from '@kbn/securitysolution-io-ts-utils'; import { DEFAULT_MAX_SIGNALS } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; import { PartialAlert } from '../../../../../alerting/server'; @@ -14,10 +14,18 @@ import { readRules } from './read_rules'; import { UpdateRulesOptions } from './types'; import { addTags } from './add_tags'; import { typeSpecificSnakeToCamel } from '../schemas/rule_converters'; -import { RuleParams } from '../schemas/rule_schemas'; +import { internalRuleUpdate, RuleParams } from '../schemas/rule_schemas'; import { enableRule } from './enable_rule'; import { maybeMute, transformToAlertThrottle, transformToNotifyWhen } from './utils'; +class UpdateError extends Error { + public readonly statusCode: number; + constructor(message: string, statusCode: number) { + super(message); + this.statusCode = statusCode; + } +} + export const updateRules = async ({ isRuleRegistryEnabled, spaceId, @@ -25,6 +33,7 @@ export const updateRules = async ({ ruleStatusClient, defaultOutputIndex, ruleUpdate, + savedObjectsClient, }: UpdateRulesOptions): Promise | null> => { const existingRule = await readRules({ isRuleRegistryEnabled, @@ -82,9 +91,14 @@ export const updateRules = async ({ notifyWhen: transformToNotifyWhen(ruleUpdate.throttle), }; + const [validated, errors] = validate(newInternalRule, internalRuleUpdate); + if (errors != null || validated === null) { + throw new UpdateError(`Applying update would create invalid rule: ${errors}`, 400); + } + const update = await rulesClient.update({ id: existingRule.id, - data: newInternalRule, + data: validated, }); await maybeMute({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts index 4647a4a9951d..a558024a73e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/utils.ts @@ -65,6 +65,9 @@ import { RulesClient } from '../../../../../alerting/server'; import { LegacyRuleActions } from '../rule_actions/legacy_types'; import { FullResponseSchema } from '../../../../common/detection_engine/schemas/request'; import { transformAlertToRuleAction } from '../../../../common/detection_engine/transform_actions'; +// eslint-disable-next-line no-restricted-imports +import { legacyRuleActionsSavedObjectType } from '../rule_actions/legacy_saved_object_mappings'; +import { LegacyMigrateParams } from './types'; export const calculateInterval = ( interval: string | undefined, @@ -296,3 +299,62 @@ export const maybeMute = async ({ // Do nothing, no-operation } }; + +/** + * Determines if rule needs to be migrated from legacy actions + * and returns necessary pieces for the updated rule + */ +export const legacyMigrate = async ({ + rulesClient, + savedObjectsClient, + rule, +}: LegacyMigrateParams): Promise | null | undefined> => { + if (rule == null || rule.id == null) { + return rule; + } + /** + * On update / patch I'm going to take the actions as they are, better off taking rules client.find (siem.notification) result + * and putting that into the actions array of the rule, then set the rules onThrottle property, notifyWhen and throttle from null -> actualy value (1hr etc..) + * Then use the rules client to delete the siem.notification + * Then with the legacy Rule Actions saved object type, just delete it. + */ + + // find it using the references array, not params.ruleAlertId + const [siemNotification, legacyRuleActionsSO] = await Promise.all([ + rulesClient.find({ + options: { + hasReference: { + type: 'alert', + id: rule.id, + }, + }, + }), + savedObjectsClient.find({ + type: legacyRuleActionsSavedObjectType, + }), + ]); + + if (siemNotification != null && siemNotification.data.length > 0) { + await Promise.all([ + rulesClient.delete({ id: siemNotification.data[0].id }), + legacyRuleActionsSO != null && legacyRuleActionsSO.saved_objects.length > 0 + ? savedObjectsClient.delete( + legacyRuleActionsSavedObjectType, + legacyRuleActionsSO.saved_objects[0].id + ) + : null, + ]); + const migratedRule = { + ...rule, + actions: siemNotification.data[0].actions, + throttle: siemNotification.data[0].schedule.interval, + notifyWhen: transformToNotifyWhen(siemNotification.data[0].throttle), + }; + await rulesClient.update({ + id: rule.id, + data: migratedRule, + }); + return migratedRule; + } + return rule; +}; From c2c08be709429270139cae7b9674d349a4b61e86 Mon Sep 17 00:00:00 2001 From: Frank Hassanabad Date: Tue, 19 Oct 2021 08:24:42 -0600 Subject: [PATCH 45/53] [Security Solutions] Adds security detection rule actions as importable and exportable (#115243) ## Summary Adds the security detection rule actions as being exportable and importable. * Adds exportable actions for legacy notification system * Adds exportable actions for the new throttle notification system * Adds importable but only imports into the new throttle notification system. * Updates unit tests In your `ndjson` file when you have actions exported you will see them like so: ```json "actions": [ { "group": "default", "id": "b55117e0-2df9-11ec-b789-7f03e3cdd668", "params": { "message": "Rule {{context.rule.name}} generated {{state.signals_count}} alerts" }, "action_type_id": ".slack" } ] ``` where before it was `actions: []` and was not provided. **Caveats** If you delete your connector and have an invalid connector then the rule(s) that were referring to that invalid connector will not import and you will get an error like this: Screen Shot 2021-10-15 at 2 47 10 PM This does _not_ export your connectors at this point in time. You have to export your connector through the Saved Object Management separate like so: Screen Shot 2021-10-15 at 2 58 03 PM However, if remove everything and import your connector without changing its saved object ID and then go to import the rules everything should import ok and you will get your actions working. **Manual Testing**: * You can create normal actions on an alert and then do exports and you should see the actions in your ndjson file * You can create legacy notifications from 7.14.0 and then upgrade and export and you should see the actions in your ndjson file * You can manually create legacy notifications by: By getting an alert id first and ensuring that your `legacy_notifications/one_action.json` contains a valid action then running this command: ```ts ./post_legacy_notification.sh 3403c0d0-2d44-11ec-b147-3b0c6d563a60 ``` * You can export your connector and remove everything and then do an import and you will have everything imported and working with your actions and connector wired up correctly. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added --- .../routes/rules/export_rules_route.ts | 13 ++++- .../routes/rules/import_rules_route.ts | 5 +- .../rules/perform_bulk_action_route.test.ts | 5 +- .../routes/rules/perform_bulk_action_route.ts | 5 ++ .../routes/rules/utils.test.ts | 6 +-- .../detection_engine/routes/rules/utils.ts | 7 ++- .../rules/get_export_all.test.ts | 26 +++++++++- .../detection_engine/rules/get_export_all.ts | 17 ++++++- .../rules/get_export_by_object_ids.test.ts | 49 +++++++++++++++++-- .../rules/get_export_by_object_ids.ts | 36 +++++++++++--- .../security_solution/server/routes/index.ts | 4 +- 11 files changed, 145 insertions(+), 28 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts index e4b99e63cb6c..c84dd8147ebc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/export_rules_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { Logger } from 'src/core/server'; import { exportRulesQuerySchema, ExportRulesQuerySchemaDecoded, @@ -24,6 +25,7 @@ import { buildSiemResponse } from '../utils'; export const exportRulesRoute = ( router: SecuritySolutionPluginRouter, config: ConfigType, + logger: Logger, isRuleRegistryEnabled: boolean ) => { router.post( @@ -44,6 +46,7 @@ export const exportRulesRoute = ( async (context, request, response) => { const siemResponse = buildSiemResponse(response); const rulesClient = context.alerting?.getRulesClient(); + const savedObjectsClient = context.core.savedObjects.client; if (!rulesClient) { return siemResponse.error({ statusCode: 404 }); @@ -71,8 +74,14 @@ export const exportRulesRoute = ( const exported = request.body?.objects != null - ? await getExportByObjectIds(rulesClient, request.body.objects, isRuleRegistryEnabled) - : await getExportAll(rulesClient, isRuleRegistryEnabled); + ? await getExportByObjectIds( + rulesClient, + savedObjectsClient, + request.body.objects, + logger, + isRuleRegistryEnabled + ) + : await getExportAll(rulesClient, savedObjectsClient, logger, isRuleRegistryEnabled); const responseBody = request.query.exclude_export_details ? exported.rulesNdjson diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index b09ef1a21574..3752128d3daa 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -194,6 +194,7 @@ export const importRulesRoute = ( throttle, version, exceptions_list: exceptionsList, + actions, } = parsedRule; try { @@ -265,7 +266,7 @@ export const importRulesRoute = ( note, version, exceptionsList, - actions: [], // Actions are not imported nor exported at this time + actions, }); resolve({ rule_id: ruleId, @@ -328,7 +329,7 @@ export const importRulesRoute = ( exceptionsList, anomalyThreshold, machineLearningJobId, - actions: undefined, + actions, }); resolve({ rule_id: ruleId, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts index 41b909bd718c..3e85b4898d01 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.test.ts @@ -17,6 +17,7 @@ import { import { requestContextMock, serverMock, requestMock } from '../__mocks__'; import { performBulkActionRoute } from './perform_bulk_action_route'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; jest.mock('../../../machine_learning/authz', () => mockMlAuthzFactory.create()); @@ -27,15 +28,17 @@ describe.each([ let server: ReturnType; let { clients, context } = requestContextMock.createTools(); let ml: ReturnType; + let logger: ReturnType; beforeEach(() => { server = serverMock.create(); + logger = loggingSystemMock.createLogger(); ({ clients, context } = requestContextMock.createTools()); ml = mlServicesMock.createSetupContract(); clients.rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); - performBulkActionRoute(server.router, ml, isRuleRegistryEnabled); + performBulkActionRoute(server.router, ml, logger, isRuleRegistryEnabled); }); describe('status codes', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts index 0eba5af4e063..fb5a2315479d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/perform_bulk_action_route.ts @@ -6,6 +6,8 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { Logger } from 'src/core/server'; + import { DETECTION_ENGINE_RULES_BULK_ACTION } from '../../../../../common/constants'; import { BulkAction } from '../../../../../common/detection_engine/schemas/common/schemas'; import { performBulkActionSchema } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema'; @@ -26,6 +28,7 @@ const BULK_ACTION_RULES_LIMIT = 10000; export const performBulkActionRoute = ( router: SecuritySolutionPluginRouter, ml: SetupPlugins['ml'], + logger: Logger, isRuleRegistryEnabled: boolean ) => { router.post( @@ -133,7 +136,9 @@ export const performBulkActionRoute = ( case BulkAction.export: const exported = await getExportByObjectIds( rulesClient, + savedObjectsClient, rules.data.map(({ params }) => ({ rule_id: params.ruleId })), + logger, isRuleRegistryEnabled ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts index c5a30c349d49..366ae607f0ba 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.test.ts @@ -469,12 +469,12 @@ describe.each([ describe('transformAlertsToRules', () => { test('given an empty array returns an empty array', () => { - expect(transformAlertsToRules([])).toEqual([]); + expect(transformAlertsToRules([], {})).toEqual([]); }); test('given single alert will return the alert transformed', () => { const result1 = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - const transformed = transformAlertsToRules([result1]); + const transformed = transformAlertsToRules([result1], {}); const expected = getOutputRuleAlertForRest(); expect(transformed).toEqual([expected]); }); @@ -485,7 +485,7 @@ describe.each([ result2.id = 'some other id'; result2.params.ruleId = 'some other id'; - const transformed = transformAlertsToRules([result1, result2]); + const transformed = transformAlertsToRules([result1, result2], {}); const expected1 = getOutputRuleAlertForRest(); const expected2 = getOutputRuleAlertForRest(); expected2.id = 'some other id'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts index afc48386a298..bb2e35d189ca 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/utils.ts @@ -103,8 +103,11 @@ export const transformAlertToRule = ( return internalRuleToAPIResponse(alert, ruleStatus?.attributes, legacyRuleActions); }; -export const transformAlertsToRules = (alerts: RuleAlertType[]): Array> => { - return alerts.map((alert) => transformAlertToRule(alert)); +export const transformAlertsToRules = ( + alerts: RuleAlertType[], + legacyRuleActions: Record +): Array> => { + return alerts.map((alert) => transformAlertToRule(alert, undefined, legacyRuleActions[alert.id])); }; export const transformFindAlerts = ( diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts index 3ca5960d7d4e..92e4f0bbb4a5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.test.ts @@ -9,21 +9,33 @@ import { getAlertMock, getFindResultWithSingleHit, FindHit, + getEmptySavedObjectsResponse, } from '../routes/__mocks__/request_responses'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getExportAll } from './get_export_all'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; + import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { requestContextMock } from '../routes/__mocks__/request_context'; describe.each([ ['Legacy', false], ['RAC', true], ])('getExportAll - %s', (_, isRuleRegistryEnabled) => { + let logger: ReturnType; + const { clients } = requestContextMock.createTools(); + + beforeEach(async () => { + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); + }); + test('it exports everything from the alerts client', async () => { const rulesClient = rulesClientMock.create(); const result = getFindResultWithSingleHit(isRuleRegistryEnabled); const alert = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + alert.params = { ...alert.params, filters: [{ query: { match_phrase: { 'host.name': 'some-host' } } }], @@ -35,7 +47,12 @@ describe.each([ result.data = [alert]; rulesClient.find.mockResolvedValue(result); - const exports = await getExportAll(rulesClient, isRuleRegistryEnabled); + const exports = await getExportAll( + rulesClient, + clients.savedObjectsClient, + logger, + isRuleRegistryEnabled + ); const rulesJson = JSON.parse(exports.rulesNdjson); const detailsJson = JSON.parse(exports.exportDetails); expect(rulesJson).toEqual({ @@ -97,7 +114,12 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); - const exports = await getExportAll(rulesClient, isRuleRegistryEnabled); + const exports = await getExportAll( + rulesClient, + clients.savedObjectsClient, + logger, + isRuleRegistryEnabled + ); expect(exports).toEqual({ rulesNdjson: '', exportDetails: '{"exported_count":0,"missing_rules":[],"missing_rules_count":0}\n', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts index 71079ccefc97..cbbda5df7e2b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_all.ts @@ -7,20 +7,33 @@ import { transformDataToNdjson } from '@kbn/securitysolution-utils'; -import { RulesClient } from '../../../../../alerting/server'; +import { Logger } from 'src/core/server'; +import { RulesClient, AlertServices } from '../../../../../alerting/server'; import { getNonPackagedRules } from './get_existing_prepackaged_rules'; import { getExportDetailsNdjson } from './get_export_details_ndjson'; import { transformAlertsToRules } from '../routes/rules/utils'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; + export const getExportAll = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], + logger: Logger, isRuleRegistryEnabled: boolean ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { const ruleAlertTypes = await getNonPackagedRules({ rulesClient, isRuleRegistryEnabled }); - const rules = transformAlertsToRules(ruleAlertTypes); + const alertIds = ruleAlertTypes.map((rule) => rule.id); + const legacyActions = await legacyGetBulkRuleActionsSavedObject({ + alertIds, + savedObjectsClient, + logger, + }); + + const rules = transformAlertsToRules(ruleAlertTypes, legacyActions); // We do not support importing/exporting actions. When we do, delete this line of code const rulesWithoutActions = rules.map((rule) => ({ ...rule, actions: [] })); const rulesNdjson = transformDataToNdjson(rulesWithoutActions); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts index 740427e44b56..961f2c6a4186 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.test.ts @@ -10,28 +10,43 @@ import { getAlertMock, getFindResultWithSingleHit, FindHit, + getEmptySavedObjectsResponse, } from '../routes/__mocks__/request_responses'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; import { getThreatMock } from '../../../../common/detection_engine/schemas/types/threat.mock'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { requestContextMock } from '../routes/__mocks__/request_context'; describe.each([ ['Legacy', false], ['RAC', true], ])('get_export_by_object_ids - %s', (_, isRuleRegistryEnabled) => { + let logger: ReturnType; + const { clients } = requestContextMock.createTools(); + beforeEach(() => { jest.resetAllMocks(); jest.restoreAllMocks(); jest.clearAllMocks(); + + clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); }); + describe('getExportByObjectIds', () => { test('it exports object ids into an expected string with new line characters', async () => { const rulesClient = rulesClientMock.create(); rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getExportByObjectIds( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const exportsObj = { rulesNdjson: JSON.parse(exports.rulesNdjson), exportDetails: JSON.parse(exports.exportDetails), @@ -102,7 +117,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getExportByObjectIds(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getExportByObjectIds( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); expect(exports).toEqual({ rulesNdjson: '', exportDetails: @@ -117,7 +138,13 @@ describe.each([ rulesClient.find.mockResolvedValue(getFindResultWithSingleHit(isRuleRegistryEnabled)); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 1, missingRules: [], @@ -192,7 +219,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], @@ -215,7 +248,13 @@ describe.each([ rulesClient.find.mockResolvedValue(findResult); const objects = [{ rule_id: 'rule-1' }]; - const exports = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); + const exports = await getRulesFromObjects( + rulesClient, + clients.savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); const expected: RulesErrors = { exportedCount: 0, missingRules: [{ rule_id: 'rule-1' }], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts index 4cf3ad9133a7..8233fe6d4948 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/get_export_by_object_ids.ts @@ -8,14 +8,20 @@ import { chunk } from 'lodash'; import { transformDataToNdjson } from '@kbn/securitysolution-utils'; +import { Logger } from 'src/core/server'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { RulesClient } from '../../../../../alerting/server'; +import { RulesClient, AlertServices } from '../../../../../alerting/server'; + import { getExportDetailsNdjson } from './get_export_details_ndjson'; + import { isAlertType } from '../rules/types'; import { transformAlertToRule } from '../routes/rules/utils'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { findRules } from './find_rules'; +// eslint-disable-next-line no-restricted-imports +import { legacyGetBulkRuleActionsSavedObject } from '../rule_actions/legacy_get_bulk_rule_actions_saved_object'; + interface ExportSuccessRule { statusCode: 200; rule: Partial; @@ -34,23 +40,32 @@ export interface RulesErrors { export const getExportByObjectIds = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], objects: Array<{ rule_id: string }>, + logger: Logger, isRuleRegistryEnabled: boolean ): Promise<{ rulesNdjson: string; exportDetails: string; }> => { - const rulesAndErrors = await getRulesFromObjects(rulesClient, objects, isRuleRegistryEnabled); - // We do not support importing/exporting actions. When we do, delete this line of code - const rulesWithoutActions = rulesAndErrors.rules.map((rule) => ({ ...rule, actions: [] })); - const rulesNdjson = transformDataToNdjson(rulesWithoutActions); - const exportDetails = getExportDetailsNdjson(rulesWithoutActions, rulesAndErrors.missingRules); + const rulesAndErrors = await getRulesFromObjects( + rulesClient, + savedObjectsClient, + objects, + logger, + isRuleRegistryEnabled + ); + + const rulesNdjson = transformDataToNdjson(rulesAndErrors.rules); + const exportDetails = getExportDetailsNdjson(rulesAndErrors.rules, rulesAndErrors.missingRules); return { rulesNdjson, exportDetails }; }; export const getRulesFromObjects = async ( rulesClient: RulesClient, + savedObjectsClient: AlertServices['savedObjectsClient'], objects: Array<{ rule_id: string }>, + logger: Logger, isRuleRegistryEnabled: boolean ): Promise => { // If we put more than 1024 ids in one block like "alert.attributes.tags: (id1 OR id2 OR ... OR id1100)" @@ -78,6 +93,13 @@ export const getRulesFromObjects = async ( sortField: undefined, sortOrder: undefined, }); + const alertIds = rules.data.map((rule) => rule.id); + const legacyActions = await legacyGetBulkRuleActionsSavedObject({ + alertIds, + savedObjectsClient, + logger, + }); + const alertsAndErrors = objects.map(({ rule_id: ruleId }) => { const matchingRule = rules.data.find((rule) => rule.params.ruleId === ruleId); if ( @@ -87,7 +109,7 @@ export const getRulesFromObjects = async ( ) { return { statusCode: 200, - rule: transformAlertToRule(matchingRule), + rule: transformAlertToRule(matchingRule, undefined, legacyActions[matchingRule.id]), }; } else { return { diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index d045c6b129e4..148580d5c447 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -91,12 +91,12 @@ export const initRoutes = ( updateRulesBulkRoute(router, ml, isRuleRegistryEnabled); patchRulesBulkRoute(router, ml, isRuleRegistryEnabled); deleteRulesBulkRoute(router, isRuleRegistryEnabled); - performBulkActionRoute(router, ml, isRuleRegistryEnabled); + performBulkActionRoute(router, ml, logger, isRuleRegistryEnabled); createTimelinesRoute(router, config, security); patchTimelinesRoute(router, config, security); importRulesRoute(router, config, ml, isRuleRegistryEnabled); - exportRulesRoute(router, config, isRuleRegistryEnabled); + exportRulesRoute(router, config, logger, isRuleRegistryEnabled); importTimelinesRoute(router, config, security); exportTimelinesRoute(router, config, security); From 6a1af300f5a08de7b1287e52d04a6b304cc3186b Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 19 Oct 2021 16:35:40 +0200 Subject: [PATCH 46/53] [Discover] Improve doc viewer code in Discover (#114759) Co-authored-by: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> --- .../public/__mocks__/index_patterns.ts | 8 +++-- .../apps/context/context_app_route.tsx | 26 +++++++++++++- .../apps/doc/components/doc.test.tsx | 21 ++--------- .../application/apps/doc/components/doc.tsx | 16 ++++----- .../application/apps/doc/single_doc_route.tsx | 35 ++++++++++++++----- .../doc_viewer/doc_viewer_tab.test.tsx | 3 ++ .../__snapshots__/source_viewer.test.tsx.snap | 18 ++++++++-- .../source_viewer/source_viewer.test.tsx | 14 ++++---- .../source_viewer/source_viewer.tsx | 17 ++++----- .../application/components/table/table.tsx | 2 +- .../application/doc_views/doc_views_types.ts | 2 +- .../helpers/use_index_pattern.test.tsx | 20 ++++++++--- .../application/helpers/use_index_pattern.tsx | 13 ++++--- .../services/use_es_doc_search.test.tsx | 24 ++++--------- .../application/services/use_es_doc_search.ts | 15 +++----- src/plugins/discover/public/plugin.tsx | 2 +- 16 files changed, 135 insertions(+), 101 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/index_patterns.ts b/src/plugins/discover/public/__mocks__/index_patterns.ts index 88447eacc884..b90338e89562 100644 --- a/src/plugins/discover/public/__mocks__/index_patterns.ts +++ b/src/plugins/discover/public/__mocks__/index_patterns.ts @@ -10,12 +10,14 @@ import { IndexPatternsService } from '../../../data/common'; import { indexPatternMock } from './index_pattern'; export const indexPatternsMock = { - getCache: () => { + getCache: async () => { return [indexPatternMock]; }, - get: (id: string) => { + get: async (id: string) => { if (id === 'the-index-pattern-id') { - return indexPatternMock; + return Promise.resolve(indexPatternMock); + } else if (id === 'invalid-index-pattern-id') { + return Promise.reject('Invald'); } }, updateSavedObject: jest.fn(), diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/apps/context/context_app_route.tsx index d124fd6cfa39..6c4722418be1 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_route.tsx @@ -8,6 +8,8 @@ import React, { useEffect } from 'react'; import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverServices } from '../../../build_services'; import { ContextApp } from './context_app'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; @@ -43,7 +45,29 @@ export function ContextAppRoute(props: ContextAppProps) { ]); }, [chrome]); - const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + const { indexPattern, error } = useIndexPattern(services.indexPatterns, indexPatternId); + + if (error) { + return ( + + } + body={ + + } + /> + ); + } if (!indexPattern) { return ; diff --git a/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx b/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx index 31ff39ea6b57..68c012ddd92e 100644 --- a/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx +++ b/src/plugins/discover/public/application/apps/doc/components/doc.test.tsx @@ -14,6 +14,7 @@ import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; import { Doc, DocProps } from './doc'; import { SEARCH_FIELDS_FROM_SOURCE as mockSearchFieldsFromSource } from '../../../../../common'; +import { indexPatternMock } from '../../../../__mocks__/index_pattern'; const mockSearchApi = jest.fn(); @@ -74,21 +75,11 @@ const waitForPromises = async () => * this works but logs ugly error messages until we're using React 16.9 * should be adapted when we upgrade */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -async function mountDoc(update = false, indexPatternGetter: any = null) { - const indexPattern = { - getComputedFields: () => [], - }; - const indexPatternService = { - get: indexPatternGetter ? indexPatternGetter : jest.fn(() => Promise.resolve(indexPattern)), - // eslint-disable-next-line @typescript-eslint/no-explicit-any - } as any; - +async function mountDoc(update = false) { const props = { id: '1', index: 'index1', - indexPatternId: 'xyz', - indexPatternService, + indexPattern: indexPatternMock, } as DocProps; let comp!: ReactWrapper; await act(async () => { @@ -108,12 +99,6 @@ describe('Test of of Discover', () => { expect(findTestSubject(comp, 'doc-msg-loading').length).toBe(1); }); - test('renders IndexPattern notFound msg', async () => { - const indexPatternGetter = jest.fn(() => Promise.reject({ savedObjectId: '007' })); - const comp = await mountDoc(true, indexPatternGetter); - expect(findTestSubject(comp, 'doc-msg-notFoundIndexPattern').length).toBe(1); - }); - test('renders notFound msg', async () => { mockSearchApi.mockImplementation(() => throwError({ status: 404 })); const comp = await mountDoc(true); diff --git a/src/plugins/discover/public/application/apps/doc/components/doc.tsx b/src/plugins/discover/public/application/apps/doc/components/doc.tsx index f33ffe561e49..c6cfad3953e9 100644 --- a/src/plugins/discover/public/application/apps/doc/components/doc.tsx +++ b/src/plugins/discover/public/application/apps/doc/components/doc.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCallOut, EuiLink, EuiLoadingSpinner, EuiPageContent, EuiPage } from '@elastic/eui'; -import { IndexPatternsContract } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; import { getServices } from '../../../../kibana_services'; import { DocViewer } from '../../../components/doc_viewer/doc_viewer'; import { ElasticRequestState } from '../types'; @@ -25,14 +25,9 @@ export interface DocProps { */ index: string; /** - * IndexPattern ID used to get IndexPattern entity - * that's used for adding additional fields (stored_fields, script_fields, docvalue_fields) + * IndexPattern entity */ - indexPatternId: string; - /** - * IndexPatternService to get a given index pattern by ID - */ - indexPatternService: IndexPatternsContract; + indexPattern: IndexPattern; /** * If set, will always request source, regardless of the global `fieldsFromSource` setting */ @@ -40,7 +35,8 @@ export interface DocProps { } export function Doc(props: DocProps) { - const [reqState, hit, indexPattern] = useEsDocSearch(props); + const { indexPattern } = props; + const [reqState, hit] = useEsDocSearch(props); const indexExistsLink = getServices().docLinks.links.apis.indexExists; return ( @@ -54,7 +50,7 @@ export function Doc(props: DocProps) { } /> diff --git a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx index 8398f6255e0f..aef928d52351 100644 --- a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx +++ b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx @@ -7,6 +7,8 @@ */ import React, { useEffect } from 'react'; import { useLocation, useParams } from 'react-router-dom'; +import { EuiEmptyPrompt } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { DiscoverServices } from '../../../build_services'; import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; import { Doc } from './components/doc'; @@ -31,7 +33,7 @@ function useQuery() { export function SingleDocRoute(props: SingleDocRouteProps) { const { services } = props; - const { chrome, timefilter, indexPatterns } = services; + const { chrome, timefilter } = services; const { indexPatternId, index } = useParams(); @@ -52,7 +54,29 @@ export function SingleDocRoute(props: SingleDocRouteProps) { timefilter.disableTimeRangeSelector(); }); - const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + const { indexPattern, error } = useIndexPattern(services.indexPatterns, indexPatternId); + + if (error) { + return ( + + } + body={ + + } + /> + ); + } if (!indexPattern) { return ; @@ -60,12 +84,7 @@ export function SingleDocRoute(props: SingleDocRouteProps) { return (
- +
); } diff --git a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx index a2434170acdd..188deba75544 100644 --- a/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx +++ b/src/plugins/discover/public/application/components/doc_viewer/doc_viewer_tab.test.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { DocViewerTab } from './doc_viewer_tab'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; +import { indexPatternMock } from '../../../__mocks__/index_pattern'; describe('DocViewerTab', () => { test('changing columns triggers an update', () => { @@ -21,6 +22,7 @@ describe('DocViewerTab', () => { renderProps: { hit: {} as ElasticSearchHit, columns: ['test'], + indexPattern: indexPatternMock, }, }; @@ -31,6 +33,7 @@ describe('DocViewerTab', () => { renderProps: { hit: {} as ElasticSearchHit, columns: ['test2'], + indexPattern: indexPatternMock, }, }; diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index 82d9183f3d39..761263ee861b 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -5,7 +5,11 @@ exports[`Source Viewer component renders error state 1`] = ` hasLineNumbers={true} id="1" index="index1" - indexPatternId="xyz" + indexPattern={ + Object { + "getComputedFields": [Function], + } + } intl={ Object { "defaultFormats": Object {}, @@ -264,7 +268,11 @@ exports[`Source Viewer component renders json code editor 1`] = ` hasLineNumbers={true} id="1" index="index1" - indexPatternId="xyz" + indexPattern={ + Object { + "getComputedFields": [Function], + } + } intl={ Object { "defaultFormats": Object {}, @@ -619,7 +627,11 @@ exports[`Source Viewer component renders loading state 1`] = ` hasLineNumbers={true} id="1" index="index1" - indexPatternId="xyz" + indexPattern={ + Object { + "getComputedFields": [Function], + } + } intl={ Object { "defaultFormats": Object {}, diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx index 7895c1025dda..a98c2de6197d 100644 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.test.tsx @@ -43,13 +43,13 @@ const mockIndexPatternService = { })); describe('Source Viewer component', () => { test('renders loading state', () => { - jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, null, () => {}]); + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [0, null, () => {}]); const comp = mountWithIntl( @@ -60,13 +60,13 @@ describe('Source Viewer component', () => { }); test('renders error state', () => { - jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, null, () => {}]); + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [3, null, () => {}]); const comp = mountWithIntl( @@ -97,9 +97,7 @@ describe('Source Viewer component', () => { _underscore: 123, }, } as never; - jest - .spyOn(hooks, 'useEsDocSearch') - .mockImplementation(() => [2, mockHit, mockIndexPattern, () => {}]); + jest.spyOn(hooks, 'useEsDocSearch').mockImplementation(() => [2, mockHit, () => {}]); jest.spyOn(useUiSettingHook, 'useUiSetting').mockImplementation(() => { return false; }); @@ -107,7 +105,7 @@ describe('Source Viewer component', () => { diff --git a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx index 9e37ae8f8bf9..31d4d866df21 100644 --- a/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx +++ b/src/plugins/discover/public/application/components/source_viewer/source_viewer.tsx @@ -17,11 +17,12 @@ import { getServices } from '../../../kibana_services'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; import { ElasticRequestState } from '../../apps/doc/types'; import { useEsDocSearch } from '../../services/use_es_doc_search'; +import { IndexPattern } from '../../../../../data_views/common'; interface SourceViewerProps { id: string; index: string; - indexPatternId: string; + indexPattern: IndexPattern; hasLineNumbers: boolean; width?: number; } @@ -29,19 +30,17 @@ interface SourceViewerProps { export const SourceViewer = ({ id, index, - indexPatternId, + indexPattern, width, hasLineNumbers, }: SourceViewerProps) => { const [editor, setEditor] = useState(); const [jsonValue, setJsonValue] = useState(''); - const indexPatternService = getServices().data.indexPatterns; const useNewFieldsApi = !getServices().uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); - const [reqState, hit, , requestData] = useEsDocSearch({ + const [reqState, hit, requestData] = useEsDocSearch({ id, index, - indexPatternId, - indexPatternService, + indexPattern, requestSource: useNewFieldsApi, }); @@ -106,11 +105,7 @@ export const SourceViewer = ({ ); - if ( - reqState === ElasticRequestState.Error || - reqState === ElasticRequestState.NotFound || - reqState === ElasticRequestState.NotFoundIndexPattern - ) { + if (reqState === ElasticRequestState.Error || reqState === ElasticRequestState.NotFound) { return errorState; } diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index 7f597d846f88..e64dbd10f785 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -25,7 +25,7 @@ export interface DocViewerTableProps { columns?: string[]; filter?: DocViewFilterFn; hit: ElasticSearchHit; - indexPattern?: IndexPattern; + indexPattern: IndexPattern; onAddColumn?: (columnName: string) => void; onRemoveColumn?: (columnName: string) => void; } diff --git a/src/plugins/discover/public/application/doc_views/doc_views_types.ts b/src/plugins/discover/public/application/doc_views/doc_views_types.ts index 48bebec22b9b..d3e482c0f2e1 100644 --- a/src/plugins/discover/public/application/doc_views/doc_views_types.ts +++ b/src/plugins/discover/public/application/doc_views/doc_views_types.ts @@ -32,7 +32,7 @@ export interface DocViewRenderProps { columns?: string[]; filter?: DocViewFilterFn; hit: ElasticSearchHit; - indexPattern?: IndexPattern; + indexPattern: IndexPattern; onAddColumn?: (columnName: string) => void; onRemoveColumn?: (columnName: string) => void; } diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx index 85282afb6fc3..dfc54d863074 100644 --- a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx @@ -8,12 +8,24 @@ import { useIndexPattern } from './use_index_pattern'; import { indexPatternMock } from '../../__mocks__/index_pattern'; import { indexPatternsMock } from '../../__mocks__/index_patterns'; -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; describe('Use Index Pattern', () => { test('returning a valid index pattern', async () => { - const { result } = renderHook(() => useIndexPattern(indexPatternsMock, 'the-index-pattern-id')); - await act(() => Promise.resolve()); - expect(result.current).toBe(indexPatternMock); + const { result, waitForNextUpdate } = renderHook(() => + useIndexPattern(indexPatternsMock, 'the-index-pattern-id') + ); + await waitForNextUpdate(); + expect(result.current.indexPattern).toBe(indexPatternMock); + expect(result.current.error).toBe(undefined); + }); + + test('returning an error', async () => { + const { result, waitForNextUpdate } = renderHook(() => + useIndexPattern(indexPatternsMock, 'invalid-index-pattern-id') + ); + await waitForNextUpdate(); + expect(result.current.indexPattern).toBe(undefined); + expect(result.current.error).toBeTruthy(); }); }); diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx index f53d131920c5..374f83cbbfe7 100644 --- a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx @@ -10,13 +10,18 @@ import { IndexPattern, IndexPatternsContract } from '../../../../data/common'; export const useIndexPattern = (indexPatterns: IndexPatternsContract, indexPatternId: string) => { const [indexPattern, setIndexPattern] = useState(undefined); + const [error, setError] = useState(); useEffect(() => { async function loadIndexPattern() { - const ip = await indexPatterns.get(indexPatternId); - setIndexPattern(ip); + try { + const item = await indexPatterns.get(indexPatternId); + setIndexPattern(item); + } catch (e) { + setError(e); + } } loadIndexPattern(); - }); - return indexPattern; + }, [indexPatternId, indexPatterns]); + return { indexPattern, error }; }; diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx b/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx index af7d189e6288..ca57b470b471 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx +++ b/src/plugins/discover/public/application/services/use_es_doc_search.test.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { renderHook, act } from '@testing-library/react-hooks'; +import { renderHook } from '@testing-library/react-hooks'; import { buildSearchBody, useEsDocSearch } from './use_es_doc_search'; import { Observable } from 'rxjs'; import { IndexPattern } from 'src/plugins/data/common'; @@ -175,26 +175,14 @@ describe('Test of helper / hook', () => { const indexPattern = { getComputedFields: () => [], }; - const getMock = jest.fn(() => Promise.resolve(indexPattern)); - const indexPatternService = { - get: getMock, - } as unknown as IndexPattern; const props = { id: '1', index: 'index1', - indexPatternId: 'xyz', - indexPatternService, - } as unknown as DocProps; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - let hook: any; - await act(async () => { - hook = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); - }); - expect(hook.result.current.slice(0, 3)).toEqual([ - ElasticRequestState.Loading, - null, indexPattern, - ]); - expect(getMock).toHaveBeenCalled(); + } as unknown as DocProps; + + const { result } = renderHook((p: DocProps) => useEsDocSearch(p), { initialProps: props }); + + expect(result.current.slice(0, 2)).toEqual([ElasticRequestState.Loading, null]); }); }); diff --git a/src/plugins/discover/public/application/services/use_es_doc_search.ts b/src/plugins/discover/public/application/services/use_es_doc_search.ts index a2f0cd6f8442..16a24ff27292 100644 --- a/src/plugins/discover/public/application/services/use_es_doc_search.ts +++ b/src/plugins/discover/public/application/services/use_es_doc_search.ts @@ -64,11 +64,9 @@ export function buildSearchBody( export function useEsDocSearch({ id, index, - indexPatternId, - indexPatternService, + indexPattern, requestSource, -}: DocProps): [ElasticRequestState, ElasticSearchHit | null, IndexPattern | null, () => void] { - const [indexPattern, setIndexPattern] = useState(null); +}: DocProps): [ElasticRequestState, ElasticSearchHit | null | null, () => void] { const [status, setStatus] = useState(ElasticRequestState.Loading); const [hit, setHit] = useState(null); const { data, uiSettings } = useMemo(() => getServices(), []); @@ -76,14 +74,11 @@ export function useEsDocSearch({ const requestData = useCallback(async () => { try { - const indexPatternEntity = await indexPatternService.get(indexPatternId); - setIndexPattern(indexPatternEntity); - const { rawResponse } = await data.search .search({ params: { index, - body: buildSearchBody(id, indexPatternEntity, useNewFieldsApi, requestSource)?.body, + body: buildSearchBody(id, indexPattern, useNewFieldsApi, requestSource)?.body, }, }) .toPromise(); @@ -105,11 +100,11 @@ export function useEsDocSearch({ setStatus(ElasticRequestState.Error); } } - }, [id, index, indexPatternId, indexPatternService, data.search, useNewFieldsApi, requestSource]); + }, [id, index, indexPattern, data.search, useNewFieldsApi, requestSource]); useEffect(() => { requestData(); }, [requestData]); - return [status, hit, indexPattern, requestData]; + return [status, hit, requestData]; } diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index d86e5f363630..6d30e6fd9e8a 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -267,7 +267,7 @@ export class DiscoverPlugin From 9dcf5bf1b70632dc511e48c2da2ef53d899b3df8 Mon Sep 17 00:00:00 2001 From: David Roberts Date: Tue, 19 Oct 2021 15:42:49 +0100 Subject: [PATCH 47/53] [ML] Stop reading the ml.max_open_jobs node attribute (#115524) The ml.max_open_jobs node attribute is going away in version 8, as the maximum number of open jobs has been defined by a dynamic cluster-wide setting during the 7 series and there is no chance of version 8 needing to run in a mixed version cluster with version 6. The ml.machine_memory attribute will still be available, so this can be checked instead as a way of detecting ML nodes. --- x-pack/plugins/ml/server/lib/node_utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/ml/server/lib/node_utils.ts b/x-pack/plugins/ml/server/lib/node_utils.ts index 82e5d7f46984..a13e44b307a7 100644 --- a/x-pack/plugins/ml/server/lib/node_utils.ts +++ b/x-pack/plugins/ml/server/lib/node_utils.ts @@ -17,8 +17,8 @@ export async function getMlNodeCount(client: IScopedClusterClient): Promise { if (body.nodes[k].attributes !== undefined) { - const maxOpenJobs = +body.nodes[k].attributes['ml.max_open_jobs']; - if (maxOpenJobs !== null && maxOpenJobs > 0) { + const machineMemory = +body.nodes[k].attributes['ml.machine_memory']; + if (machineMemory !== null && machineMemory > 0) { count++; } } From e8663d4ea420cf8b01793e2a61d8963ff45c2b6a Mon Sep 17 00:00:00 2001 From: Tim Roes Date: Tue, 19 Oct 2021 16:43:23 +0200 Subject: [PATCH 48/53] [Discover] Show ignored field values (#115040) * WIP replacing indexPattern.flattenHit by tabify * Fix jest tests * Read metaFields from index pattern * Remove old test code * remove unnecessary changes * Remove flattenHitWrapper APIs * Fix imports * Fix missing metaFields * Add all meta fields to allowlist * Improve inline comments * Move flattenHit test to new implementation * Add deprecation comment to implementation * WIP - Show ignored field values * Disable filters in doc_table * remove redundant comments * No, it wasn't * start warning message * Enable ignored values in CSV reports * Add help tooltip * Better styling with warning plus collapsible button * Disable filtering within table for ignored values * Fix jest tests * Fix types in tests * Add more tests and documentation * Remove comment * Move dangerouslySetInnerHTML into helper method * Extract document formatting into common utility * Remove HTML source field formatter * Move formatHit to Discover * Change wording of ignored warning * Add cache for formatted hits * Remove dead type * Fix row_formatter for objects * Improve mobile layout * Fix jest tests * Fix typo * Remove additional span again * Change mock to revert test * Improve tests * More jest tests * Fix typo * Change wording * Remove dead comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/search/tabify/tabify_docs.test.ts | 49 ++++++++ .../data/common/search/tabify/tabify_docs.ts | 38 ++++++- .../data_views/common/data_views/data_view.ts | 13 --- .../common/data_views/format_hit.ts | 74 ------------ .../data_views/common/data_views/index.ts | 1 - src/plugins/data_views/public/index.ts | 2 +- src/plugins/discover/kibana.json | 3 +- .../public/__mocks__/index_pattern.ts | 18 ++- .../__mocks__/index_pattern_with_timefield.ts | 12 +- .../discover/public/__mocks__/services.ts | 11 +- .../apps/context/context_app.test.tsx | 4 + .../doc_table/components/table_row.tsx | 42 +++++-- .../doc_table/lib/row_formatter.test.ts | 86 ++++++++------ .../doc_table/lib/row_formatter.tsx | 36 ++---- .../layout/discover_documents.test.tsx | 5 + .../layout/discover_layout.test.tsx | 13 +++ ...ver_index_pattern_management.test.tsx.snap | 39 +------ .../sidebar/lib/field_calculator.js | 2 +- .../apps/main/utils/calc_field_counts.ts | 2 +- .../discover_grid/discover_grid.test.tsx | 5 + .../discover_grid/discover_grid.tsx | 6 +- .../get_render_cell_value.test.tsx | 98 ++++++++++++++-- .../discover_grid/get_render_cell_value.tsx | 55 ++++----- .../components/table/table.test.tsx | 34 +----- .../application/components/table/table.tsx | 12 +- .../components/table/table_cell_actions.tsx | 6 +- .../components/table/table_cell_value.tsx | 107 ++++++++++++++++-- .../components/table/table_columns.tsx | 17 ++- .../table/table_row_btn_filter_add.tsx | 2 +- .../table/table_row_btn_filter_remove.tsx | 2 +- .../application/helpers/format_hit.test.ts | 96 ++++++++++++++++ .../public/application/helpers/format_hit.ts | 67 +++++++++++ .../application/helpers/format_value.test.ts | 69 +++++++++++ .../application/helpers/format_value.ts | 39 +++++++ .../helpers/get_ignored_reason.test.ts | 54 +++++++++ .../application/helpers/get_ignored_reason.ts | 52 +++++++++ src/plugins/discover/public/build_services.ts | 3 + src/plugins/discover/public/plugin.tsx | 2 + .../common/converters/source.test.ts | 21 +--- .../common/converters/source.tsx | 55 +-------- src/plugins/field_formats/common/types.ts | 4 - .../generate_csv/generate_csv.ts | 2 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 44 files changed, 858 insertions(+), 402 deletions(-) delete mode 100644 src/plugins/data_views/common/data_views/format_hit.ts create mode 100644 src/plugins/discover/public/application/helpers/format_hit.test.ts create mode 100644 src/plugins/discover/public/application/helpers/format_hit.ts create mode 100644 src/plugins/discover/public/application/helpers/format_value.test.ts create mode 100644 src/plugins/discover/public/application/helpers/format_value.ts create mode 100644 src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts create mode 100644 src/plugins/discover/public/application/helpers/get_ignored_reason.ts diff --git a/src/plugins/data/common/search/tabify/tabify_docs.test.ts b/src/plugins/data/common/search/tabify/tabify_docs.test.ts index a2910a1be4a9..1964247b0958 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.test.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.test.ts @@ -41,6 +41,11 @@ function create(id: string) { }); } +const meta = { + _index: 'index-name', + _id: '1', +}; + describe('tabify_docs', () => { describe('flattenHit', () => { let indexPattern: DataView; @@ -70,6 +75,50 @@ describe('tabify_docs', () => { expect(Object.keys(response)).toEqual(expectedOrder); expect(Object.entries(response).map(([key]) => key)).toEqual(expectedOrder); }); + + it('does merge values from ignored_field_values and fields correctly', () => { + const flatten = flattenHit( + { + ...meta, + fields: { 'extension.keyword': ['foo'], extension: ['foo', 'ignored'] }, + ignored_field_values: { + 'extension.keyword': ['ignored'], + fully_ignored: ['some', 'value'], + }, + }, + indexPattern, + { includeIgnoredValues: true } + ); + expect(flatten).toHaveProperty(['extension.keyword'], ['foo', 'ignored']); + expect(flatten).toHaveProperty('extension', ['foo', 'ignored']); + expect(flatten).toHaveProperty('fully_ignored', ['some', 'value']); + }); + + it('does not merge values from ignored_field_values into _source', () => { + const flatten = flattenHit( + { + ...meta, + _source: { 'extension.keyword': ['foo', 'ignored'] }, + ignored_field_values: { 'extension.keyword': ['ignored'] }, + }, + indexPattern, + { includeIgnoredValues: true, source: true } + ); + expect(flatten).toHaveProperty(['extension.keyword'], ['foo', 'ignored']); + }); + + it('does merge ignored_field_values when no _source was present, even when parameter was on', () => { + const flatten = flattenHit( + { + ...meta, + fields: { 'extension.keyword': ['foo'] }, + ignored_field_values: { 'extension.keyword': ['ignored'] }, + }, + indexPattern, + { includeIgnoredValues: true, source: true } + ); + expect(flatten).toHaveProperty(['extension.keyword'], ['foo', 'ignored']); + }); }); describe('tabifyDocs', () => { diff --git a/src/plugins/data/common/search/tabify/tabify_docs.ts b/src/plugins/data/common/search/tabify/tabify_docs.ts index 425948877176..353a0c10ba12 100644 --- a/src/plugins/data/common/search/tabify/tabify_docs.ts +++ b/src/plugins/data/common/search/tabify/tabify_docs.ts @@ -55,8 +55,18 @@ export interface TabifyDocsOptions { * merged into the flattened document. */ source?: boolean; + /** + * If set to `true` values that have been ignored in ES (ignored_field_values) + * will be merged into the flattened document. This will only have an effect if + * the `hit` has been retrieved using the `fields` option. + */ + includeIgnoredValues?: boolean; } +// This is an overwrite of the SearchHit type to add the ignored_field_values. +// Can be removed once the estypes.SearchHit knows about ignored_field_values +type Hit = estypes.SearchHit & { ignored_field_values?: Record }; + /** * Flattens an individual hit (from an ES response) into an object. This will * create flattened field names, like `user.name`. @@ -65,11 +75,7 @@ export interface TabifyDocsOptions { * @param indexPattern The index pattern for the requested index if available. * @param params Parameters how to flatten the hit */ -export function flattenHit( - hit: estypes.SearchHit, - indexPattern?: IndexPattern, - params?: TabifyDocsOptions -) { +export function flattenHit(hit: Hit, indexPattern?: IndexPattern, params?: TabifyDocsOptions) { const flat = {} as Record; function flatten(obj: Record, keyPrefix: string = '') { @@ -109,6 +115,28 @@ export function flattenHit( flatten(hit.fields || {}); if (params?.source !== false && hit._source) { flatten(hit._source as Record); + } else if (params?.includeIgnoredValues && hit.ignored_field_values) { + // If enabled merge the ignored_field_values into the flattened hit. This will + // merge values that are not actually indexed by ES (i.e. ignored), e.g. because + // they were above the `ignore_above` limit or malformed for specific types. + // This API will only contain the values that were actually ignored, i.e. for the same + // field there might exist another value in the `fields` response, why this logic + // merged them both together. We do not merge this (even if enabled) in case source has been + // merged, since we would otherwise duplicate values, since ignore_field_values and _source + // contain the same values. + Object.entries(hit.ignored_field_values).forEach(([fieldName, fieldValue]) => { + if (flat[fieldName]) { + // If there was already a value from the fields API, make sure we're merging both together + if (Array.isArray(flat[fieldName])) { + flat[fieldName] = [...flat[fieldName], ...fieldValue]; + } else { + flat[fieldName] = [flat[fieldName], ...fieldValue]; + } + } else { + // If no previous value was assigned we can simply use the value from `ignored_field_values` as it is + flat[fieldName] = fieldValue; + } + }); } // Merge all valid meta fields into the flattened object diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index 57db127208dc..b7823677b70f 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -17,7 +17,6 @@ import { DuplicateField } from '../../../kibana_utils/common'; import { IIndexPattern, IFieldType } from '../../common'; import { DataViewField, IIndexPatternFieldList, fieldList } from '../fields'; -import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; import { FieldFormatsStartCommon, @@ -45,8 +44,6 @@ interface SavedObjectBody { type?: string; } -type FormatFieldFn = (hit: Record, fieldName: string) => any; - export class DataView implements IIndexPattern { public id?: string; public title: string = ''; @@ -67,11 +64,6 @@ export class DataView implements IIndexPattern { * Type is used to identify rollup index patterns */ public type: string | undefined; - public formatHit: { - (hit: Record, type?: string): any; - formatField: FormatFieldFn; - }; - public formatField: FormatFieldFn; /** * @deprecated Use `flattenHit` utility method exported from data plugin instead. */ @@ -103,11 +95,6 @@ export class DataView implements IIndexPattern { this.fields = fieldList([], this.shortDotsEnable); this.flattenHit = flattenHitWrapper(this, metaFields); - this.formatHit = formatHitProvider( - this, - fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) - ); - this.formatField = this.formatHit.formatField; // set values this.id = spec.id; diff --git a/src/plugins/data_views/common/data_views/format_hit.ts b/src/plugins/data_views/common/data_views/format_hit.ts deleted file mode 100644 index c8e6e8e33715..000000000000 --- a/src/plugins/data_views/common/data_views/format_hit.ts +++ /dev/null @@ -1,74 +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 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 _ from 'lodash'; -import { DataView } from './data_view'; -import { FieldFormatsContentType } from '../../../field_formats/common'; - -const formattedCache = new WeakMap(); -const partialFormattedCache = new WeakMap(); - -// Takes a hit, merges it with any stored/scripted fields, and with the metaFields -// returns a formatted version -export function formatHitProvider(dataView: DataView, defaultFormat: any) { - function convert( - hit: Record, - val: any, - fieldName: string, - type: FieldFormatsContentType = 'html' - ) { - const field = dataView.fields.getByName(fieldName); - const format = field ? dataView.getFormatterForField(field) : defaultFormat; - - return format.convert(val, type, { field, hit, indexPattern: dataView }); - } - - function formatHit(hit: Record, type: string = 'html') { - const cached = formattedCache.get(hit); - if (cached) { - return cached; - } - - // use and update the partial cache, but don't rewrite it. - // _source is stored in partialFormattedCache but not formattedCache - const partials = partialFormattedCache.get(hit) || {}; - partialFormattedCache.set(hit, partials); - - const cache: Record = {}; - formattedCache.set(hit, cache); - - _.forOwn(dataView.flattenHit(hit), function (val: any, fieldName?: string) { - // sync the formatted and partial cache - if (!fieldName) { - return; - } - const formatted = - partials[fieldName] == null ? convert(hit, val, fieldName) : partials[fieldName]; - cache[fieldName] = partials[fieldName] = formatted; - }); - - return cache; - } - - formatHit.formatField = function (hit: Record, fieldName: string) { - let partials = partialFormattedCache.get(hit); - if (partials && partials[fieldName] != null) { - return partials[fieldName]; - } - - if (!partials) { - partials = {}; - partialFormattedCache.set(hit, partials); - } - - const val = fieldName === '_source' ? hit._source : dataView.flattenHit(hit)[fieldName]; - return convert(hit, val, fieldName); - }; - - return formatHit; -} diff --git a/src/plugins/data_views/common/data_views/index.ts b/src/plugins/data_views/common/data_views/index.ts index 7c94dff961c9..d925d42fbea0 100644 --- a/src/plugins/data_views/common/data_views/index.ts +++ b/src/plugins/data_views/common/data_views/index.ts @@ -8,6 +8,5 @@ export * from './_pattern_cache'; export * from './flatten_hit'; -export * from './format_hit'; export * from './data_view'; export * from './data_views'; diff --git a/src/plugins/data_views/public/index.ts b/src/plugins/data_views/public/index.ts index 5c810ec1fd4c..3a6b5ccb237f 100644 --- a/src/plugins/data_views/public/index.ts +++ b/src/plugins/data_views/public/index.ts @@ -13,7 +13,7 @@ export { ILLEGAL_CHARACTERS, validateDataView, } from '../common/lib'; -export { formatHitProvider, onRedirectNoIndexPattern } from './data_views'; +export { onRedirectNoIndexPattern } from './data_views'; export { IndexPatternField, IIndexPatternFieldList, TypeMeta } from '../common'; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 3d5fdefd276d..791ce54a0cb1 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -8,6 +8,7 @@ "data", "embeddable", "inspector", + "fieldFormats", "kibanaLegacy", "urlForwarding", "navigation", @@ -16,7 +17,7 @@ "indexPatternFieldEditor" ], "optionalPlugins": ["home", "share", "usageCollection", "spaces"], - "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats", "dataViews"], + "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "dataViews"], "extraPublicDirs": ["common"], "owner": { "name": "Data Discovery", diff --git a/src/plugins/discover/public/__mocks__/index_pattern.ts b/src/plugins/discover/public/__mocks__/index_pattern.ts index 2acb512617a6..d33445baa0a2 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import type { estypes } from '@elastic/elasticsearch'; -import { flattenHit, IIndexPatternFieldList } from '../../../data/common'; +import { IIndexPatternFieldList } from '../../../data/common'; import { IndexPattern } from '../../../data/common'; const fields = [ @@ -28,6 +27,7 @@ const fields = [ { name: 'message', type: 'string', + displayName: 'message', scripted: false, filterable: false, aggregatable: false, @@ -35,6 +35,7 @@ const fields = [ { name: 'extension', type: 'string', + displayName: 'extension', scripted: false, filterable: true, aggregatable: true, @@ -42,6 +43,7 @@ const fields = [ { name: 'bytes', type: 'number', + displayName: 'bytesDisplayName', scripted: false, filterable: true, aggregatable: true, @@ -49,12 +51,14 @@ const fields = [ { name: 'scripted', type: 'number', + displayName: 'scripted', scripted: true, filterable: false, }, { name: 'object.value', type: 'number', + displayName: 'object.value', scripted: false, filterable: true, aggregatable: true, @@ -73,23 +77,15 @@ const indexPattern = { id: 'the-index-pattern-id', title: 'the-index-pattern-title', metaFields: ['_index', '_score'], - formatField: jest.fn(), - flattenHit: undefined, - formatHit: jest.fn((hit) => (hit.fields ? hit.fields : hit._source)), fields, getComputedFields: () => ({ docvalueFields: [], scriptFields: {}, storedFields: ['*'] }), getSourceFiltering: () => ({}), getFieldByName: jest.fn(() => ({})), timeFieldName: '', docvalueFields: [], - getFormatterForField: () => ({ convert: () => 'formatted' }), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), } as unknown as IndexPattern; indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; -indexPattern.formatField = (hit: Record, fieldName: string) => { - return fieldName === '_source' - ? hit._source - : flattenHit(hit as unknown as estypes.SearchHit, indexPattern)[fieldName]; -}; export const indexPatternMock = indexPattern; diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index 6cf8e8b3485f..906ebdebdd06 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { flattenHit, IIndexPatternFieldList } from '../../../data/common'; +import { IIndexPatternFieldList } from '../../../data/common'; import { IndexPattern } from '../../../data/common'; -import type { estypes } from '@elastic/elasticsearch'; const fields = [ { @@ -64,23 +63,16 @@ const indexPattern = { id: 'index-pattern-with-timefield-id', title: 'index-pattern-with-timefield', metaFields: ['_index', '_score'], - flattenHit: undefined, - formatHit: jest.fn((hit) => hit._source), fields, getComputedFields: () => ({}), getSourceFiltering: () => ({}), getFieldByName: (name: string) => fields.getByName(name), timeFieldName: 'timestamp', - getFormatterForField: () => ({ convert: () => 'formatted' }), + getFormatterForField: () => ({ convert: (value: unknown) => value }), isTimeNanosBased: () => false, popularizeField: () => {}, } as unknown as IndexPattern; indexPattern.isTimeBased = () => !!indexPattern.timeFieldName; -indexPattern.formatField = (hit: Record, fieldName: string) => { - return fieldName === '_source' - ? hit._source - : flattenHit(hit as unknown as estypes.SearchHit, indexPattern)[fieldName]; -}; export const indexPatternWithTimefieldMock = indexPattern; diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 8cc5ccf5aa12..6a90ed42417e 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -13,6 +13,7 @@ import { CONTEXT_STEP_SETTING, DEFAULT_COLUMNS_SETTING, DOC_HIDE_TIME_COLUMN_SETTING, + MAX_DOC_FIELDS_DISPLAYED, SAMPLE_SIZE_SETTING, SORT_DEFAULT_ORDER_SETTING, } from '../../common'; @@ -43,9 +44,13 @@ export const discoverServiceMock = { save: true, }, }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, filterManager: dataPlugin.query.filterManager, uiSettings: { - get: (key: string) => { + get: jest.fn((key: string) => { if (key === 'fields:popularLimit') { return 5; } else if (key === DEFAULT_COLUMNS_SETTING) { @@ -62,8 +67,10 @@ export const discoverServiceMock = { return false; } else if (key === SAMPLE_SIZE_SETTING) { return 250; + } else if (key === MAX_DOC_FIELDS_DISPLAYED) { + return 50; } - }, + }), isDefault: (key: string) => { return true; }, diff --git a/src/plugins/discover/public/application/apps/context/context_app.test.tsx b/src/plugins/discover/public/application/apps/context/context_app.test.tsx index 0e50f8f714a2..d1c557f2839b 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app.test.tsx @@ -62,6 +62,10 @@ describe('ContextApp test', () => { navigation: mockNavigationPlugin, core: { notifications: { toasts: [] } }, history: () => {}, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, filterManager: mockFilterManager, uiSettings: uiSettingsMock, } as unknown as DiscoverServices); diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx index d91735460af0..0bf4a36555d1 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -10,6 +10,7 @@ import React, { Fragment, useCallback, useMemo, useState } from 'react'; import classNames from 'classnames'; import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiIcon } from '@elastic/eui'; +import { formatFieldValue } from '../../../../../helpers/format_value'; import { flattenHit } from '../../../../../../../../data/common'; import { DocViewer } from '../../../../../components/doc_viewer/doc_viewer'; import { FilterManager, IndexPattern } from '../../../../../../../../data/public'; @@ -58,7 +59,10 @@ export const TableRow = ({ }); const anchorDocTableRowSubj = row.isAnchor ? ' docTableAnchorRow' : ''; - const flattenedRow = useMemo(() => flattenHit(row, indexPattern), [indexPattern, row]); + const flattenedRow = useMemo( + () => flattenHit(row, indexPattern, { includeIgnoredValues: true }), + [indexPattern, row] + ); const mapping = useMemo(() => indexPattern.fields.getByName, [indexPattern]); // toggle display of the rows details, a full list of the fields from each row @@ -68,13 +72,24 @@ export const TableRow = ({ * Fill an element with the value of a field */ const displayField = (fieldName: string) => { - const formattedField = indexPattern.formatField(row, fieldName); - - // field formatters take care of escaping - // eslint-disable-next-line react/no-danger - const fieldElement = ; + // If we're formatting the _source column, don't use the regular field formatter, + // but our Discover mechanism to format a hit in a better human-readable way. + if (fieldName === '_source') { + return formatRow(row, indexPattern, fieldsToShow); + } + + const formattedField = formatFieldValue( + flattenedRow[fieldName], + row, + indexPattern, + mapping(fieldName) + ); - return
{fieldElement}
; + return ( + // formatFieldValue always returns sanitized HTML + // eslint-disable-next-line react/no-danger +
+ ); }; const inlineFilter = useCallback( (column: string, type: '+' | '-') => { @@ -141,10 +156,9 @@ export const TableRow = ({ ); } else { columns.forEach(function (column: string) { - // when useNewFieldsApi is true, addressing to the fields property is safe - if (useNewFieldsApi && !mapping(column) && !row.fields![column]) { + if (useNewFieldsApi && !mapping(column) && row.fields && !row.fields[column]) { const innerColumns = Object.fromEntries( - Object.entries(row.fields!).filter(([key]) => { + Object.entries(row.fields).filter(([key]) => { return key.indexOf(`${column}.`) === 0; }) ); @@ -161,7 +175,13 @@ export const TableRow = ({ /> ); } else { - const isFilterable = Boolean(mapping(column)?.filterable && filter); + // Check whether the field is defined as filterable in the mapping and does + // NOT have ignored values in it to determine whether we want to allow filtering. + // We should improve this and show a helpful tooltip why the filter buttons are not + // there/disabled when there are ignored values. + const isFilterable = Boolean( + mapping(column)?.filterable && filter && !row._ignored?.includes(column) + ); rowCells.push( { const hit = { _id: 'a', + _index: 'foo', _type: 'doc', _score: 1, _source: { @@ -39,7 +40,7 @@ describe('Row formatter', () => { spec: { id, type, version, timeFieldName, fields: JSON.parse(fields), title }, fieldFormats: fieldFormatsMock, shortDotsEnable: false, - metaFields: [], + metaFields: ['_id', '_type', '_score'], }); }; @@ -47,26 +48,15 @@ describe('Row formatter', () => { const fieldsToShow = indexPattern.fields.getAll().map((fld) => fld.name); - // Realistic response with alphabetical insertion order - const formatHitReturnValue = { - also: 'with \\"quotes\\" or 'single qoutes'', - foo: 'bar', - number: '42', - hello: '<h1>World</h1>', - _id: 'a', - _type: 'doc', - _score: 1, - }; - - const formatHitMock = jest.fn().mockReturnValue(formatHitReturnValue); - beforeEach(() => { - // @ts-expect-error - indexPattern.formatHit = formatHitMock; setServices({ uiSettings: { get: () => 100, }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, } as unknown as DiscoverServices); }); @@ -77,32 +67,32 @@ describe('Row formatter', () => { Array [ Array [ "also", - "with \\\\"quotes\\\\" or 'single qoutes'", + "with \\"quotes\\" or 'single quotes'", ], Array [ "foo", "bar", ], Array [ - "number", - "42", + "hello", + "

World

", ], Array [ - "hello", - "<h1>World</h1>", + "number", + 42, ], Array [ "_id", "a", ], - Array [ - "_type", - "doc", - ], Array [ "_score", 1, ], + Array [ + "_type", + "doc", + ], ] } /> @@ -114,6 +104,10 @@ describe('Row formatter', () => { uiSettings: { get: () => 1, }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, } as unknown as DiscoverServices); expect(formatRow(hit, indexPattern, [])).toMatchInlineSnapshot(` { Array [ Array [ "also", - "with \\\\"quotes\\\\" or 'single qoutes'", + "with \\"quotes\\" or 'single quotes'", + ], + Array [ + "foo", + "bar", + ], + Array [ + "hello", + "

World

", + ], + Array [ + "number", + 42, + ], + Array [ + "_id", + "a", + ], + Array [ + "_score", + 1, + ], + Array [ + "_type", + "doc", ], ] } @@ -130,18 +148,18 @@ describe('Row formatter', () => { }); it('formats document with highlighted fields first', () => { - expect(formatRow({ ...hit, highlight: { number: '42' } }, indexPattern, fieldsToShow)) + expect(formatRow({ ...hit, highlight: { number: ['42'] } }, indexPattern, fieldsToShow)) .toMatchInlineSnapshot(` { ], Array [ "hello", - "<h1>World</h1>", + "

World

", ], Array [ "_id", "a", ], - Array [ - "_type", - "doc", - ], Array [ "_score", 1, ], + Array [ + "_type", + "doc", + ], ] } /> diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index 14cf1839107e..2702a232f21e 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -6,15 +6,17 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; import React, { Fragment } from 'react'; import type { IndexPattern } from 'src/plugins/data/common'; import { MAX_DOC_FIELDS_DISPLAYED } from '../../../../../../../common'; import { getServices } from '../../../../../../kibana_services'; +import { formatHit } from '../../../../../helpers/format_hit'; import './row_formatter.scss'; interface Props { - defPairs: Array<[string, unknown]>; + defPairs: Array<[string, string]>; } const TemplateComponent = ({ defPairs }: Props) => { return ( @@ -24,8 +26,8 @@ const TemplateComponent = ({ defPairs }: Props) => {
{pair[0]}:
{' '} ))} @@ -34,30 +36,12 @@ const TemplateComponent = ({ defPairs }: Props) => { }; export const formatRow = ( - // eslint-disable-next-line @typescript-eslint/no-explicit-any - hit: Record, + hit: estypes.SearchHit, indexPattern: IndexPattern, fieldsToShow: string[] ) => { - const highlights = hit?.highlight ?? {}; - // Keys are sorted in the hits object - const formatted = indexPattern.formatHit(hit); - const fields = indexPattern.fields; - const highlightPairs: Array<[string, unknown]> = []; - const sourcePairs: Array<[string, unknown]> = []; - Object.entries(formatted).forEach(([key, val]) => { - const displayKey = fields.getByName ? fields.getByName(key)?.displayName : undefined; - const pairs = highlights[key] ? highlightPairs : sourcePairs; - if (displayKey) { - if (fieldsToShow.includes(displayKey)) { - pairs.push([displayKey, val]); - } - } else { - pairs.push([key, val]); - } - }); - const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); - return ; + const pairs = formatHit(hit, indexPattern, fieldsToShow); + return ; }; export const formatTopLevelObject = ( @@ -68,8 +52,8 @@ export const formatTopLevelObject = ( indexPattern: IndexPattern ) => { const highlights = row.highlight ?? {}; - const highlightPairs: Array<[string, unknown]> = []; - const sourcePairs: Array<[string, unknown]> = []; + const highlightPairs: Array<[string, string]> = []; + const sourcePairs: Array<[string, string]> = []; const sorted = Object.entries(fields).sort(([keyA], [keyB]) => keyA.localeCompare(keyB)); sorted.forEach(([key, values]) => { const field = indexPattern.getFieldByName(key); diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx index e5212e877e8b..60540268dcd7 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_documents.test.tsx @@ -20,6 +20,11 @@ import { DiscoverDocuments } from './discover_documents'; import { ElasticSearchHit } from '../../../../doc_views/doc_views_types'; import { indexPatternMock } from '../../../../../__mocks__/index_pattern'; +jest.mock('../../../../../kibana_services', () => ({ + ...jest.requireActual('../../../../../kibana_services'), + getServices: () => jest.requireActual('../../../../../__mocks__/services').discoverServiceMock, +})); + setHeaderActionMenuMounter(jest.fn()); function getProps(fetchStatus: FetchStatus, hits: ElasticSearchHit[]) { diff --git a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx index 6ebed3185e2f..7e3252dce1ef 100644 --- a/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/layout/discover_layout.test.tsx @@ -33,6 +33,19 @@ import { RequestAdapter } from '../../../../../../../inspector'; import { Chart } from '../chart/point_series'; import { DiscoverSidebar } from '../sidebar/discover_sidebar'; +jest.mock('../../../../../kibana_services', () => ({ + ...jest.requireActual('../../../../../kibana_services'), + getServices: () => ({ + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, + uiSettings: { + get: jest.fn((key: string) => key === 'discover:maxDocFieldsDisplayed' && 50), + }, + }), +})); + setHeaderActionMenuMounter(jest.fn()); function getProps(indexPattern: IndexPattern, wasSidebarClosed?: boolean): DiscoverLayoutProps { diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap index ebb06e0b2ecd..02e2879476a5 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/__snapshots__/discover_index_pattern_management.test.tsx.snap @@ -115,42 +115,7 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` "deserialize": [MockFunction], "getByFieldType": [MockFunction], "getDefaultConfig": [MockFunction], - "getDefaultInstance": [MockFunction] { - "calls": Array [ - Array [ - "string", - ], - Array [ - "string", - ], - Array [ - "string", - ], - ], - "results": Array [ - Object { - "type": "return", - "value": Object { - "convert": [MockFunction], - "getConverterFor": [MockFunction], - }, - }, - Object { - "type": "return", - "value": Object { - "convert": [MockFunction], - "getConverterFor": [MockFunction], - }, - }, - Object { - "type": "return", - "value": Object { - "convert": [MockFunction], - "getConverterFor": [MockFunction], - }, - }, - ], - }, + "getDefaultInstance": [MockFunction], "getDefaultInstanceCacheResolver": [MockFunction], "getDefaultInstancePlain": [MockFunction], "getDefaultType": [MockFunction], @@ -651,8 +616,6 @@ exports[`Discover IndexPattern Management renders correctly 1`] = ` }, ], "flattenHit": [Function], - "formatField": [Function], - "formatHit": [Function], "getFieldAttrs": [Function], "getOriginalSavedObjectBody": [Function], "id": "logstash-*", diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js index be7e9c616273..c709f3311105 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/lib/field_calculator.js @@ -13,7 +13,7 @@ import { flattenHit } from '../../../../../../../../data/common'; function getFieldValues(hits, field, indexPattern) { const name = field.name; return map(hits, function (hit) { - return flattenHit(hit, indexPattern)[name]; + return flattenHit(hit, indexPattern, { includeIgnoredValues: true })[name]; }); } diff --git a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts index 211c4e5c8b06..2198d2f66b6b 100644 --- a/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts +++ b/src/plugins/discover/public/application/apps/main/utils/calc_field_counts.ts @@ -22,7 +22,7 @@ export function calcFieldCounts( return {}; } for (const hit of rows) { - const fields = Object.keys(flattenHit(hit, indexPattern)); + const fields = Object.keys(flattenHit(hit, indexPattern, { includeIgnoredValues: true })); for (const fieldName of fields) { counts[fieldName] = (counts[fieldName] || 0) + 1; } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx index b2be40c00820..22284480afc0 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.test.tsx @@ -19,6 +19,11 @@ import { DiscoverServices } from '../../../build_services'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { getDocId } from './discover_grid_document_selection'; +jest.mock('../../../kibana_services', () => ({ + ...jest.requireActual('../../../kibana_services'), + getServices: () => jest.requireActual('../../../__mocks__/services').discoverServiceMock, +})); + function getProps() { const servicesMock = { uiSettings: uiSettingsMock, diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index 11323080274a..ca403c813010 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -271,7 +271,11 @@ export const DiscoverGrid = ({ getRenderCellValueFn( indexPattern, displayedRows, - displayedRows ? displayedRows.map((hit) => flattenHit(hit, indexPattern)) : [], + displayedRows + ? displayedRows.map((hit) => + flattenHit(hit, indexPattern, { includeIgnoredValues: true }) + ) + : [], useNewFieldsApi, fieldsToShow, services.uiSettings.get(MAX_DOC_FIELDS_DISPLAYED) diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx index 655687621795..3fb96ba9e9da 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.test.tsx @@ -25,6 +25,9 @@ jest.mock('../../../kibana_services', () => ({ uiSettings: { get: jest.fn(), }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => (value ? value : '-') })), + }, }), })); @@ -102,7 +105,7 @@ describe('Discover grid cell rendering', function () { rowsSource, rowsSource.map(flatten), false, - [], + ['extension', 'bytes'], 100 ); const component = shallow( @@ -133,7 +136,7 @@ describe('Discover grid cell rendering', function () { } /> - bytes + bytesDisplayName + + _index + + + + _score + + `); }); @@ -196,7 +221,7 @@ describe('Discover grid cell rendering', function () { rowsFields, rowsFields.map(flatten), true, - [], + ['extension', 'bytes'], 100 ); const component = shallow( @@ -229,7 +254,7 @@ describe('Discover grid cell rendering', function () { } /> - bytes + bytesDisplayName + + _index + + + + _score + + `); }); @@ -251,7 +298,7 @@ describe('Discover grid cell rendering', function () { rowsFields, rowsFields.map(flatten), true, - [], + ['extension', 'bytes'], // this is the number of rendered items 1 ); @@ -284,6 +331,41 @@ describe('Discover grid cell rendering', function () { } } /> + + bytesDisplayName + + + + _index + + + + _score + + `); }); @@ -342,7 +424,7 @@ describe('Discover grid cell rendering', function () { rowsFieldsWithTopLevelObject, rowsFieldsWithTopLevelObject.map(flatten), true, - [], + ['object.value', 'extension', 'bytes'], 100 ); const component = shallow( @@ -368,7 +450,7 @@ describe('Discover grid cell rendering', function () { className="dscDiscoverGrid__descriptionListDescription" dangerouslySetInnerHTML={ Object { - "__html": "formatted", + "__html": "100", } } /> @@ -383,7 +465,7 @@ describe('Discover grid cell rendering', function () { rowsFieldsWithTopLevelObject, rowsFieldsWithTopLevelObject.map(flatten), true, - [], + ['extension', 'bytes', 'object.value'], 100 ); const component = shallow( diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index a05297158066..4066c13f6391 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -22,6 +22,8 @@ import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; import { EsHitRecord } from '../../types'; +import { formatFieldValue } from '../../helpers/format_value'; +import { formatHit } from '../../helpers/format_hit'; export const getRenderCellValueFn = ( @@ -145,39 +147,19 @@ export const getRenderCellValueFn = // eslint-disable-next-line @typescript-eslint/no-explicit-any return ; } - const formatted = indexPattern.formatHit(row); - - // Put the most important fields first - const highlights: Record = (row.highlight as Record) ?? {}; - const highlightPairs: Array<[string, string]> = []; - const sourcePairs: Array<[string, string]> = []; - Object.entries(formatted).forEach(([key, val]) => { - const pairs = highlights[key] ? highlightPairs : sourcePairs; - const displayKey = indexPattern.fields.getByName - ? indexPattern.fields.getByName(key)?.displayName - : undefined; - if (displayKey) { - if (fieldsToShow.includes(displayKey)) { - pairs.push([displayKey, val as string]); - } - } else { - pairs.push([key, val as string]); - } - }); + const pairs = formatHit(row, indexPattern, fieldsToShow); return ( - {[...highlightPairs, ...sourcePairs] - .slice(0, maxDocFieldsDisplayed) - .map(([key, value]) => ( - - {key} - - - ))} + {pairs.map(([key, value]) => ( + + {key} + + + ))} ); } @@ -191,12 +173,13 @@ export const getRenderCellValueFn = return {JSON.stringify(rowFlattened[columnId])}; } - const valueFormatted = indexPattern.formatField(row, columnId); - if (typeof valueFormatted === 'undefined') { - return -; - } return ( - // eslint-disable-next-line react/no-danger - + ); }; diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index ce914edcec70..e61333cce116 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -27,6 +27,10 @@ import { getServices } from '../../../kibana_services'; } }, }, + fieldFormats: { + getDefaultInstance: jest.fn(() => ({ convert: (value: unknown) => value })), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), + }, })); const indexPattern = { @@ -65,8 +69,7 @@ const indexPattern = { ], }, metaFields: ['_index', '_score'], - flattenHit: jest.fn(), - formatHit: jest.fn((hit) => hit._source), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), } as unknown as IndexPattern; indexPattern.fields.getByName = (name: string) => { @@ -359,32 +362,7 @@ describe('DocViewTable at Discover Doc with Fields API', () => { ], }, metaFields: ['_index', '_type', '_score', '_id'], - flattenHit: jest.fn((hit) => { - const result = {} as Record; - Object.keys(hit).forEach((key) => { - if (key !== 'fields') { - result[key] = hit[key]; - } else { - Object.keys(hit.fields).forEach((field) => { - result[field] = hit.fields[field]; - }); - } - }); - return result; - }), - formatHit: jest.fn((hit) => { - const result = {} as Record; - Object.keys(hit).forEach((key) => { - if (key !== 'fields') { - result[key] = hit[key]; - } else { - Object.keys(hit.fields).forEach((field) => { - result[field] = hit.fields[field]; - }); - } - }); - return result; - }), + getFormatterForField: jest.fn(() => ({ convert: (value: unknown) => value })), } as unknown as IndexPattern; indexPatterneCommerce.fields.getByName = (name: string) => { diff --git a/src/plugins/discover/public/application/components/table/table.tsx b/src/plugins/discover/public/application/components/table/table.tsx index e64dbd10f785..78a6d9ddd323 100644 --- a/src/plugins/discover/public/application/components/table/table.tsx +++ b/src/plugins/discover/public/application/components/table/table.tsx @@ -20,6 +20,8 @@ import { } from '../../doc_views/doc_views_types'; import { ACTIONS_COLUMN, MAIN_COLUMNS } from './table_columns'; import { getFieldsToShow } from '../../helpers/get_fields_to_show'; +import { getIgnoredReason, IgnoredReason } from '../../helpers/get_ignored_reason'; +import { formatFieldValue } from '../../helpers/format_value'; export interface DocViewerTableProps { columns?: string[]; @@ -46,6 +48,7 @@ export interface FieldRecord { }; value: { formattedValue: string; + ignored?: IgnoredReason; }; } @@ -64,8 +67,6 @@ export const DocViewerTable = ({ [indexPattern?.fields] ); - const formattedHit = useMemo(() => indexPattern?.formatHit(hit, 'html'), [hit, indexPattern]); - const tableColumns = useMemo(() => { return filter ? [ACTIONS_COLUMN, ...MAIN_COLUMNS] : MAIN_COLUMNS; }, [filter]); @@ -96,7 +97,7 @@ export const DocViewerTable = ({ return null; } - const flattened = flattenHit(hit, indexPattern, { source: true }); + const flattened = flattenHit(hit, indexPattern, { source: true, includeIgnoredValues: true }); const fieldsToShow = getFieldsToShow(Object.keys(flattened), indexPattern, showMultiFields); const items: FieldRecord[] = Object.keys(flattened) @@ -115,6 +116,8 @@ export const DocViewerTable = ({ const displayName = fieldMapping?.displayName ?? field; const fieldType = isNestedFieldParent(field, indexPattern) ? 'nested' : fieldMapping?.type; + const ignored = getIgnoredReason(fieldMapping ?? field, hit._ignored); + return { action: { onToggleColumn, @@ -130,7 +133,8 @@ export const DocViewerTable = ({ scripted: Boolean(fieldMapping?.scripted), }, value: { - formattedValue: formattedHit[field], + formattedValue: formatFieldValue(flattened[field], hit, indexPattern, fieldMapping), + ignored, }, }; }); diff --git a/src/plugins/discover/public/application/components/table/table_cell_actions.tsx b/src/plugins/discover/public/application/components/table/table_cell_actions.tsx index 7f2f87e7c296..e43a17448de2 100644 --- a/src/plugins/discover/public/application/components/table/table_cell_actions.tsx +++ b/src/plugins/discover/public/application/components/table/table_cell_actions.tsx @@ -21,6 +21,7 @@ interface TableActionsProps { fieldMapping?: IndexPatternField; onFilter: DocViewFilterFn; onToggleColumn: (field: string) => void; + ignoredValue: boolean; } export const TableActions = ({ @@ -30,15 +31,16 @@ export const TableActions = ({ flattenedField, onToggleColumn, onFilter, + ignoredValue, }: TableActionsProps) => { return (
onFilter(fieldMapping, flattenedField, '+')} /> onFilter(fieldMapping, flattenedField, '-')} /> ; +interface IgnoreWarningProps { + reason: IgnoredReason; + rawValue: unknown; +} -export const TableFieldValue = ({ formattedValue, field }: TableFieldValueProps) => { +const IgnoreWarning: React.FC = React.memo(({ rawValue, reason }) => { + const multiValue = Array.isArray(rawValue) && rawValue.length > 1; + + const getToolTipContent = (): string => { + switch (reason) { + case IgnoredReason.IGNORE_ABOVE: + return multiValue + ? i18n.translate('discover.docView.table.ignored.multiAboveTooltip', { + defaultMessage: `One or more values in this field are too long and can't be searched or filtered.`, + }) + : i18n.translate('discover.docView.table.ignored.singleAboveTooltip', { + defaultMessage: `The value in this field is too long and can't be searched or filtered.`, + }); + case IgnoredReason.MALFORMED: + return multiValue + ? i18n.translate('discover.docView.table.ignored.multiMalformedTooltip', { + defaultMessage: `This field has one or more malformed values that can't be searched or filtered.`, + }) + : i18n.translate('discover.docView.table.ignored.singleMalformedTooltip', { + defaultMessage: `The value in this field is malformed and can't be searched or filtered.`, + }); + case IgnoredReason.UNKNOWN: + return multiValue + ? i18n.translate('discover.docView.table.ignored.multiUnknownTooltip', { + defaultMessage: `One or more values in this field were ignored by Elasticsearch and can't be searched or filtered.`, + }) + : i18n.translate('discover.docView.table.ignored.singleUnknownTooltip', { + defaultMessage: `The value in this field was ignored by Elasticsearch and can't be searched or filtered.`, + }); + } + }; + + return ( + + + + + + + + {multiValue + ? i18n.translate('discover.docViews.table.ignored.multiValueLabel', { + defaultMessage: 'Contains ignored values', + }) + : i18n.translate('discover.docViews.table.ignored.singleValueLabel', { + defaultMessage: 'Ignored value', + })} + + + + + ); +}); + +type TableFieldValueProps = Pick & { + formattedValue: FieldRecord['value']['formattedValue']; + rawValue: unknown; + ignoreReason?: IgnoredReason; +}; + +export const TableFieldValue = ({ + formattedValue, + field, + rawValue, + ignoreReason, +}: TableFieldValueProps) => { const [fieldOpen, setFieldOpen] = useState(false); - const value = String(formattedValue); + const value = String(rawValue); const isCollapsible = value.length > COLLAPSE_LINE_LENGTH; const isCollapsed = isCollapsible && !fieldOpen; @@ -32,18 +111,26 @@ export const TableFieldValue = ({ formattedValue, field }: TableFieldValueProps) return ( - {isCollapsible && ( - + {(isCollapsible || ignoreReason) && ( + + {isCollapsible && ( + + + + )} + {ignoreReason && ( + + + + )} + )}
); diff --git a/src/plugins/discover/public/application/components/table/table_columns.tsx b/src/plugins/discover/public/application/components/table/table_columns.tsx index 5bd92fe9166e..5944f9bede64 100644 --- a/src/plugins/discover/public/application/components/table/table_columns.tsx +++ b/src/plugins/discover/public/application/components/table/table_columns.tsx @@ -31,7 +31,7 @@ export const ACTIONS_COLUMN: EuiBasicTableColumn = { ), render: ( { flattenedField, isActive, onFilter, onToggleColumn }: FieldRecord['action'], - { field: { field, fieldMapping } }: FieldRecord + { field: { field, fieldMapping }, value: { ignored } }: FieldRecord ) => { return ( = { flattenedField={flattenedField} onFilter={onFilter!} onToggleColumn={onToggleColumn} + ignoredValue={!!ignored} /> ); }, @@ -82,8 +83,18 @@ export const MAIN_COLUMNS: Array> = [ ), - render: ({ formattedValue }: FieldRecord['value'], { field: { field } }: FieldRecord) => { - return ; + render: ( + { formattedValue, ignored }: FieldRecord['value'], + { field: { field }, action: { flattenedField } }: FieldRecord + ) => { + return ( + + ); }, }, ]; diff --git a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx index 5fe1b4dc3334..de56d733442d 100644 --- a/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx +++ b/src/plugins/discover/public/application/components/table/table_row_btn_filter_add.tsx @@ -20,7 +20,7 @@ export function DocViewTableRowBtnFilterAdd({ onClick, disabled = false }: Props const tooltipContent = disabled ? ( ) : ( ) : ( ({ + getServices: () => jest.requireActual('../../__mocks__/services').discoverServiceMock, +})); + +describe('formatHit', () => { + let hit: estypes.SearchHit; + beforeEach(() => { + hit = { + _id: '1', + _index: 'logs', + fields: { + message: ['foobar'], + extension: ['png'], + 'object.value': [42, 13], + bytes: [123], + }, + }; + (dataViewMock.getFormatterForField as jest.Mock).mockReturnValue({ + convert: (value: unknown) => `formatted:${value}`, + }); + }); + + afterEach(() => { + (discoverServiceMock.uiSettings.get as jest.Mock).mockReset(); + }); + + it('formats a document as expected', () => { + const formatted = formatHit(hit, dataViewMock, ['message', 'extension', 'object.value']); + expect(formatted).toEqual([ + ['extension', 'formatted:png'], + ['message', 'formatted:foobar'], + ['object.value', 'formatted:42,13'], + ['_index', 'formatted:logs'], + ['_score', undefined], + ]); + }); + + it('orders highlighted fields first', () => { + const formatted = formatHit({ ...hit, highlight: { message: ['%%'] } }, dataViewMock, [ + 'message', + 'extension', + 'object.value', + ]); + expect(formatted.map(([fieldName]) => fieldName)).toEqual([ + 'message', + 'extension', + 'object.value', + '_index', + '_score', + ]); + }); + + it('only limits count of pairs based on advanced setting', () => { + (discoverServiceMock.uiSettings.get as jest.Mock).mockImplementation( + (key) => key === MAX_DOC_FIELDS_DISPLAYED && 2 + ); + const formatted = formatHit(hit, dataViewMock, ['message', 'extension', 'object.value']); + expect(formatted).toEqual([ + ['extension', 'formatted:png'], + ['message', 'formatted:foobar'], + ]); + }); + + it('should not include fields not mentioned in fieldsToShow', () => { + const formatted = formatHit(hit, dataViewMock, ['message', 'object.value']); + expect(formatted).toEqual([ + ['message', 'formatted:foobar'], + ['object.value', 'formatted:42,13'], + ['_index', 'formatted:logs'], + ['_score', undefined], + ]); + }); + + it('should filter fields based on their real name not displayName', () => { + const formatted = formatHit(hit, dataViewMock, ['bytes']); + expect(formatted).toEqual([ + ['bytesDisplayName', 'formatted:123'], + ['_index', 'formatted:logs'], + ['_score', undefined], + ]); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/format_hit.ts b/src/plugins/discover/public/application/helpers/format_hit.ts new file mode 100644 index 000000000000..3890973a3f3e --- /dev/null +++ b/src/plugins/discover/public/application/helpers/format_hit.ts @@ -0,0 +1,67 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { DataView, flattenHit } from '../../../../data/common'; +import { MAX_DOC_FIELDS_DISPLAYED } from '../../../common'; +import { getServices } from '../../kibana_services'; +import { formatFieldValue } from './format_value'; + +const formattedHitCache = new WeakMap(); + +type FormattedHit = Array<[fieldName: string, formattedValue: string]>; + +/** + * Returns a formatted document in form of key/value pairs of the fields name and a formatted value. + * The value returned in each pair is an HTML string which is safe to be applied to the DOM, since + * it's formatted using field formatters. + * @param hit The hit to format + * @param dataView The corresponding data view + * @param fieldsToShow A list of fields that should be included in the document summary. + */ +export function formatHit( + hit: estypes.SearchHit, + dataView: DataView, + fieldsToShow: string[] +): FormattedHit { + const cached = formattedHitCache.get(hit); + if (cached) { + return cached; + } + + const highlights = hit.highlight ?? {}; + // Flatten the object using the flattenHit implementation we use across Discover for flattening documents. + const flattened = flattenHit(hit, dataView, { includeIgnoredValues: true, source: true }); + + const highlightPairs: Array<[fieldName: string, formattedValue: string]> = []; + const sourcePairs: Array<[fieldName: string, formattedValue: string]> = []; + + // Add each flattened field into the corresponding array for highlighted or other fields, + // depending on whether the original hit had a highlight for it. That way we can later + // put highlighted fields first in the document summary. + Object.entries(flattened).forEach(([key, val]) => { + // Retrieve the (display) name of the fields, if it's a mapped field on the data view + const displayKey = dataView.fields.getByName(key)?.displayName; + const pairs = highlights[key] ? highlightPairs : sourcePairs; + // Format the raw value using the regular field formatters for that field + const formattedValue = formatFieldValue(val, hit, dataView, dataView.fields.getByName(key)); + // If the field was a mapped field, we validate it against the fieldsToShow list, if not + // we always include it into the result. + if (displayKey) { + if (fieldsToShow.includes(key)) { + pairs.push([displayKey, formattedValue]); + } + } else { + pairs.push([key, formattedValue]); + } + }); + const maxEntries = getServices().uiSettings.get(MAX_DOC_FIELDS_DISPLAYED); + const formatted = [...highlightPairs, ...sourcePairs].slice(0, maxEntries); + formattedHitCache.set(hit, formatted); + return formatted; +} diff --git a/src/plugins/discover/public/application/helpers/format_value.test.ts b/src/plugins/discover/public/application/helpers/format_value.test.ts new file mode 100644 index 000000000000..76d95c08e4a1 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/format_value.test.ts @@ -0,0 +1,69 @@ +/* + * 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 type { FieldFormat } from '../../../../field_formats/common'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { formatFieldValue } from './format_value'; + +import { getServices } from '../../kibana_services'; + +jest.mock('../../kibana_services', () => { + const services = { + fieldFormats: { + getDefaultInstance: jest.fn( + () => ({ convert: (value: unknown) => value } as FieldFormat) + ), + }, + }; + return { getServices: () => services }; +}); + +const hit = { + _id: '1', + _index: 'index', + fields: { + message: 'foo', + }, +}; + +describe('formatFieldValue', () => { + afterEach(() => { + (indexPatternMock.getFormatterForField as jest.Mock).mockReset(); + (getServices().fieldFormats.getDefaultInstance as jest.Mock).mockReset(); + }); + + it('should call correct fieldFormatter for field', () => { + const formatterForFieldMock = indexPatternMock.getFormatterForField as jest.Mock; + const convertMock = jest.fn((value: unknown) => `formatted:${value}`); + formatterForFieldMock.mockReturnValue({ convert: convertMock }); + const field = indexPatternMock.fields.getByName('message'); + expect(formatFieldValue('foo', hit, indexPatternMock, field)).toBe('formatted:foo'); + expect(indexPatternMock.getFormatterForField).toHaveBeenCalledWith(field); + expect(convertMock).toHaveBeenCalledWith('foo', 'html', { field, hit }); + }); + + it('should call default string formatter if no field specified', () => { + const convertMock = jest.fn((value: unknown) => `formatted:${value}`); + (getServices().fieldFormats.getDefaultInstance as jest.Mock).mockReturnValue({ + convert: convertMock, + }); + expect(formatFieldValue('foo', hit, indexPatternMock)).toBe('formatted:foo'); + expect(getServices().fieldFormats.getDefaultInstance).toHaveBeenCalledWith('string'); + expect(convertMock).toHaveBeenCalledWith('foo', 'html', { field: undefined, hit }); + }); + + it('should call default string formatter if no indexPattern is specified', () => { + const convertMock = jest.fn((value: unknown) => `formatted:${value}`); + (getServices().fieldFormats.getDefaultInstance as jest.Mock).mockReturnValue({ + convert: convertMock, + }); + expect(formatFieldValue('foo', hit)).toBe('formatted:foo'); + expect(getServices().fieldFormats.getDefaultInstance).toHaveBeenCalledWith('string'); + expect(convertMock).toHaveBeenCalledWith('foo', 'html', { field: undefined, hit }); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/format_value.ts b/src/plugins/discover/public/application/helpers/format_value.ts new file mode 100644 index 000000000000..cc3327679037 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/format_value.ts @@ -0,0 +1,39 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { DataView, DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; +import { getServices } from '../../kibana_services'; + +/** + * Formats the value of a specific field using the appropriate field formatter if available + * or the default string field formatter otherwise. + * + * @param value The value to format + * @param hit The actual search hit (required to get highlight information from) + * @param dataView The data view if available + * @param field The field that value was from if available + * @returns An sanitized HTML string, that is safe to be applied via dangerouslySetInnerHTML + */ +export function formatFieldValue( + value: unknown, + hit: estypes.SearchHit, + dataView?: DataView, + field?: DataViewField +): string { + if (!dataView || !field) { + // If either no field is available or no data view, we'll use the default + // string formatter to format that field. + return getServices() + .fieldFormats.getDefaultInstance(KBN_FIELD_TYPES.STRING) + .convert(value, 'html', { hit, field }); + } + + // If we have a data view and field we use that fields field formatter + return dataView.getFormatterForField(field).convert(value, 'html', { hit, field }); +} diff --git a/src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts b/src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts new file mode 100644 index 000000000000..13632ca5ed90 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_ignored_reason.test.ts @@ -0,0 +1,54 @@ +/* + * 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 { getIgnoredReason, IgnoredReason } from './get_ignored_reason'; +import { DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; + +function field(params: Partial): DataViewField { + return { + name: 'text', + type: 'keyword', + ...params, + } as unknown as DataViewField; +} + +describe('getIgnoredReason', () => { + it('will correctly return undefined when no value was ignored', () => { + expect(getIgnoredReason(field({ name: 'foo' }), undefined)).toBeUndefined(); + expect(getIgnoredReason(field({ name: 'foo' }), ['bar', 'baz'])).toBeUndefined(); + }); + + it('will return UNKNOWN if the field passed in was only a name, and thus no type information is present', () => { + expect(getIgnoredReason('foo', ['foo'])).toBe(IgnoredReason.UNKNOWN); + }); + + it('will return IGNORE_ABOVE for string types', () => { + expect(getIgnoredReason(field({ name: 'foo', type: KBN_FIELD_TYPES.STRING }), ['foo'])).toBe( + IgnoredReason.IGNORE_ABOVE + ); + }); + + // Each type that can have malformed values + [ + KBN_FIELD_TYPES.DATE, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.GEO_POINT, + KBN_FIELD_TYPES.GEO_SHAPE, + KBN_FIELD_TYPES.NUMBER, + ].forEach((type) => { + it(`will return MALFORMED for ${type} fields`, () => { + expect(getIgnoredReason(field({ name: 'foo', type }), ['foo'])).toBe(IgnoredReason.MALFORMED); + }); + }); + + it('will return unknown reasons if it does not know what the reason was', () => { + expect(getIgnoredReason(field({ name: 'foo', type: 'range' }), ['foo'])).toBe( + IgnoredReason.UNKNOWN + ); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/get_ignored_reason.ts b/src/plugins/discover/public/application/helpers/get_ignored_reason.ts new file mode 100644 index 000000000000..4d2fb85bdb2c --- /dev/null +++ b/src/plugins/discover/public/application/helpers/get_ignored_reason.ts @@ -0,0 +1,52 @@ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { DataViewField, KBN_FIELD_TYPES } from '../../../../data/common'; + +export enum IgnoredReason { + IGNORE_ABOVE = 'ignore_above', + MALFORMED = 'malformed', + UNKNOWN = 'unknown', +} + +/** + * Returns the reason why a specific field was ignored in the response. + * Will return undefined if the field had no ignored values in it. + * This implementation will make some assumptions based on specific types + * of ignored values can only happen with specific field types in Elasticsearch. + * + * @param field Either the data view field or the string name of it. + * @param ignoredFields The hit._ignored value of the hit to validate. + */ +export function getIgnoredReason( + field: DataViewField | string, + ignoredFields: estypes.SearchHit['_ignored'] +): IgnoredReason | undefined { + const fieldName = typeof field === 'string' ? field : field.name; + if (!ignoredFields?.includes(fieldName)) { + return undefined; + } + + if (typeof field === 'string') { + return IgnoredReason.UNKNOWN; + } + + switch (field.type) { + case KBN_FIELD_TYPES.STRING: + return IgnoredReason.IGNORE_ABOVE; + case KBN_FIELD_TYPES.NUMBER: + case KBN_FIELD_TYPES.DATE: + case KBN_FIELD_TYPES.GEO_POINT: + case KBN_FIELD_TYPES.GEO_SHAPE: + case KBN_FIELD_TYPES.IP: + return IgnoredReason.MALFORMED; + default: + return IgnoredReason.UNKNOWN; + } +} diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index ab2484abee89..ac16b6b3cc2b 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -36,6 +36,7 @@ import { KibanaLegacyStart } from '../../kibana_legacy/public'; import { UrlForwardingStart } from '../../url_forwarding/public'; import { NavigationPublicPluginStart } from '../../navigation/public'; import { IndexPatternFieldEditorStart } from '../../index_pattern_field_editor/public'; +import { FieldFormatsStart } from '../../field_formats/public'; import type { SpacesApi } from '../../../../x-pack/plugins/spaces/public'; @@ -49,6 +50,7 @@ export interface DiscoverServices { history: () => History; theme: ChartsPluginStart['theme']; filterManager: FilterManager; + fieldFormats: FieldFormatsStart; indexPatterns: IndexPatternsContract; inspector: InspectorPublicPluginStart; metadata: { branch: string }; @@ -82,6 +84,7 @@ export function buildServices( data: plugins.data, docLinks: core.docLinks, theme: plugins.charts.theme, + fieldFormats: plugins.fieldFormats, filterManager: plugins.data.query.filterManager, history: getHistory, indexPatterns: plugins.data.indexPatterns, diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index 6d30e6fd9e8a..e170e61f7ebc 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -61,6 +61,7 @@ import { IndexPatternFieldEditorStart } from '../../../plugins/index_pattern_fie import { DeferredSpinner } from './shared'; import { ViewSavedSearchAction } from './application/embeddable/view_saved_search_action'; import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; +import { FieldFormatsStart } from '../../field_formats/public'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -180,6 +181,7 @@ export interface DiscoverStartPlugins { navigation: NavigationStart; charts: ChartsPluginStart; data: DataPublicPluginStart; + fieldFormats: FieldFormatsStart; share?: SharePluginStart; kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart; diff --git a/src/plugins/field_formats/common/converters/source.test.ts b/src/plugins/field_formats/common/converters/source.test.ts index 298c93dac8c4..6f9e96a136d0 100644 --- a/src/plugins/field_formats/common/converters/source.test.ts +++ b/src/plugins/field_formats/common/converters/source.test.ts @@ -19,7 +19,7 @@ describe('Source Format', () => { convertHtml = source.getConverterFor(HTML_CONTEXT_TYPE) as HtmlContextTypeConvert; }); - test('should use the text content type if a field is not passed', () => { + test('should render stringified object', () => { const hit = { foo: 'bar', number: 42, @@ -27,23 +27,8 @@ describe('Source Format', () => { also: 'with "quotes" or \'single quotes\'', }; - expect(convertHtml(hit)).toBe( - '{"foo":"bar","number":42,"hello":"<h1>World</h1>","also":"with \\"quotes\\" or 'single quotes'"}' - ); - }); - - test('should render a description list if a field is passed', () => { - const hit = { - foo: 'bar', - number: 42, - hello: '

World

', - also: 'with "quotes" or \'single quotes\'', - }; - - expect( - convertHtml(hit, { field: 'field', indexPattern: { formatHit: (h: string) => h }, hit }) - ).toMatchInlineSnapshot( - `"
foo:
bar
number:
42
hello:

World

also:
with \\"quotes\\" or 'single quotes'
"` + expect(convertHtml(hit, { field: 'field', hit })).toMatchInlineSnapshot( + `"{"foo":"bar","number":42,"hello":"<h1>World</h1>","also":"with \\\\"quotes\\\\" or 'single quotes'"}"` ); }); }); diff --git a/src/plugins/field_formats/common/converters/source.tsx b/src/plugins/field_formats/common/converters/source.tsx index 1caffb5bfb9a..f92027ec0745 100644 --- a/src/plugins/field_formats/common/converters/source.tsx +++ b/src/plugins/field_formats/common/converters/source.tsx @@ -7,33 +7,8 @@ */ import { KBN_FIELD_TYPES } from '@kbn/field-types'; -import React, { Fragment } from 'react'; -import ReactDOM from 'react-dom/server'; -import { escape, keys } from 'lodash'; -import { shortenDottedString } from '../utils'; import { FieldFormat } from '../field_format'; -import { TextContextTypeConvert, HtmlContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; -import { FORMATS_UI_SETTINGS } from '../constants/ui_settings'; - -interface Props { - defPairs: Array<[string, string]>; -} -const TemplateComponent = ({ defPairs }: Props) => { - return ( -
- {defPairs.map((pair, idx) => ( - -
-
{' '} - - ))} -
- ); -}; +import { TextContextTypeConvert, FIELD_FORMAT_IDS } from '../types'; /** @public */ export class SourceFormat extends FieldFormat { @@ -42,32 +17,4 @@ export class SourceFormat extends FieldFormat { static fieldType = KBN_FIELD_TYPES._SOURCE; textConvert: TextContextTypeConvert = (value: string) => JSON.stringify(value); - - htmlConvert: HtmlContextTypeConvert = (value: string, options = {}) => { - const { field, hit, indexPattern } = options; - - if (!field) { - const converter = this.getConverterFor('text') as Function; - - return escape(converter(value)); - } - - const highlights: Record = (hit && hit.highlight) || {}; - // TODO: remove index pattern dependency - const formatted = hit ? indexPattern!.formatHit(hit) : {}; - const highlightPairs: Array<[string, string]> = []; - const sourcePairs: Array<[string, string]> = []; - const isShortDots = this.getConfig!(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE); - - keys(formatted).forEach((key) => { - const pairs = highlights[key] ? highlightPairs : sourcePairs; - const newField = isShortDots ? shortenDottedString(key) : key; - const val = formatted![key]; - pairs.push([newField as string, val]); - }, []); - - return ReactDOM.renderToStaticMarkup( - - ); - }; } diff --git a/src/plugins/field_formats/common/types.ts b/src/plugins/field_formats/common/types.ts index 00f9f5d707e8..6f0efebe389a 100644 --- a/src/plugins/field_formats/common/types.ts +++ b/src/plugins/field_formats/common/types.ts @@ -17,10 +17,6 @@ export type FieldFormatsContentType = 'html' | 'text'; */ export interface HtmlContextTypeOptions { field?: { name: string }; - // TODO: get rid of indexPattern dep completely - indexPattern?: { - formatHit: (hit: { highlight: Record }) => Record; - }; hit?: { highlight: Record }; } diff --git a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts index 6c2989d54309..77ad4fba1ab6 100644 --- a/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts +++ b/x-pack/plugins/reporting/server/export_types/csv_searchsource/generate_csv/generate_csv.ts @@ -356,7 +356,7 @@ export class CsvGenerator { let table: Datatable | undefined; try { - table = tabifyDocs(results, index, { shallow: true }); + table = tabifyDocs(results, index, { shallow: true, includeIgnoredValues: true }); } catch (err) { this.logger.error(err); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index aac0f651b8de..64258d42a4cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2424,7 +2424,6 @@ "discover.docViews.table.toggleFieldDetails": "フィールド詳細を切り替える", "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "メタフィールドの有無でフィルタリングできません", "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "スクリプトフィールドの有無でフィルタリングできません", - "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "インデックスされていないフィールドは検索できません", "discover.embeddable.inspectorRequestDataTitle": "データ", "discover.embeddable.inspectorRequestDescription": "このリクエストはElasticsearchにクエリをかけ、検索データを取得します。", "discover.embeddable.search.displayName": "検索", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 50d90f514458..ecc27bbe9dea 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2449,7 +2449,6 @@ "discover.docViews.table.toggleFieldDetails": "切换字段详细信息", "discover.docViews.table.unableToFilterForPresenceOfMetaFieldsTooltip": "无法筛选元数据字段是否存在", "discover.docViews.table.unableToFilterForPresenceOfScriptedFieldsTooltip": "无法筛选脚本字段是否存在", - "discover.docViews.table.unindexedFieldsCanNotBeSearchedTooltip": "无法搜索未索引字段", "discover.embeddable.inspectorRequestDataTitle": "数据", "discover.embeddable.inspectorRequestDescription": "此请求将查询 Elasticsearch 以获取搜索的数据。", "discover.embeddable.search.displayName": "搜索", From 96c89e0fcab42366078c35ecadbc31681a8bf834 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Tue, 19 Oct 2021 16:43:51 +0200 Subject: [PATCH 49/53] [Unified Integrations] Remove and cleanup add data views (#115424) Co-authored-by: cchaos Co-authored-by: Dave Snider Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Thomas Neirynck --- .../__snapshots__/home.test.tsx.snap | 422 +++++------------- .../application/components/home.test.tsx | 26 +- .../public/application/components/home.tsx | 8 +- .../public/application/components/home_app.js | 12 +- .../components/tutorial/tutorial.js | 13 +- .../components/tutorial_directory.js | 24 +- .../elastic_agent_card.test.tsx.snap | 246 ++++++---- .../no_data_card/elastic_agent_card.tsx | 39 +- .../apps/home/{_add_data.js => _add_data.ts} | 13 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 11 files changed, 356 insertions(+), 449 deletions(-) rename test/functional/apps/home/{_add_data.js => _add_data.ts} (59%) diff --git a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap index b6679dd7ba49..f38bdb9ac53f 100644 --- a/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap +++ b/src/plugins/home/public/application/components/__snapshots__/home.test.tsx.snap @@ -21,10 +21,28 @@ exports[`home change home route should render a link to change the default route /> - - - -`; - -exports[`home welcome should show the normal home page if loading fails 1`] = ` -, - } - } - template="empty" -> - - - - - -`; - -exports[`home welcome should show the normal home page if welcome screen is disabled locally 1`] = ` -, + application={ + Object { + "capabilities": Object { + "navLinks": Object { + "integrations": true, + }, + }, + } } - } - template="empty" -> - - - - -`; - -exports[`home welcome should show the welcome screen if enabled, and there are no index patterns defined 1`] = ` - -`; - -exports[`home welcome stores skip welcome setting if skipped 1`] = ` -, - } - } - template="empty" -> - - - ({ getServices: () => ({ getBasePath: () => 'path', @@ -22,6 +24,13 @@ jest.mock('../kibana_services', () => ({ chrome: { setBreadcrumbs: () => {}, }, + application: { + capabilities: { + navLinks: { + integrations: mockHasIntegrationsPermission, + }, + }, + }, }), })); @@ -35,6 +44,7 @@ describe('home', () => { let defaultProps: HomeProps; beforeEach(() => { + mockHasIntegrationsPermission = true; defaultProps = { directories: [], solutions: [], @@ -182,7 +192,7 @@ describe('home', () => { expect(defaultProps.localStorage.getItem).toHaveBeenCalledTimes(1); - expect(component).toMatchSnapshot(); + expect(component.find(Welcome).exists()).toBe(true); }); test('stores skip welcome setting if skipped', async () => { @@ -196,7 +206,7 @@ describe('home', () => { expect(defaultProps.localStorage.setItem).toHaveBeenCalledWith('home:welcome:show', 'false'); - expect(component).toMatchSnapshot(); + expect(component.find(Welcome).exists()).toBe(false); }); test('should show the normal home page if loading fails', async () => { @@ -205,7 +215,7 @@ describe('home', () => { const hasUserIndexPattern = jest.fn(() => Promise.reject('Doh!')); const component = await renderHome({ hasUserIndexPattern }); - expect(component).toMatchSnapshot(); + expect(component.find(Welcome).exists()).toBe(false); }); test('should show the normal home page if welcome screen is disabled locally', async () => { @@ -213,7 +223,15 @@ describe('home', () => { const component = await renderHome(); - expect(component).toMatchSnapshot(); + expect(component.find(Welcome).exists()).toBe(false); + }); + + test("should show the normal home page if user doesn't have access to integrations", async () => { + mockHasIntegrationsPermission = false; + + const component = await renderHome(); + + expect(component.find(Welcome).exists()).toBe(false); }); }); diff --git a/src/plugins/home/public/application/components/home.tsx b/src/plugins/home/public/application/components/home.tsx index d398311d3025..2a08754889c2 100644 --- a/src/plugins/home/public/application/components/home.tsx +++ b/src/plugins/home/public/application/components/home.tsx @@ -45,10 +45,10 @@ export class Home extends Component { constructor(props: HomeProps) { super(props); - const isWelcomeEnabled = !( - getServices().homeConfig.disableWelcomeScreen || - props.localStorage.getItem(KEY_ENABLE_WELCOME) === 'false' - ); + const isWelcomeEnabled = + !getServices().homeConfig.disableWelcomeScreen && + getServices().application.capabilities.navLinks.integrations && + props.localStorage.getItem(KEY_ENABLE_WELCOME) !== 'false'; const body = document.querySelector('body')!; body.classList.add('isHomPage'); diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index b0ba4d46646d..1dbcaa6f50fa 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -17,8 +17,11 @@ import { getTutorial } from '../load_tutorials'; import { replaceTemplateStrings } from './tutorial/replace_template_strings'; import { getServices } from '../kibana_services'; +const REDIRECT_TO_INTEGRATIONS_TAB_IDS = ['all', 'logging', 'metrics', 'security']; + export function HomeApp({ directories, solutions }) { const { + application, savedObjectsClient, getBasePath, addBasePath, @@ -30,10 +33,17 @@ export function HomeApp({ directories, solutions }) { const isCloudEnabled = environment.cloud; const renderTutorialDirectory = (props) => { + // Redirect to integrations app unless a specific tab that is still supported was specified. + const tabId = props.match.params.tab; + if (!tabId || REDIRECT_TO_INTEGRATIONS_TAB_IDS.includes(tabId)) { + application.navigateToApp('integrations', { replace: true }); + return null; + } + return ( ); diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 508a236bf45d..4af5e362baca 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -26,9 +26,8 @@ const INSTRUCTIONS_TYPE = { ON_PREM_ELASTIC_CLOUD: 'onPremElasticCloud', }; -const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); -const addDataTitle = i18n.translate('home.breadcrumbs.addDataTitle', { - defaultMessage: 'Add data', +const integrationsTitle = i18n.translate('home.breadcrumbs.integrationsAppTitle', { + defaultMessage: 'Integrations', }); class TutorialUi extends React.Component { @@ -80,12 +79,8 @@ class TutorialUi extends React.Component { getServices().chrome.setBreadcrumbs([ { - text: homeTitle, - href: '#/', - }, - { - text: addDataTitle, - href: '#/tutorial_directory', + text: integrationsTitle, + href: this.props.addBasePath('/app/integrations/browse'), }, { text: tutorial ? tutorial.name : this.props.tutorialId, diff --git a/src/plugins/home/public/application/components/tutorial_directory.js b/src/plugins/home/public/application/components/tutorial_directory.js index 83e629a7c891..ac0d1524145a 100644 --- a/src/plugins/home/public/application/components/tutorial_directory.js +++ b/src/plugins/home/public/application/components/tutorial_directory.js @@ -18,12 +18,10 @@ import { getServices } from '../kibana_services'; import { KibanaPageTemplate } from '../../../../kibana_react/public'; import { getTutorials } from '../load_tutorials'; -const ALL_TAB_ID = 'all'; const SAMPLE_DATA_TAB_ID = 'sampleData'; -const homeTitle = i18n.translate('home.breadcrumbs.homeTitle', { defaultMessage: 'Home' }); -const addDataTitle = i18n.translate('home.breadcrumbs.addDataTitle', { - defaultMessage: 'Add data', +const integrationsTitle = i18n.translate('home.breadcrumbs.integrationsAppTitle', { + defaultMessage: 'Integrations', }); class TutorialDirectoryUi extends React.Component { @@ -48,7 +46,7 @@ class TutorialDirectoryUi extends React.Component { })), ]; - let openTab = ALL_TAB_ID; + let openTab = SAMPLE_DATA_TAB_ID; if ( props.openTab && this.tabs.some((tab) => { @@ -72,10 +70,9 @@ class TutorialDirectoryUi extends React.Component { getServices().chrome.setBreadcrumbs([ { - text: homeTitle, - href: '#/', + text: integrationsTitle, + href: this.props.addBasePath(`/app/integrations/browse`), }, - { text: addDataTitle }, ]); const tutorialConfigs = await getTutorials(); @@ -155,6 +152,15 @@ class TutorialDirectoryUi extends React.Component { renderTabContent = () => { const tab = this.tabs.find(({ id }) => id === this.state.selectedTabId); if (tab?.content) { + getServices().chrome.setBreadcrumbs([ + { + text: integrationsTitle, + href: this.props.addBasePath(`/app/integrations/browse`), + }, + { + text: tab.name, + }, + ]); return tab.content; } @@ -163,7 +169,7 @@ class TutorialDirectoryUi extends React.Component { {this.state.tutorialCards .filter((tutorial) => { return ( - this.state.selectedTabId === ALL_TAB_ID || + this.state.selectedTabId === SAMPLE_DATA_TAB_ID || this.state.selectedTabId === tutorial.category ); }) diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap index f66d05140b2e..8e1d0cb92e00 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/__snapshots__/elastic_agent_card.test.tsx.snap @@ -1,117 +1,177 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ElasticAgentCard props button 1`] = ` - - Button - + - - Add Elastic Agent - - - } -/> +> + + Button + + } + href="/app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } + /> + `; exports[`ElasticAgentCard props category 1`] = ` - - Add Elastic Agent - + - +> + Add Elastic Agent - - - } -/> + + } + href="/app/integrations/browse/custom" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } + /> + `; exports[`ElasticAgentCard props href 1`] = ` - - Button - + - - Add Elastic Agent - - - } -/> +> + + Button + + } + href="#" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } + /> + `; exports[`ElasticAgentCard props recommended 1`] = ` - - Add Elastic Agent - + - +> + Add Elastic Agent - - - } -/> + + } + href="/app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } + /> + `; exports[`ElasticAgentCard renders 1`] = ` - - Add Elastic Agent - + - +> + Add Elastic Agent - - - } -/> + + } + href="/app/integrations/browse" + image="/plugins/kibanaReact/assets/elastic_agent_card.svg" + paddingSize="l" + title={ + + + Add Elastic Agent + + + } + /> + `; diff --git a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx index 5a91e568471d..b9d412fe4df8 100644 --- a/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx +++ b/src/plugins/kibana_react/public/page_template/no_data_page/no_data_card/elastic_agent_card.tsx @@ -12,6 +12,7 @@ import { CoreStart } from 'kibana/public'; import { EuiButton, EuiCard, EuiTextColor, EuiScreenReaderOnly } from '@elastic/eui'; import { useKibana } from '../../../context'; import { NoDataPageActions, NO_DATA_RECOMMENDED } from '../no_data_page'; +import { RedirectAppLinks } from '../../../app_links'; export type ElasticAgentCardProps = NoDataPageActions & { solution: string; @@ -76,23 +77,25 @@ export const ElasticAgentCard: FunctionComponent = ({ ); return ( - - {defaultCTAtitle} - - } - description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', { - defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`, - })} - betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} - footer={footer} - layout={layout as 'vertical' | undefined} - {...cardRest} - /> + + + {defaultCTAtitle} + + } + description={i18n.translate('kibana-react.noDataPage.elasticAgentCard.description', { + defaultMessage: `Use Elastic Agent for a simple, unified way to collect data from your machines.`, + })} + betaBadgeLabel={recommended ? NO_DATA_RECOMMENDED : undefined} + footer={footer} + layout={layout as 'vertical' | undefined} + {...cardRest} + /> + ); }; diff --git a/test/functional/apps/home/_add_data.js b/test/functional/apps/home/_add_data.ts similarity index 59% rename from test/functional/apps/home/_add_data.js rename to test/functional/apps/home/_add_data.ts index c69e0a02c26e..3fd69c1a488f 100644 --- a/test/functional/apps/home/_add_data.js +++ b/test/functional/apps/home/_add_data.ts @@ -6,20 +6,15 @@ * Side Public License, v 1. */ -import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; -export default function ({ getService, getPageObjects }) { - const retry = getService('retry'); +export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'home', 'dashboard']); describe('add data tutorials', function describeIndexTests() { - it('directory should display registered tutorials', async () => { + it('directory should redirect to integrations app', async () => { await PageObjects.common.navigateToUrl('home', 'tutorial_directory', { useActualUrl: true }); - await PageObjects.header.waitUntilLoadingHasFinished(); - await retry.try(async () => { - const tutorialExists = await PageObjects.home.doesSynopsisExist('netflowlogs'); - expect(tutorialExists).to.be(true); - }); + await PageObjects.common.waitUntilUrlIncludes('/app/integrations'); }); }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 64258d42a4cc..f831f3b91eaa 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2914,7 +2914,6 @@ "home.addData.sampleDataButtonLabel": "サンプルデータを試す", "home.addData.sectionTitle": "データを追加して開始する", "home.addData.text": "データの操作を開始するには、多数の取り込みオプションのいずれかを使用します。アプリまたはサービスからデータを収集するか、ファイルをアップロードします。独自のデータを使用する準備ができていない場合は、サンプルデータセットを追加してください。", - "home.breadcrumbs.addDataTitle": "データの追加", "home.breadcrumbs.homeTitle": "ホーム", "home.dataManagementDisableCollection": " 収集を停止するには、", "home.dataManagementDisableCollectionLink": "ここで使用状況データを無効にします。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ecc27bbe9dea..5fbcd26340be 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2943,7 +2943,6 @@ "home.addData.sampleDataButtonLabel": "试用样例数据", "home.addData.sectionTitle": "首先添加您的数据", "home.addData.text": "要开始使用您的数据,请使用我们众多采集选项中的一个选项。从应用或服务收集数据,或上传文件。如果未准备好使用自己的数据,请添加示例数据集。", - "home.breadcrumbs.addDataTitle": "添加数据", "home.breadcrumbs.homeTitle": "主页", "home.dataManagementDisableCollection": " 要停止收集,", "home.dataManagementDisableCollectionLink": "请在此禁用使用情况数据。", From e4fb118fee9302d65e696c40adc3c44ff44c5a27 Mon Sep 17 00:00:00 2001 From: Joe Portner <5295965+jportner@users.noreply.github.com> Date: Tue, 19 Oct 2021 10:44:15 -0400 Subject: [PATCH 50/53] Change deleteByNamespace to include legacy URL aliases (#115459) --- .../object_types/registration.ts | 1 + .../service/lib/repository.test.js | 7 ++- .../saved_objects/service/lib/repository.ts | 16 +++++- .../common/lib/space_test_utils.ts | 2 + .../common/suites/delete.ts | 53 +++++++++---------- 5 files changed, 47 insertions(+), 32 deletions(-) diff --git a/src/core/server/saved_objects/object_types/registration.ts b/src/core/server/saved_objects/object_types/registration.ts index 6ef4f79ef77c..ce1089674717 100644 --- a/src/core/server/saved_objects/object_types/registration.ts +++ b/src/core/server/saved_objects/object_types/registration.ts @@ -17,6 +17,7 @@ const legacyUrlAliasType: SavedObjectsType = { properties: { sourceId: { type: 'keyword' }, targetType: { type: 'keyword' }, + targetNamespace: { type: 'keyword' }, resolveCounter: { type: 'long' }, disabled: { type: 'boolean' }, // other properties exist, but we aren't querying or aggregating on those, so we don't need to specify them (because we use `dynamic: false` above) diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 82a0dd71700f..84359147fccb 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -26,6 +26,7 @@ import { encodeHitVersion } from '../../version'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { DocumentMigrator } from '../../migrations/core/document_migrator'; import { mockKibanaMigrator } from '../../migrations/kibana/kibana_migrator.mock'; +import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import { elasticsearchClientMock } from '../../../elasticsearch/client/mocks'; import * as esKuery from '@kbn/es-query'; import { errors as EsErrors } from '@elastic/elasticsearch'; @@ -2714,7 +2715,11 @@ describe('SavedObjectsRepository', () => { const allTypes = registry.getAllTypes().map((type) => type.name); expect(getSearchDslNS.getSearchDsl).toHaveBeenCalledWith(mappings, registry, { namespaces: [namespace], - type: allTypes.filter((type) => !registry.isNamespaceAgnostic(type)), + type: [ + ...allTypes.filter((type) => !registry.isNamespaceAgnostic(type)), + LEGACY_URL_ALIAS_TYPE, + ], + kueryNode: expect.anything(), }); }); }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index e522d770b3f5..c081c5991140 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -8,6 +8,7 @@ import { omit, isObject } from 'lodash'; import type { estypes } from '@elastic/elasticsearch'; +import * as esKuery from '@kbn/es-query'; import type { ElasticsearchClient } from '../../../elasticsearch/'; import { isSupportedEsServer, isNotFoundFromUnsupportedServer } from '../../../elasticsearch'; import type { Logger } from '../../../logging'; @@ -55,6 +56,7 @@ import { SavedObjectsBulkResolveObject, SavedObjectsBulkResolveResponse, } from '../saved_objects_client'; +import { LEGACY_URL_ALIAS_TYPE } from '../../object_types'; import { SavedObject, SavedObjectsBaseOptions, @@ -780,7 +782,16 @@ export class SavedObjectsRepository { } const allTypes = Object.keys(getRootPropertiesObjects(this._mappings)); - const typesToUpdate = allTypes.filter((type) => !this._registry.isNamespaceAgnostic(type)); + const typesToUpdate = [ + ...allTypes.filter((type) => !this._registry.isNamespaceAgnostic(type)), + LEGACY_URL_ALIAS_TYPE, + ]; + + // Construct kueryNode to filter legacy URL aliases (these space-agnostic objects do not use root-level "namespace/s" fields) + const { buildNode } = esKuery.nodeTypes.function; + const match1 = buildNode('is', `${LEGACY_URL_ALIAS_TYPE}.targetNamespace`, namespace); + const match2 = buildNode('not', buildNode('is', 'type', LEGACY_URL_ALIAS_TYPE)); + const kueryNode = buildNode('or', [match1, match2]); const { body, statusCode, headers } = await this.client.updateByQuery( { @@ -803,8 +814,9 @@ export class SavedObjectsRepository { }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { - namespaces: namespace ? [namespace] : undefined, + namespaces: [namespace], type: typesToUpdate, + kueryNode, }), }, }, diff --git a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts index 28b19d5db20b..c047a741e35d 100644 --- a/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts +++ b/x-pack/test/spaces_api_integration/common/lib/space_test_utils.ts @@ -50,6 +50,8 @@ export function getAggregatedSpaceData(es: KibanaClient, objectTypes: string[]) emit(doc["namespaces"].value); } else if (doc["namespace"].size() > 0) { emit(doc["namespace"].value); + } else if (doc["legacy-url-alias.targetNamespace"].size() > 0) { + emit(doc["legacy-url-alias.targetNamespace"].value); } `, }, diff --git a/x-pack/test/spaces_api_integration/common/suites/delete.ts b/x-pack/test/spaces_api_integration/common/suites/delete.ts index aaca4fa843d6..4bf44d88db8e 100644 --- a/x-pack/test/spaces_api_integration/common/suites/delete.ts +++ b/x-pack/test/spaces_api_integration/common/suites/delete.ts @@ -48,6 +48,7 @@ export function deleteTestSuiteFactory( 'dashboard', 'space', 'index-pattern', + 'legacy-url-alias', // TODO: add assertions for config objects -- these assertions were removed because of flaky behavior in #92358, but we should // consider adding them again at some point, especially if we convert config objects to `namespaceType: 'multiple-isolated'` in // the future. @@ -56,6 +57,10 @@ export function deleteTestSuiteFactory( // @ts-expect-error @elastic/elasticsearch doesn't defined `count.buckets`. const buckets = response.aggregations?.count.buckets; + // The test fixture contains three legacy URL aliases: + // (1) one for "space_1", (2) one for "space_2", and (3) one for "other_space", which is a non-existent space. + // Each test deletes "space_2", so the agg buckets should reflect that aliases (1) and (3) still exist afterwards. + // Space 2 deleted, all others should exist const expectedBuckets = [ { @@ -65,47 +70,37 @@ export function deleteTestSuiteFactory( doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ - { - key: 'visualization', - doc_count: 3, - }, - { - key: 'dashboard', - doc_count: 2, - }, - { - key: 'space', - doc_count: 2, - }, - { - key: 'index-pattern', - doc_count: 1, - }, + { key: 'visualization', doc_count: 3 }, + { key: 'dashboard', doc_count: 2 }, + { key: 'space', doc_count: 2 }, // since space objects are namespace-agnostic, they appear in the "default" agg bucket + { key: 'index-pattern', doc_count: 1 }, + // legacy-url-alias objects cannot exist for the default space ], }, }, { - doc_count: 6, + doc_count: 7, key: 'space_1', countByType: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [ - { - key: 'visualization', - doc_count: 3, - }, - { - key: 'dashboard', - doc_count: 2, - }, - { - key: 'index-pattern', - doc_count: 1, - }, + { key: 'visualization', doc_count: 3 }, + { key: 'dashboard', doc_count: 2 }, + { key: 'index-pattern', doc_count: 1 }, + { key: 'legacy-url-alias', doc_count: 1 }, // alias (1) ], }, }, + { + doc_count: 1, + key: 'other_space', + countByType: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [{ key: 'legacy-url-alias', doc_count: 1 }], // alias (3) + }, + }, ]; expect(buckets).to.eql(expectedBuckets); From 92e1cd25b7fd8145689cb68d3b21a70f80c6b504 Mon Sep 17 00:00:00 2001 From: James Gowdy Date: Tue, 19 Oct 2021 15:51:30 +0100 Subject: [PATCH 51/53] [ML] Adding ability to change data view in advanced job wizard (#115191) * [ML] Adding ability to change data view in advanced job wizard * updating translation ids * type and text changes * code clean up * route id change * text changes * text change * changing data view to index pattern * adding api tests * text updates * removing first step * renaming temp variable * adding permission checks Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/ml/common/types/job_validation.ts | 14 + .../plugins/ml/common/types/saved_objects.ts | 2 +- .../contexts/kibana/use_navigate_to_path.ts | 2 +- .../new_job/common/job_creator/job_creator.ts | 4 + .../common/job_creator/util/general.ts | 8 +- .../json_editor_flyout/json_editor_flyout.tsx | 2 +- .../components/data_view/change_data_view.tsx | 326 ++++++++++++++++++ .../data_view/change_data_view_button.tsx | 36 ++ .../components/data_view/description.tsx | 32 ++ .../components/data_view/index.ts | 8 + .../components/datafeed_step/datafeed.tsx | 2 + .../services/ml_api_service/index.ts | 12 +- .../ml/server/models/job_validation/index.ts | 4 + .../models/job_validation/job_validation.ts | 6 +- .../validate_datafeed_preview.ts | 29 +- x-pack/plugins/ml/server/routes/apidoc.json | 2 + .../ml/server/routes/job_validation.ts | 44 ++- .../routes/schemas/job_validation_schema.ts | 7 + .../apis/ml/data_frame_analytics/delete.ts | 4 +- .../datafeed_preview_validation.ts | 175 ++++++++++ .../apis/ml/job_validation/index.ts | 1 + x-pack/test/functional/services/ml/api.ts | 10 +- 22 files changed, 711 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/ml/common/types/job_validation.ts create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view_button.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx create mode 100644 x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/index.ts create mode 100644 x-pack/test/api_integration/apis/ml/job_validation/datafeed_preview_validation.ts diff --git a/x-pack/plugins/ml/common/types/job_validation.ts b/x-pack/plugins/ml/common/types/job_validation.ts new file mode 100644 index 000000000000..0c1db63ff376 --- /dev/null +++ b/x-pack/plugins/ml/common/types/job_validation.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { ErrorType } from '../util/errors'; + +export interface DatafeedValidationResponse { + valid: boolean; + documentsFound: boolean; + error?: ErrorType; +} diff --git a/x-pack/plugins/ml/common/types/saved_objects.ts b/x-pack/plugins/ml/common/types/saved_objects.ts index 0e48800dd845..e376fddbe627 100644 --- a/x-pack/plugins/ml/common/types/saved_objects.ts +++ b/x-pack/plugins/ml/common/types/saved_objects.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ErrorType } from '../util/errors'; +import type { ErrorType } from '../util/errors'; export type JobType = 'anomaly-detector' | 'data-frame-analytics'; export const ML_SAVED_OBJECT_TYPE = 'ml-job'; export const ML_MODULE_SAVED_OBJECT_TYPE = 'ml-module'; diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts index 951d9d6dfded..00050803b97c 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_navigate_to_path.ts @@ -23,7 +23,7 @@ export const useNavigateToPath = () => { const location = useLocation(); return useCallback( - async (path: string | undefined, preserveSearch = false) => { + async (path: string | undefined, preserveSearch: boolean = false) => { if (path === undefined) return; const modifiedPath = `${path}${preserveSearch === true ? location.search : ''}`; /** diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts index a44b4bdef60c..607a4fcf9a73 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/job_creator.ts @@ -502,6 +502,10 @@ export class JobCreator { return this._datafeed_config.indices; } + public set indices(indics: string[]) { + this._datafeed_config.indices = indics; + } + public get scriptFields(): Field[] { return this._scriptFields; } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts index 78903e64686f..46315ac3b02d 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/general.ts @@ -258,17 +258,21 @@ export function convertToMultiMetricJob( jobCreator.createdBy = CREATED_BY_LABEL.MULTI_METRIC; jobCreator.modelPlot = false; stashJobForCloning(jobCreator, true, true); - navigateToPath(`jobs/new_job/${JOB_TYPE.MULTI_METRIC}`, true); } export function convertToAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.createdBy = null; stashJobForCloning(jobCreator, true, true); - navigateToPath(`jobs/new_job/${JOB_TYPE.ADVANCED}`, true); } +export function resetAdvancedJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { + jobCreator.createdBy = null; + stashJobForCloning(jobCreator, true, false); + navigateToPath('/jobs/new_job'); +} + export function resetJob(jobCreator: JobCreatorType, navigateToPath: NavigateToPath) { jobCreator.jobId = ''; stashJobForCloning(jobCreator, true, true); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx index ce71cd80e45c..9e5d1ac5eef6 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/common/json_editor_flyout/json_editor_flyout.tsx @@ -204,7 +204,7 @@ export const JsonEditorFlyout: FC = ({ isDisabled, jobEditorMode, datafee > diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx new file mode 100644 index 000000000000..c402ee4bf979 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view.tsx @@ -0,0 +1,326 @@ +/* + * 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, { FC, useState, useEffect, useCallback, useContext } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { i18n } from '@kbn/i18n'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiModal, + EuiButton, + EuiCallOut, + EuiSpacer, + EuiModalHeader, + EuiLoadingSpinner, + EuiModalHeaderTitle, + EuiModalBody, +} from '@elastic/eui'; + +import { JobCreatorContext } from '../../../job_creator_context'; +import { AdvancedJobCreator } from '../../../../../common/job_creator'; +import { resetAdvancedJob } from '../../../../../common/job_creator/util/general'; +import { + CombinedJob, + Datafeed, +} from '../../../../../../../../../common/types/anomaly_detection_jobs'; +import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; +import type { DatafeedValidationResponse } from '../../../../../../../../../common/types/job_validation'; + +import { SavedObjectFinderUi } from '../../../../../../../../../../../../src/plugins/saved_objects/public'; +import { + useMlKibana, + useMlApiContext, + useNavigateToPath, +} from '../../../../../../../contexts/kibana'; + +const fixedPageSize: number = 8; + +enum STEP { + PICK_DATA_VIEW, + VALIDATE, +} + +interface Props { + onClose: () => void; +} + +export const ChangeDataViewModal: FC = ({ onClose }) => { + const { + services: { + savedObjects, + uiSettings, + data: { dataViews }, + }, + } = useMlKibana(); + const navigateToPath = useNavigateToPath(); + const { validateDatafeedPreview } = useMlApiContext(); + + const { jobCreator: jc } = useContext(JobCreatorContext); + const jobCreator = jc as AdvancedJobCreator; + + const [validating, setValidating] = useState(false); + const [step, setStep] = useState(STEP.PICK_DATA_VIEW); + + const [currentDataViewTitle, setCurrentDataViewTitle] = useState(''); + const [newDataViewTitle, setNewDataViewTitle] = useState(''); + const [validationResponse, setValidationResponse] = useState( + null + ); + + useEffect(function initialPageLoad() { + setCurrentDataViewTitle(jobCreator.indexPatternTitle); + }, []); + + useEffect( + function stepChange() { + if (step === STEP.PICK_DATA_VIEW) { + setValidationResponse(null); + } + }, + [step] + ); + + function onDataViewSelected(dataViewId: string) { + if (validating === false) { + setStep(STEP.VALIDATE); + validate(dataViewId); + } + } + + const validate = useCallback( + async (dataViewId: string) => { + setValidating(true); + + const { title } = await dataViews.get(dataViewId); + setNewDataViewTitle(title); + + const indices = title.split(','); + if (jobCreator.detectors.length) { + const datafeed: Datafeed = { ...jobCreator.datafeedConfig, indices }; + const resp = await validateDatafeedPreview({ + job: { + ...jobCreator.jobConfig, + datafeed_config: datafeed, + } as CombinedJob, + }); + setValidationResponse(resp); + } + setValidating(false); + }, + [dataViews, validateDatafeedPreview, jobCreator] + ); + + const applyDataView = useCallback(() => { + const newIndices = newDataViewTitle.split(','); + jobCreator.indices = newIndices; + resetAdvancedJob(jobCreator, navigateToPath); + }, [jobCreator, newDataViewTitle, navigateToPath]); + + return ( + <> + + + + + + + + + {step === STEP.PICK_DATA_VIEW && ( + <> + + + + + 'indexPatternApp', + name: i18n.translate( + 'xpack.ml.newJob.wizard.datafeedStep.dataView.step1.dataView', + { + defaultMessage: 'Index pattern', + } + ), + }, + ]} + fixedPageSize={fixedPageSize} + uiSettings={uiSettings} + savedObjects={savedObjects} + /> + + )} + {step === STEP.VALIDATE && ( + <> + + + + + {validating === true ? ( + <> + + + + ) : ( + + )} + + + + + + + + + + + applyDataView()} + isDisabled={validating} + data-test-subj="mlJobsImportButton" + > + + + + + + )} + + + + ); +}; + +const ValidationMessage: FC<{ + validationResponse: DatafeedValidationResponse | null; + dataViewTitle: string; +}> = ({ validationResponse, dataViewTitle }) => { + if (validationResponse === null) { + return ( + + + + ); + } + if (validationResponse.valid === true) { + if (validationResponse.documentsFound === true) { + return ( + + + + ); + } else { + return ( + + + + ); + } + } else { + return ( + + + + + + + + + + {validationResponse.error ? extractErrorMessage(validationResponse.error) : null} + + ); + } +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view_button.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view_button.tsx new file mode 100644 index 000000000000..dc9af26236d8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/change_data_view_button.tsx @@ -0,0 +1,36 @@ +/* + * 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, { FC, useState } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiButtonEmpty } from '@elastic/eui'; +import { Description } from './description'; +import { ChangeDataViewModal } from './change_data_view'; + +export const ChangeDataView: FC<{ isDisabled: boolean }> = ({ isDisabled }) => { + const [showFlyout, setShowFlyout] = useState(false); + + return ( + <> + {showFlyout && } + + + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx new file mode 100644 index 000000000000..2632660738a5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/description.tsx @@ -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 React, { memo, FC } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiDescribedFormGroup, EuiFormRow } from '@elastic/eui'; + +export const Description: FC = memo(({ children }) => { + const title = i18n.translate('xpack.ml.newJob.wizard.datafeedStep.dataView.title', { + defaultMessage: 'Index pattern', + }); + return ( + {title}} + description={ + + } + > + + <>{children} + + + ); +}); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/index.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/index.ts new file mode 100644 index 000000000000..ef7c451b4889 --- /dev/null +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/components/data_view/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 { ChangeDataView } from './change_data_view_button'; diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx index 77db2eb2419c..47e488ab201e 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/datafeed_step/datafeed.tsx @@ -14,6 +14,7 @@ import { FrequencyInput } from './components/frequency'; import { ScrollSizeInput } from './components/scroll_size'; import { ResetQueryButton } from './components/reset_query'; import { TimeField } from './components/time_field'; +import { ChangeDataView } from './components/data_view'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; @@ -46,6 +47,7 @@ export const DatafeedStep: FC = ({ setCurrentStep, isCurrentStep }) = + diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 883e5d499c3d..720e54e386cb 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -43,6 +43,7 @@ import type { FieldHistogramRequestConfig } from '../../datavisualizer/index_bas import type { DataRecognizerConfigResponse, Module } from '../../../../common/types/modules'; import { getHttp } from '../../util/dependency_cache'; import type { RuntimeMappings } from '../../../../common/types/fields'; +import type { DatafeedValidationResponse } from '../../../../common/types/job_validation'; export interface MlInfoResponse { defaults: MlServerDefaults; @@ -194,7 +195,7 @@ export function mlApiServicesProvider(httpService: HttpService) { }, validateJob(payload: { - job: Job; + job: CombinedJob; duration: { start?: number; end?: number; @@ -209,6 +210,15 @@ export function mlApiServicesProvider(httpService: HttpService) { }); }, + validateDatafeedPreview(payload: { job: CombinedJob }) { + const body = JSON.stringify(payload); + return httpService.http({ + path: `${basePath()}/validate/datafeed_preview`, + method: 'POST', + body, + }); + }, + validateCardinality$(job: CombinedJob): Observable { const body = JSON.stringify(job); return httpService.http$({ diff --git a/x-pack/plugins/ml/server/models/job_validation/index.ts b/x-pack/plugins/ml/server/models/job_validation/index.ts index 92d3e7d613ef..a527b9dcf3d4 100644 --- a/x-pack/plugins/ml/server/models/job_validation/index.ts +++ b/x-pack/plugins/ml/server/models/job_validation/index.ts @@ -7,3 +7,7 @@ export { validateJob } from './job_validation'; export { validateCardinality } from './validate_cardinality'; +export { + validateDatafeedPreviewWithMessages, + validateDatafeedPreview, +} from './validate_datafeed_preview'; diff --git a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts index 838f188455d4..4cd2d8a95ee7 100644 --- a/x-pack/plugins/ml/server/models/job_validation/job_validation.ts +++ b/x-pack/plugins/ml/server/models/job_validation/job_validation.ts @@ -17,7 +17,7 @@ import { basicJobValidation, uniqWithIsEqual } from '../../../common/util/job_ut import { validateBucketSpan } from './validate_bucket_span'; import { validateCardinality } from './validate_cardinality'; import { validateInfluencers } from './validate_influencers'; -import { validateDatafeedPreview } from './validate_datafeed_preview'; +import { validateDatafeedPreviewWithMessages } from './validate_datafeed_preview'; import { validateModelMemoryLimit } from './validate_model_memory_limit'; import { validateTimeRange, isValidTimeField } from './validate_time_range'; import { validateJobSchema } from '../../routes/schemas/job_validation_schema'; @@ -111,7 +111,9 @@ export async function validateJob( validationMessages.push({ id: 'missing_summary_count_field_name' }); } - validationMessages.push(...(await validateDatafeedPreview(mlClient, authHeader, job))); + validationMessages.push( + ...(await validateDatafeedPreviewWithMessages(mlClient, authHeader, job)) + ); } else { validationMessages = basicValidation.messages; validationMessages.push({ id: 'skipped_extended_tests' }); diff --git a/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts index 4ae94229a930..0775de7ae0e1 100644 --- a/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts +++ b/x-pack/plugins/ml/server/models/job_validation/validate_datafeed_preview.ts @@ -9,12 +9,25 @@ import type { MlClient } from '../../lib/ml_client'; import type { AuthorizationHeader } from '../../lib/request_authorization'; import type { CombinedJob } from '../../../common/types/anomaly_detection_jobs'; import type { JobValidationMessage } from '../../../common/constants/messages'; +import type { DatafeedValidationResponse } from '../../../common/types/job_validation'; -export async function validateDatafeedPreview( +export async function validateDatafeedPreviewWithMessages( mlClient: MlClient, authHeader: AuthorizationHeader, job: CombinedJob ): Promise { + const { valid, documentsFound } = await validateDatafeedPreview(mlClient, authHeader, job); + if (valid) { + return documentsFound ? [] : [{ id: 'datafeed_preview_no_documents' }]; + } + return [{ id: 'datafeed_preview_failed' }]; +} + +export async function validateDatafeedPreview( + mlClient: MlClient, + authHeader: AuthorizationHeader, + job: CombinedJob +): Promise { const { datafeed_config: datafeed, ...tempJob } = job; try { const { body } = (await mlClient.previewDatafeed( @@ -28,11 +41,15 @@ export async function validateDatafeedPreview( // previewDatafeed response type is incorrect )) as unknown as { body: unknown[] }; - if (Array.isArray(body) === false || body.length === 0) { - return [{ id: 'datafeed_preview_no_documents' }]; - } - return []; + return { + valid: true, + documentsFound: Array.isArray(body) && body.length > 0, + }; } catch (error) { - return [{ id: 'datafeed_preview_failed' }]; + return { + valid: false, + documentsFound: false, + error: error.body ?? error, + }; } } diff --git a/x-pack/plugins/ml/server/routes/apidoc.json b/x-pack/plugins/ml/server/routes/apidoc.json index 7f53ebb92b68..226b69e06b48 100644 --- a/x-pack/plugins/ml/server/routes/apidoc.json +++ b/x-pack/plugins/ml/server/routes/apidoc.json @@ -123,11 +123,13 @@ "GetJobAuditMessages", "GetAllJobAuditMessages", "ClearJobAuditMessages", + "JobValidation", "EstimateBucketSpan", "CalculateModelMemoryLimit", "ValidateCardinality", "ValidateJob", + "ValidateDataFeedPreview", "DatafeedService", "CreateDatafeed", diff --git a/x-pack/plugins/ml/server/routes/job_validation.ts b/x-pack/plugins/ml/server/routes/job_validation.ts index b75eab20e7bc..bceb59fa33fc 100644 --- a/x-pack/plugins/ml/server/routes/job_validation.ts +++ b/x-pack/plugins/ml/server/routes/job_validation.ts @@ -16,12 +16,18 @@ import { modelMemoryLimitSchema, validateCardinalitySchema, validateJobSchema, + validateDatafeedPreviewSchema, } from './schemas/job_validation_schema'; import { estimateBucketSpanFactory } from '../models/bucket_span_estimator'; import { calculateModelMemoryLimitProvider } from '../models/calculate_model_memory_limit'; -import { validateJob, validateCardinality } from '../models/job_validation'; +import { + validateJob, + validateCardinality, + validateDatafeedPreview, +} from '../models/job_validation'; import { getAuthorizationHeader } from '../lib/request_authorization'; import type { MlClient } from '../lib/ml_client'; +import { CombinedJob } from '../../common/types/anomaly_detection_jobs'; type CalculateModelMemoryLimitPayload = TypeOf; @@ -205,4 +211,40 @@ export function jobValidationRoutes({ router, mlLicense, routeGuard }: RouteInit } }) ); + + /** + * @apiGroup DataFeedPreviewValidation + * + * @api {post} /api/ml/validate/datafeed_preview Validates datafeed preview + * @apiName ValidateDataFeedPreview + * @apiDescription Validates that the datafeed preview runs successfully and produces results + * + * @apiSchema (body) validateDatafeedPreviewSchema + */ + router.post( + { + path: '/api/ml/validate/datafeed_preview', + validate: { + body: validateDatafeedPreviewSchema, + }, + options: { + tags: ['access:ml:canCreateJob'], + }, + }, + routeGuard.fullLicenseAPIGuard(async ({ client, mlClient, request, response }) => { + try { + const resp = await validateDatafeedPreview( + mlClient, + getAuthorizationHeader(request), + request.body.job as CombinedJob + ); + + return response.ok({ + body: resp, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts index a83bbbff6cec..a481713f6735 100644 --- a/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/job_validation_schema.ts @@ -60,6 +60,13 @@ export const validateJobSchema = schema.object({ }), }); +export const validateDatafeedPreviewSchema = schema.object({ + job: schema.object({ + ...anomalyDetectionJobSchema, + datafeed_config: datafeedConfigSchema, + }), +}); + export const validateCardinalitySchema = schema.object({ ...anomalyDetectionJobSchema, datafeed_config: datafeedConfigSchema, diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts index 055b4b69ab7a..e7ea71863352 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts @@ -130,7 +130,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestinationIndex(analyticsId); before(async () => { - await ml.api.createIndices(destinationIndex); + await ml.api.createIndex(destinationIndex); await ml.api.assertIndicesExist(destinationIndex); }); @@ -189,7 +189,7 @@ export default ({ getService }: FtrProviderContext) => { before(async () => { // Mimic real job by creating target index & index pattern after DFA job is created - await ml.api.createIndices(destinationIndex); + await ml.api.createIndex(destinationIndex); await ml.api.assertIndicesExist(destinationIndex); await ml.testResources.createIndexPatternIfNeeded(destinationIndex); }); diff --git a/x-pack/test/api_integration/apis/ml/job_validation/datafeed_preview_validation.ts b/x-pack/test/api_integration/apis/ml/job_validation/datafeed_preview_validation.ts new file mode 100644 index 000000000000..c16050e08c88 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/job_validation/datafeed_preview_validation.ts @@ -0,0 +1,175 @@ +/* + * 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. + */ +/* + * 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 { estypes } from '@elastic/elasticsearch'; +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; + +const farequoteMappings: estypes.MappingTypeMapping = { + properties: { + '@timestamp': { + type: 'date', + }, + airline: { + type: 'keyword', + }, + responsetime: { + type: 'float', + }, + }, +}; + +function getBaseJobConfig() { + return { + job_id: 'test', + description: '', + analysis_config: { + bucket_span: '15m', + detectors: [ + { + function: 'mean', + field_name: 'responsetime', + }, + ], + influencers: [], + }, + analysis_limits: { + model_memory_limit: '11MB', + }, + data_description: { + time_field: '@timestamp', + time_format: 'epoch_ms', + }, + model_plot_config: { + enabled: false, + annotations_enabled: false, + }, + model_snapshot_retention_days: 10, + daily_model_snapshot_retention_after_days: 1, + allow_lazy_open: false, + datafeed_config: { + query: { + bool: { + must: [ + { + match_all: {}, + }, + ], + }, + }, + indices: ['ft_farequote'], + scroll_size: 1000, + delayed_data_check_config: { + enabled: true, + }, + job_id: 'test', + datafeed_id: 'datafeed-test', + }, + }; +} + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + describe('Validate datafeed preview', function () { + before(async () => { + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + await ml.testResources.setKibanaTimeZoneToUTC(); + await ml.api.createIndex('farequote_empty', farequoteMappings); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices('farequote_empty'); + }); + + it(`should validate a job with documents`, async () => { + const job = getBaseJobConfig(); + + const { body } = await supertest + .post('/api/ml/validate/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job }) + .expect(200); + + expect(body.valid).to.eql(true, `valid should be true, but got ${body.valid}`); + expect(body.documentsFound).to.eql( + true, + `documentsFound should be true, but got ${body.documentsFound}` + ); + }); + + it(`should fail to validate a job with documents and non-existent field`, async () => { + const job = getBaseJobConfig(); + job.analysis_config.detectors[0].field_name = 'no_such_field'; + + const { body } = await supertest + .post('/api/ml/validate/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job }) + .expect(200); + + expect(body.valid).to.eql(false, `valid should be false, but got ${body.valid}`); + expect(body.documentsFound).to.eql( + false, + `documentsFound should be false, but got ${body.documentsFound}` + ); + }); + + it(`should validate a job with no documents`, async () => { + const job = getBaseJobConfig(); + job.datafeed_config.indices = ['farequote_empty']; + + const { body } = await supertest + .post('/api/ml/validate/datafeed_preview') + .auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job }) + .expect(200); + + expect(body.valid).to.eql(true, `valid should be true, but got ${body.valid}`); + expect(body.documentsFound).to.eql( + false, + `documentsFound should be false, but got ${body.documentsFound}` + ); + }); + + it(`should fail for viewer user`, async () => { + const job = getBaseJobConfig(); + + await supertest + .post('/api/ml/validate/datafeed_preview') + .auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER)) + .set(COMMON_REQUEST_HEADERS) + .send({ job }) + .expect(403); + }); + + it(`should fail for unauthorized user`, async () => { + const job = getBaseJobConfig(); + + await supertest + .post('/api/ml/validate/datafeed_preview') + .auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED)) + .set(COMMON_REQUEST_HEADERS) + .send({ job }) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/ml/job_validation/index.ts b/x-pack/test/api_integration/apis/ml/job_validation/index.ts index 4b75102d7b0b..be07ae3b1852 100644 --- a/x-pack/test/api_integration/apis/ml/job_validation/index.ts +++ b/x-pack/test/api_integration/apis/ml/job_validation/index.ts @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./calculate_model_memory_limit')); loadTestFile(require.resolve('./cardinality')); loadTestFile(require.resolve('./validate')); + loadTestFile(require.resolve('./datafeed_preview_validation')); }); } diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index abde3bf36538..6ffd95f213c4 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -126,14 +126,20 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { ); }, - async createIndices(indices: string) { + async createIndex( + indices: string, + mappings?: Record | estypes.MappingTypeMapping + ) { log.debug(`Creating indices: '${indices}'...`); if ((await es.indices.exists({ index: indices, allow_no_indices: false })).body === true) { log.debug(`Indices '${indices}' already exist. Nothing to create.`); return; } - const { body } = await es.indices.create({ index: indices }); + const { body } = await es.indices.create({ + index: indices, + ...(mappings ? { body: { mappings } } : {}), + }); expect(body) .to.have.property('acknowledged') .eql(true, 'Response for create request indices should be acknowledged.'); From b306f8e2c31c96623d310fdd0dc110e7935b8993 Mon Sep 17 00:00:00 2001 From: DeDe Morton Date: Tue, 19 Oct 2021 08:01:49 -0700 Subject: [PATCH 52/53] Update UI links to Fleet and Agent docs (#115295) * Update UI links to Fleet and Agent docs * Update link service * Fix merge problem * Update link service Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/kibana-plugin-core-public.doclinksstart.links.md | 1 + .../core/public/kibana-plugin-core-public.doclinksstart.md | 2 +- src/core/public/doc_links/doc_links_service.ts | 4 +++- src/core/public/public.api.md | 1 + .../components/enrollment_instructions/manual/index.tsx | 2 +- 5 files changed, 7 insertions(+), 3 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index e79bc7a0db02..73efed79324f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -237,6 +237,7 @@ readonly links: { elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; + installElasticAgent: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index d90972d32704..fdf469f443f2 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
readonly troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index a07e12eae8d7..91ad18544798 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -477,9 +477,10 @@ export class DocLinksService { settings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, settingsFleetServerHostSettings: `${FLEET_DOCS}fleet-settings.html#fleet-server-hosts-setting`, troubleshooting: `${FLEET_DOCS}fleet-troubleshooting.html`, - elasticAgent: `${FLEET_DOCS}elastic-agent-installation-configuration.html`, + elasticAgent: `${FLEET_DOCS}elastic-agent-installation.html`, datastreams: `${FLEET_DOCS}data-streams.html`, datastreamsNamingScheme: `${FLEET_DOCS}data-streams.html#data-streams-naming-scheme`, + installElasticAgent: `${FLEET_DOCS}install-fleet-managed-elastic-agent.html`, upgradeElasticAgent: `${FLEET_DOCS}upgrade-elastic-agent.html`, upgradeElasticAgent712lower: `${FLEET_DOCS}upgrade-elastic-agent.html#upgrade-7.12-lower`, learnMoreBlog: `${ELASTIC_WEBSITE_URL}blog/elastic-agent-and-fleet-make-it-easier-to-integrate-your-systems-with-elastic`, @@ -740,6 +741,7 @@ export interface DocLinksStart { elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; + installElasticAgent: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 4c7f8aab5b76..508299686b0d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -706,6 +706,7 @@ export interface DocLinksStart { elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; + installElasticAgent: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index ecbcf309c599..6d4d6a717253 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -78,7 +78,7 @@ export const ManualInstructions: React.FunctionComponent = ({ defaultMessage="See the {link} for RPM / DEB deploy instructions." values={{ link: ( - + Date: Tue, 19 Oct 2021 17:15:52 +0200 Subject: [PATCH 53/53] [Transform] Add alerting rules management to Transform UI (#115363) * transform alert flyout * fetch alerting rules * show alerting rules indicators * filter continuous transforms * add alert rules to the expanded row * edit alert rule from the list * fix ts issues * fix types * update texts * refactor using context, wip create alert from the list * update unit test * fix ts issue * privilege check --- .../common/api_schemas/transforms.ts | 4 +- .../transform/common/types/alerting.ts | 4 +- .../transform/common/types/transform.ts | 17 ++- .../alerting/transform_alerting_flyout.tsx | 127 ++++++++++++++++++ .../public/app/__mocks__/app_dependencies.tsx | 2 + .../transform/public/app/app_dependencies.tsx | 2 + .../public/app/common/transform_list.ts | 9 +- .../public/app/hooks/use_get_transforms.ts | 1 + .../components/authorization_provider.tsx | 12 +- .../lib/authorization/components/common.ts | 10 ++ .../public/app/mount_management_section.ts | 3 +- .../clone_transform_section.tsx | 4 +- .../step_create/step_create_form.tsx | 38 +++++- .../components/step_details/common.ts | 4 +- .../step_details/step_details_form.tsx | 4 +- .../components/wizard/wizard.tsx | 4 +- .../create_alert_rule_action_name.tsx | 38 ++++++ .../components/action_create_alert/index.ts | 8 ++ .../use_create_alert_rule_action.tsx | 47 +++++++ .../transform_list/expanded_row.test.tsx | 3 +- .../transform_list/expanded_row.tsx | 52 +++++-- .../expanded_row_details_pane.tsx | 5 +- .../transform_list/transform_list.tsx | 14 +- .../transform_list/use_actions.test.tsx | 1 + .../components/transform_list/use_actions.tsx | 3 + .../transform_list/use_columns.test.tsx | 15 ++- .../components/transform_list/use_columns.tsx | 34 +++++ .../transform_management_section.tsx | 20 ++- x-pack/plugins/transform/public/plugin.ts | 4 +- .../transform_health_service.ts | 97 ++++++++++--- .../transform/server/routes/api/transforms.ts | 12 ++ .../transform/server/services/license.ts | 9 +- .../services/endpoint.ts | 6 +- 33 files changed, 539 insertions(+), 74 deletions(-) create mode 100644 x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/index.ts create mode 100644 x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts index 7fb1a62a67bb..8867ecb5cc76 100644 --- a/x-pack/plugins/transform/common/api_schemas/transforms.ts +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -12,7 +12,7 @@ import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; import type { Dictionary } from '../types/common'; import type { PivotAggDict } from '../types/pivot_aggs'; import type { PivotGroupByDict } from '../types/pivot_group_by'; -import type { TransformId, TransformPivotConfig } from '../types/transform'; +import type { TransformId, TransformConfigUnion } from '../types/transform'; import { transformStateSchema, runtimeMappingsSchema } from './common'; @@ -33,7 +33,7 @@ export type GetTransformsRequestSchema = TypeOf; + +export type TransformHealthAlertRule = Omit, 'apiKey'>; diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts index f1e7efdadca9..a478946ff917 100644 --- a/x-pack/plugins/transform/common/types/transform.ts +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; +import type { EuiComboBoxOptionOption } from '@elastic/eui/src/components/combo_box/types'; import type { LatestFunctionConfig, PutTransformsRequestSchema } from '../api_schemas/transforms'; import { isPopulatedObject } from '../shared_imports'; -import { PivotGroupByDict } from './pivot_group_by'; -import { PivotAggDict } from './pivot_aggs'; +import type { PivotGroupByDict } from './pivot_group_by'; +import type { PivotAggDict } from './pivot_aggs'; +import type { TransformHealthAlertRule } from './alerting'; export type IndexName = string; export type IndexPattern = string; @@ -22,6 +23,7 @@ export type TransformBaseConfig = PutTransformsRequestSchema & { id: TransformId; create_time?: number; version?: string; + alerting_rules?: TransformHealthAlertRule[]; }; export interface PivotConfigDefinition { @@ -45,6 +47,11 @@ export type TransformLatestConfig = Omit & { export type TransformConfigUnion = TransformPivotConfig | TransformLatestConfig; +export type ContinuousTransform = Omit & + Required<{ + sync: TransformConfigUnion['sync']; + }>; + export function isPivotTransform(transform: unknown): transform is TransformPivotConfig { return isPopulatedObject(transform, ['pivot']); } @@ -53,6 +60,10 @@ export function isLatestTransform(transform: unknown): transform is TransformLat return isPopulatedObject(transform, ['latest']); } +export function isContinuousTransform(transform: unknown): transform is ContinuousTransform { + return isPopulatedObject(transform, ['sync']); +} + export interface LatestFunctionConfigUI { unique_key: Array> | undefined; sort: EuiComboBoxOptionOption | undefined; diff --git a/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx b/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx new file mode 100644 index 000000000000..63d00f280f3f --- /dev/null +++ b/x-pack/plugins/transform/public/alerting/transform_alerting_flyout.tsx @@ -0,0 +1,127 @@ +/* + * 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, { createContext, FC, useContext, useMemo } from 'react'; +import { memoize } from 'lodash'; +import { BehaviorSubject, Observable } from 'rxjs'; +import { pluck } from 'rxjs/operators'; +import useObservable from 'react-use/lib/useObservable'; +import { useAppDependencies } from '../app/app_dependencies'; +import { TransformHealthAlertRule, TransformHealthRuleParams } from '../../common/types/alerting'; +import { TRANSFORM_RULE_TYPE } from '../../common'; + +interface TransformAlertFlyoutProps { + initialAlert?: TransformHealthAlertRule | null; + ruleParams?: TransformHealthRuleParams | null; + onSave?: () => void; + onCloseFlyout: () => void; +} + +export const TransformAlertFlyout: FC = ({ + initialAlert, + ruleParams, + onCloseFlyout, + onSave, +}) => { + const { triggersActionsUi } = useAppDependencies(); + + const AlertFlyout = useMemo(() => { + if (!triggersActionsUi) return; + + const commonProps = { + onClose: () => { + onCloseFlyout(); + }, + onSave: async () => { + if (onSave) { + onSave(); + } + }, + }; + + if (initialAlert) { + return triggersActionsUi.getEditAlertFlyout({ + ...commonProps, + initialAlert, + }); + } + + return triggersActionsUi.getAddAlertFlyout({ + ...commonProps, + consumer: 'stackAlerts', + canChangeTrigger: false, + alertTypeId: TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH, + metadata: {}, + initialValues: { + params: ruleParams!, + }, + }); + // deps on id to avoid re-rendering on auto-refresh + }, [triggersActionsUi, initialAlert, ruleParams, onCloseFlyout, onSave]); + + return <>{AlertFlyout}; +}; + +interface AlertRulesManage { + editAlertRule$: Observable; + createAlertRule$: Observable; + setEditAlertRule: (alertRule: TransformHealthAlertRule) => void; + setCreateAlertRule: (transformId: string) => void; + hideAlertFlyout: () => void; +} + +export const getAlertRuleManageContext = memoize(function (): AlertRulesManage { + const ruleState$ = new BehaviorSubject<{ + editAlertRule: null | TransformHealthAlertRule; + createAlertRule: null | TransformHealthRuleParams; + }>({ + editAlertRule: null, + createAlertRule: null, + }); + return { + editAlertRule$: ruleState$.pipe(pluck('editAlertRule')), + createAlertRule$: ruleState$.pipe(pluck('createAlertRule')), + setEditAlertRule: (initialRule) => { + ruleState$.next({ + createAlertRule: null, + editAlertRule: initialRule, + }); + }, + setCreateAlertRule: (transformId: string) => { + ruleState$.next({ + createAlertRule: { includeTransforms: [transformId] }, + editAlertRule: null, + }); + }, + hideAlertFlyout: () => { + ruleState$.next({ + createAlertRule: null, + editAlertRule: null, + }); + }, + }; +}); + +export const AlertRulesManageContext = createContext(getAlertRuleManageContext()); + +export function useAlertRuleFlyout(): AlertRulesManage { + return useContext(AlertRulesManageContext); +} + +export const TransformAlertFlyoutWrapper = () => { + const { editAlertRule$, createAlertRule$, hideAlertFlyout } = useAlertRuleFlyout(); + const editAlertRule = useObservable(editAlertRule$); + const createAlertRule = useObservable(createAlertRule$); + + return editAlertRule || createAlertRule ? ( + + ) : null; +}; diff --git a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx index 8dc0e277c284..ab38d05ec9f8 100644 --- a/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/__mocks__/app_dependencies.tsx @@ -19,6 +19,7 @@ import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; import type { AppDependencies } from '../app_dependencies'; import { MlSharedContext } from './shared_context'; import type { GetMlSharedImportsReturnType } from '../../shared_imports'; +import type { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public'; const coreSetup = coreMock.createSetup(); const coreStart = coreMock.createStart(); @@ -43,6 +44,7 @@ const appDependencies: AppDependencies = { savedObjectsPlugin: savedObjectsPluginMock.createStartContract(), share: { urlGenerators: { getUrlGenerator: jest.fn() } } as unknown as SharePluginStart, ml: {} as GetMlSharedImportsReturnType, + triggersActionsUi: {} as jest.Mocked, }; export const useAppDependencies = () => { diff --git a/x-pack/plugins/transform/public/app/app_dependencies.tsx b/x-pack/plugins/transform/public/app/app_dependencies.tsx index d3f356f3e83b..da1178e39572 100644 --- a/x-pack/plugins/transform/public/app/app_dependencies.tsx +++ b/x-pack/plugins/transform/public/app/app_dependencies.tsx @@ -16,6 +16,7 @@ import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import type { Storage } from '../../../../../src/plugins/kibana_utils/public'; import type { GetMlSharedImportsReturnType } from '../shared_imports'; +import type { TriggersAndActionsUIPublicPluginStart } from '../../../triggers_actions_ui/public'; export interface AppDependencies { application: CoreStart['application']; @@ -34,6 +35,7 @@ export interface AppDependencies { share: SharePluginStart; ml: GetMlSharedImportsReturnType; spaces?: SpacesPluginStart; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export const useAppDependencies = () => { diff --git a/x-pack/plugins/transform/public/app/common/transform_list.ts b/x-pack/plugins/transform/public/app/common/transform_list.ts index d73018e284a8..c8ddd32f6a8f 100644 --- a/x-pack/plugins/transform/public/app/common/transform_list.ts +++ b/x-pack/plugins/transform/public/app/common/transform_list.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { EuiTableActionsColumnType } from '@elastic/eui'; - -import { TransformConfigUnion, TransformId } from '../../../common/types/transform'; -import { TransformStats } from '../../../common/types/transform_stats'; +import type { EuiTableActionsColumnType } from '@elastic/eui'; +import type { TransformConfigUnion, TransformId } from '../../../common/types/transform'; +import type { TransformStats } from '../../../common/types/transform_stats'; +import type { TransformHealthAlertRule } from '../../../common/types/alerting'; // Used to pass on attribute names to table columns export enum TRANSFORM_LIST_COLUMN { @@ -21,6 +21,7 @@ export interface TransformListRow { config: TransformConfigUnion; mode?: string; // added property on client side to allow filtering by this field stats: TransformStats; + alerting_rules?: TransformHealthAlertRule[]; } // The single Action type is not exported as is diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts index 2d3425dfeedc..7879e15118a3 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts @@ -87,6 +87,7 @@ export const useGetTransforms = ( mode: typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH, stats, + alerting_rules: config.alerting_rules, }); return reducedtableRows; }, [] as TransformListRow[]); diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 875c0f60969e..cc6313bf058c 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -20,12 +20,14 @@ interface Authorization { capabilities: Capabilities; } -const initialCapabalities: Capabilities = { +const initialCapabilities: Capabilities = { canGetTransform: false, canDeleteTransform: false, canPreviewTransform: false, canCreateTransform: false, canStartStopTransform: false, + canCreateTransformAlerts: false, + canUseTransformAlerts: false, }; const initialValue: Authorization = { @@ -35,7 +37,7 @@ const initialValue: Authorization = { hasAllPrivileges: false, missingPrivileges: {}, }, - capabilities: initialCapabalities, + capabilities: initialCapabilities, }; export const AuthorizationContext = createContext({ ...initialValue }); @@ -58,7 +60,7 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) = const value = { isLoading, privileges: isLoading ? { ...initialValue.privileges } : privilegesData, - capabilities: { ...initialCapabalities }, + capabilities: { ...initialCapabilities }, apiError: error ? (error as Error) : null, }; @@ -85,6 +87,10 @@ export const AuthorizationProvider = ({ privilegesEndpoint, children }: Props) = hasPrivilege(['cluster', 'cluster:admin/transform/start_task']) && hasPrivilege(['cluster', 'cluster:admin/transform/stop']); + value.capabilities.canCreateTransformAlerts = value.capabilities.canCreateTransform; + + value.capabilities.canUseTransformAlerts = value.capabilities.canGetTransform; + return ( {children} ); diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index d059f73a7613..d430a4d059e5 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -16,6 +16,8 @@ export interface Capabilities { canPreviewTransform: boolean; canCreateTransform: boolean; canStartStopTransform: boolean; + canCreateTransformAlerts: boolean; + canUseTransformAlerts: boolean; } export type Privilege = [string, string]; @@ -67,6 +69,14 @@ export function createCapabilityFailureMessage( defaultMessage: 'You do not have permission to create transforms.', }); break; + case 'canCreateTransformAlerts': + message = i18n.translate( + 'xpack.transform.capability.noPermission.canCreateTransformAlertsTooltip', + { + defaultMessage: 'You do not have permission to create transform alert rules.', + } + ); + break; case 'canStartStopTransform': message = i18n.translate( 'xpack.transform.capability.noPermission.startOrStopTransformTooltip', diff --git a/x-pack/plugins/transform/public/app/mount_management_section.ts b/x-pack/plugins/transform/public/app/mount_management_section.ts index 174733081854..6e6309406458 100644 --- a/x-pack/plugins/transform/public/app/mount_management_section.ts +++ b/x-pack/plugins/transform/public/app/mount_management_section.ts @@ -29,7 +29,7 @@ export async function mountManagementSection( const startServices = await getStartServices(); const [core, plugins] = startServices; const { application, chrome, docLinks, i18n, overlays, savedObjects, uiSettings } = core; - const { data, share, spaces } = plugins; + const { data, share, spaces, triggersActionsUi } = plugins; const { docTitle } = chrome; // Initialize services @@ -55,6 +55,7 @@ export async function mountManagementSection( share, spaces, ml: await getMlSharedImports(), + triggersActionsUi, }; const unmountAppCallback = renderApp(element, appDependencies); diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 8aecf403186c..218edb95c5f4 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; -import { TransformPivotConfig } from '../../../../common/types/transform'; +import { TransformConfigUnion } from '../../../../common/types/transform'; import { isHttpFetchError } from '../../common/request'; import { useApi } from '../../hooks/use_api'; @@ -50,7 +50,7 @@ export const CloneTransformSection: FC = ({ match, location }) => { const transformId = match.params.transformId; - const [transformConfig, setTransformConfig] = useState(); + const [transformConfig, setTransformConfig] = useState(); const [errorMessage, setErrorMessage] = useState(); const [isInitialized, setIsInitialized] = useState(false); const { error: searchItemsError, searchItems, setSavedObjectId } = useSearchItems(undefined); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 7ccf986d5d49..859ea77ea5a1 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -23,6 +23,7 @@ import { EuiText, } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; import { @@ -52,7 +53,8 @@ import { } from '../../../../../../common/api_schemas/transforms'; import type { RuntimeField } from '../../../../../../../../../src/plugins/data/common'; import { isPopulatedObject } from '../../../../../../common/shared_imports'; -import { isLatestTransform } from '../../../../../../common/types/transform'; +import { isContinuousTransform, isLatestTransform } from '../../../../../../common/types/transform'; +import { TransformAlertFlyout } from '../../../../../alerting/transform_alerting_flyout'; export interface StepDetailsExposedState { created: boolean; @@ -86,6 +88,7 @@ export const StepCreateForm: FC = React.memo( const [loading, setLoading] = useState(false); const [created, setCreated] = useState(defaults.created); const [started, setStarted] = useState(defaults.started); + const [alertFlyoutVisible, setAlertFlyoutVisible] = useState(false); const [indexPatternId, setIndexPatternId] = useState(defaults.indexPatternId); const [progressPercentComplete, setProgressPercentComplete] = useState( undefined @@ -398,6 +401,31 @@ export const StepCreateForm: FC = React.memo( )} + {isContinuousTransform(transformConfig) && created ? ( + + + + + + + + + {i18n.translate('xpack.transform.stepCreateForm.createAlertRuleDescription', { + defaultMessage: + 'Opens a wizard to create an alert rule for monitoring transform health.', + })} + + + + ) : null} = React.memo( {i18n.translate('xpack.transform.stepCreateForm.createTransformDescription', { defaultMessage: - 'Create the transform without starting it. You will be able to start the transform later by returning to the transforms list.', + 'Creates the transform without starting it. You will be able to start the transform later by returning to the transforms list.', })} @@ -535,6 +563,12 @@ export const StepCreateForm: FC = React.memo( )} + {alertFlyoutVisible ? ( + + ) : null}
); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts index fbe32e9bea12..39b1a2de26f8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/common.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform'; +import type { TransformConfigUnion, TransformId } from '../../../../../../common/types/transform'; export type EsIndexName = string; export type IndexPatternTitle = string; @@ -55,7 +55,7 @@ export function getDefaultStepDetailsState(): StepDetailsExposedState { export function applyTransformConfigToDetailsState( state: StepDetailsExposedState, - transformConfig?: TransformPivotConfig + transformConfig?: TransformConfigUnion ): StepDetailsExposedState { // apply the transform configuration to wizard DETAILS state if (transformConfig !== undefined) { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 0d39ec77d059..7a47cc539c4a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -29,7 +29,7 @@ import { isEsIndices, isPostTransformsPreviewResponseSchema, } from '../../../../../../common/api_schemas/type_guards'; -import { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform'; +import { TransformId } from '../../../../../../common/types/transform'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; import { getErrorMessage } from '../../../../../../common/utils/errors'; @@ -158,7 +158,7 @@ export const StepDetailsForm: FC = React.memo( ), }); } else { - setTransformIds(resp.transforms.map((transform: TransformPivotConfig) => transform.id)); + setTransformIds(resp.transforms.map((transform) => transform.id)); } const indices = await api.getEsIndices(); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 63e21e5d8aa1..27c43ed01a93 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -11,7 +11,7 @@ import { i18n } from '@kbn/i18n'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; -import { TransformPivotConfig } from '../../../../../../common/types/transform'; +import type { TransformConfigUnion } from '../../../../../../common/types/transform'; import { getCreateTransformRequestBody } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; @@ -81,7 +81,7 @@ const StepDefine: FC = ({ }; interface WizardProps { - cloneConfig?: TransformPivotConfig; + cloneConfig?: TransformConfigUnion; searchItems: SearchItems; } diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.tsx new file mode 100644 index 000000000000..c8d67a86d579 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/create_alert_rule_action_name.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, { FC } from 'react'; +import { EuiToolTip } from '@elastic/eui'; + +import { i18n } from '@kbn/i18n'; +import { createCapabilityFailureMessage } from '../../../../lib/authorization'; + +interface CreateAlertRuleActionProps { + disabled: boolean; +} + +export const crateAlertRuleActionNameText = i18n.translate( + 'xpack.transform.transformList.createAlertRuleNameText', + { + defaultMessage: 'Create alert rule', + } +); + +export const CreateAlertRuleActionName: FC = ({ disabled }) => { + if (disabled) { + return ( + + <>{crateAlertRuleActionNameText} + + ); + } + + return <>{crateAlertRuleActionNameText}; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/index.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/index.ts new file mode 100644 index 000000000000..80999d774bdc --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/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 { useCreateAlertRuleAction } from './use_create_alert_rule_action'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.tsx new file mode 100644 index 000000000000..070f1eb08ac6 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_create_alert/use_create_alert_rule_action.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, { useCallback, useContext, useMemo } from 'react'; +import { AuthorizationContext } from '../../../../lib/authorization'; +import { TransformListAction, TransformListRow } from '../../../../common'; +import { + crateAlertRuleActionNameText, + CreateAlertRuleActionName, +} from './create_alert_rule_action_name'; +import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout'; +import { isContinuousTransform } from '../../../../../../common/types/transform'; + +export type CreateAlertRuleAction = ReturnType; +export const useCreateAlertRuleAction = (forceDisable: boolean) => { + const { canCreateTransformAlerts } = useContext(AuthorizationContext).capabilities; + const { setCreateAlertRule } = useAlertRuleFlyout(); + + const clickHandler = useCallback( + (item: TransformListRow) => { + setCreateAlertRule(item.id); + }, + [setCreateAlertRule] + ); + + const action: TransformListAction = useMemo( + () => ({ + name: (item: TransformListRow) => ( + + ), + available: (item: TransformListRow) => isContinuousTransform(item.config), + enabled: () => canCreateTransformAlerts && !forceDisable, + description: crateAlertRuleActionNameText, + type: 'icon', + icon: 'bell', + onClick: clickHandler, + 'data-test-subj': 'transformActionCreateAlertRule', + }), + [canCreateTransformAlerts, forceDisable, clickHandler] + ); + + return { action }; +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx index bccd3aff72c5..af85049ce691 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.test.tsx @@ -22,6 +22,7 @@ import { getMlSharedImports } from '../../../../../shared_imports'; // FLAKY https://github.com/elastic/kibana/issues/112922 describe.skip('Transform: Transform List ', () => { + const onAlertEdit = jest.fn(); // Set timezone to US/Eastern for consistent test results. beforeEach(() => { moment.tz.setDefault('US/Eastern'); @@ -38,7 +39,7 @@ describe.skip('Transform: Transform List ', () => { render( - + ); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx index dff2ba17cb3f..84110e67d701 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row.tsx @@ -7,17 +7,18 @@ import React, { FC } from 'react'; -import { EuiTabbedContent } from '@elastic/eui'; +import { EuiButtonEmpty, EuiTabbedContent } from '@elastic/eui'; import { Optional } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import moment from 'moment-timezone'; import { TransformListRow } from '../../../../common'; import { useAppDependencies } from '../../../../app_dependencies'; -import { ExpandedRowDetailsPane, SectionConfig } from './expanded_row_details_pane'; +import { ExpandedRowDetailsPane, SectionConfig, SectionItem } from './expanded_row_details_pane'; import { ExpandedRowJsonPane } from './expanded_row_json_pane'; import { ExpandedRowMessagesPane } from './expanded_row_messages_pane'; import { ExpandedRowPreviewPane } from './expanded_row_preview_pane'; +import { TransformHealthAlertRule } from '../../../../../../common/types/alerting'; function getItemDescription(value: any) { if (typeof value === 'object') { @@ -44,18 +45,16 @@ export function stringHash(str: string): number { return hash < 0 ? hash * -2 : hash; } -interface Item { - title: string; - description: any; -} +type Item = SectionItem; interface Props { item: TransformListRow; + onAlertEdit: (alertRule: TransformHealthAlertRule) => void; } type StateValues = Optional; -export const ExpandedRow: FC = ({ item }) => { +export const ExpandedRow: FC = ({ item, onAlertEdit }) => { const { ml: { formatHumanReadableDateTimeSeconds }, } = useAppDependencies(); @@ -166,12 +165,40 @@ export const ExpandedRow: FC = ({ item }) => { } } + const alertRuleItems: Item[] | undefined = item.alerting_rules?.map((rule) => { + return { + title: ( + { + onAlertEdit(rule); + }} + flush="left" + size={'xs'} + iconSize={'s'} + > + {rule.name} + + ), + description: rule.executionStatus.status, + }; + }); + const checkpointing: SectionConfig = { title: 'Checkpointing', items: checkpointingItems, position: 'right', }; + const alertingRules: SectionConfig = { + title: i18n.translate('xpack.transform.transformList.transformDetails.alertRulesTitle', { + defaultMessage: 'Alert rules', + }), + items: alertRuleItems!, + position: 'right', + }; + const stats: SectionConfig = { title: 'Stats', items: Object.entries(item.stats.stats).map((s) => { @@ -192,7 +219,16 @@ export const ExpandedRow: FC = ({ item }) => { defaultMessage: 'Details', } ), - content: , + content: ( + + ), }, { id: `transform-stats-tab-${tabId}`, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx index 03e2fb2115d6..1b2dde0a2e57 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_details_pane.tsx @@ -17,9 +17,10 @@ import { } from '@elastic/eui'; export interface SectionItem { - title: string; - description: string; + title: string | JSX.Element; + description: string | number | JSX.Element; } + export interface SectionConfig { title: string; position: 'left' | 'right'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index ab30f4793a31..8b7aaf1cf8fd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -50,15 +50,18 @@ import { useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; import { transformFilters, filterTransforms } from './transform_search_bar_filters'; import { useTableSettings } from './use_table_settings'; +import { useAlertRuleFlyout } from '../../../../../alerting/transform_alerting_flyout'; +import { TransformHealthAlertRule } from '../../../../../../common/types/alerting'; function getItemIdToExpandedRowMap( itemIds: TransformId[], - transforms: TransformListRow[] + transforms: TransformListRow[], + onAlertEdit: (alertRule: TransformHealthAlertRule) => void ): ItemIdToExpandedRowMap { return itemIds.reduce((m: ItemIdToExpandedRowMap, transformId: TransformId) => { const item = transforms.find((transform) => transform.config.id === transformId); if (item !== undefined) { - m[transformId] = ; + m[transformId] = ; } return m; }, {} as ItemIdToExpandedRowMap); @@ -79,6 +82,7 @@ export const TransformList: FC = ({ }) => { const [isLoading, setIsLoading] = useState(false); const { refresh } = useRefreshTransformList({ isLoading: setIsLoading }); + const { setEditAlertRule } = useAlertRuleFlyout(); const [filterActive, setFilterActive] = useState(false); @@ -171,7 +175,11 @@ export const TransformList: FC = ({ ); } - const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, transforms); + const itemIdToExpandedRowMap = getItemIdToExpandedRowMap( + expandedRowItemIds, + transforms, + setEditAlertRule + ); const bulkActionMenuItems = [
diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx index b7d5a2b7104a..20d2f784a4d8 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.test.tsx @@ -27,6 +27,7 @@ describe('Transform: Transform List Actions', () => { // in the runtime result here anyway. expect(actions.map((a: any) => a['data-test-subj'])).toStrictEqual([ 'transformActionDiscover', + 'transformActionCreateAlertRule', 'transformActionStart', 'transformActionStop', 'transformActionEdit', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx index 81e51cdafc32..40b40cfa8c7b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_actions.tsx @@ -18,6 +18,7 @@ import { EditTransformFlyout } from '../edit_transform_flyout'; import { useEditAction } from '../action_edit'; import { useStartAction, StartActionModal } from '../action_start'; import { useStopAction } from '../action_stop'; +import { useCreateAlertRuleAction } from '../action_create_alert'; export const useActions = ({ forceDisable, @@ -35,6 +36,7 @@ export const useActions = ({ const editAction = useEditAction(forceDisable, transformNodes); const startAction = useStartAction(forceDisable, transformNodes); const stopAction = useStopAction(forceDisable); + const createAlertRuleAction = useCreateAlertRuleAction(forceDisable); return { modals: ( @@ -52,6 +54,7 @@ export const useActions = ({ ), actions: [ discoverAction.action, + createAlertRuleAction.action, startAction.action, stopAction.action, editAction.action, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx index af2325ede202..a26ccf0348c9 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.test.tsx @@ -20,14 +20,15 @@ describe('Transform: Job List Columns', () => { const columns: ReturnType['columns'] = result.current.columns; - expect(columns).toHaveLength(8); + expect(columns).toHaveLength(9); expect(columns[0].isExpander).toBeTruthy(); expect(columns[1].name).toBe('ID'); - expect(columns[2].name).toBe('Description'); - expect(columns[3].name).toBe('Type'); - expect(columns[4].name).toBe('Status'); - expect(columns[5].name).toBe('Mode'); - expect(columns[6].name).toBe('Progress'); - expect(columns[7].name).toBe('Actions'); + expect(columns[2].id).toBe('alertRule'); + expect(columns[3].name).toBe('Description'); + expect(columns[4].name).toBe('Type'); + expect(columns[5].name).toBe('Status'); + expect(columns[6].name).toBe('Mode'); + expect(columns[7].name).toBe('Progress'); + expect(columns[8].name).toBe('Actions'); }); }); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index dbdd3409c7e3..bad42c212293 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -21,6 +21,7 @@ import { EuiText, EuiToolTip, RIGHT_ALIGNMENT, + EuiIcon, } from '@elastic/eui'; import { @@ -95,6 +96,7 @@ export const useColumns = ( const columns: [ EuiTableComputedColumnType, EuiTableFieldDataColumnType, + EuiTableComputedColumnType, EuiTableFieldDataColumnType, EuiTableComputedColumnType, EuiTableComputedColumnType, @@ -143,6 +145,38 @@ export const useColumns = ( truncateText: true, scope: 'row', }, + { + id: 'alertRule', + name: ( + +

+ +

+
+ ), + width: '30px', + render: (item) => { + return Array.isArray(item.alerting_rules) ? ( + + } + > + + + ) : ( + + ); + }, + }, { field: TRANSFORM_LIST_COLUMN.DESCRIPTION, 'data-test-subj': 'transformListColumnDescription', diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx index 2479d34f1579..055e1e50701f 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/transform_management_section.tsx @@ -35,6 +35,11 @@ import { useRefreshInterval } from './components/transform_list/use_refresh_inte import { SearchSelection } from './components/search_selection'; import { TransformList } from './components/transform_list'; import { TransformStatsBar } from './components/transform_list/transforms_stats_bar'; +import { + AlertRulesManageContext, + getAlertRuleManageContext, + TransformAlertFlyoutWrapper, +} from '../../../alerting/transform_alerting_flyout'; export const TransformManagement: FC = () => { const { esTransform } = useDocumentationLinks(); @@ -149,12 +154,15 @@ export const TransformManagement: FC = () => { )} {typeof errorMessage === 'undefined' && ( - + + + + )} )} diff --git a/x-pack/plugins/transform/public/plugin.ts b/x-pack/plugins/transform/public/plugin.ts index da280452c1f0..a7d0dce25664 100644 --- a/x-pack/plugins/transform/public/plugin.ts +++ b/x-pack/plugins/transform/public/plugin.ts @@ -16,7 +16,7 @@ import type { SharePluginStart } from 'src/plugins/share/public'; import type { SpacesApi } from '../../spaces/public'; import { registerFeature } from './register_feature'; import type { PluginSetupContract as AlertingSetup } from '../../alerting/public'; -import type { TriggersAndActionsUIPublicPluginSetup } from '../../triggers_actions_ui/public'; +import type { TriggersAndActionsUIPublicPluginStart } from '../../triggers_actions_ui/public'; import { getTransformHealthRuleType } from './alerting'; export interface PluginsDependencies { @@ -27,7 +27,7 @@ export interface PluginsDependencies { share: SharePluginStart; spaces?: SpacesApi; alerting?: AlertingSetup; - triggersActionsUi?: TriggersAndActionsUIPublicPluginSetup; + triggersActionsUi: TriggersAndActionsUIPublicPluginStart; } export class TransformUiPlugin { diff --git a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts index 88b5396c7b11..eb51c04e0bca 100644 --- a/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts +++ b/x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/transform_health_service.ts @@ -8,16 +8,21 @@ import { ElasticsearchClient } from 'kibana/server'; import { i18n } from '@kbn/i18n'; import type { Transform as EsTransform } from '@elastic/elasticsearch/api/types'; +import { keyBy } from 'lodash'; import { TransformHealthRuleParams } from './schema'; import { ALL_TRANSFORMS_SELECTION, TRANSFORM_HEALTH_CHECK_NAMES, + TRANSFORM_RULE_TYPE, } from '../../../../common/constants'; import { getResultTestConfig } from '../../../../common/utils/alerts'; import { NotStartedTransformResponse, TransformHealthAlertContext, } from './register_transform_health_rule_type'; +import type { RulesClient } from '../../../../../alerting/server'; +import type { TransformHealthAlertRule } from '../../../../common/types/alerting'; +import { isContinuousTransform } from '../../../../common/types/transform'; interface TestResult { name: string; @@ -27,37 +32,48 @@ interface TestResult { // @ts-ignore FIXME update types in the elasticsearch client type Transform = EsTransform & { id: string; description?: string; sync: object }; -export function transformHealthServiceProvider(esClient: ElasticsearchClient) { +type TransformWithAlertingRules = Transform & { alerting_rules: TransformHealthAlertRule[] }; + +export function transformHealthServiceProvider( + esClient: ElasticsearchClient, + rulesClient?: RulesClient +) { const transformsDict = new Map(); /** * Resolves result transform selection. * @param includeTransforms * @param excludeTransforms + * @param skipIDsCheck */ const getResultsTransformIds = async ( includeTransforms: string[], - excludeTransforms: string[] | null + excludeTransforms: string[] | null, + skipIDsCheck = false ): Promise => { const includeAll = includeTransforms.some((id) => id === ALL_TRANSFORMS_SELECTION); - // Fetch transforms to make sure assigned transforms exists. - const transformsResponse = ( - await esClient.transform.getTransform({ - ...(includeAll ? {} : { transform_id: includeTransforms.join(',') }), - allow_no_match: true, - size: 1000, - }) - ).body.transforms as Transform[]; - let resultTransformIds: string[] = []; - transformsResponse.forEach((t) => { - transformsDict.set(t.id, t); - if (t.sync) { - resultTransformIds.push(t.id); - } - }); + if (skipIDsCheck) { + resultTransformIds = includeTransforms; + } else { + // Fetch transforms to make sure assigned transforms exists. + const transformsResponse = ( + await esClient.transform.getTransform({ + ...(includeAll ? {} : { transform_id: includeTransforms.join(',') }), + allow_no_match: true, + size: 1000, + }) + ).body.transforms as Transform[]; + + transformsResponse.forEach((t) => { + transformsDict.set(t.id, t); + if (t.sync) { + resultTransformIds.push(t.id); + } + }); + } if (excludeTransforms && excludeTransforms.length > 0) { const excludeIdsSet = new Set(excludeTransforms); @@ -129,6 +145,53 @@ export function transformHealthServiceProvider(esClient: ElasticsearchClient) { return result; }, + + /** + * Updates transform list with associated alerting rules. + */ + async populateTransformsWithAssignedRules( + transforms: Transform[] + ): Promise { + const newList = transforms.filter(isContinuousTransform) as TransformWithAlertingRules[]; + + if (!rulesClient) { + throw new Error('Rules client is missing'); + } + + const transformMap = keyBy(newList, 'id'); + + const transformAlertingRules = await rulesClient.find({ + options: { + perPage: 1000, + filter: `alert.attributes.alertTypeId:${TRANSFORM_RULE_TYPE.TRANSFORM_HEALTH}`, + }, + }); + + for (const ruleInstance of transformAlertingRules.data) { + // Retrieve result transform IDs + const resultTransformIds: string[] = await getResultsTransformIds( + ruleInstance.params.includeTransforms.includes(ALL_TRANSFORMS_SELECTION) + ? Object.keys(transformMap) + : ruleInstance.params.includeTransforms, + ruleInstance.params.excludeTransforms, + true + ); + + resultTransformIds.forEach((transformId) => { + const transformRef = transformMap[transformId] as TransformWithAlertingRules; + + if (transformRef) { + if (Array.isArray(transformRef.alerting_rules)) { + transformRef.alerting_rules.push(ruleInstance); + } else { + transformRef.alerting_rules = [ruleInstance]; + } + } + }); + } + + return newList; + }, }; } diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index 76aac9686c37..4a657ae615d9 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -63,6 +63,7 @@ import { registerTransformNodesRoutes } from './transforms_nodes'; import { IIndexPattern } from '../../../../../../src/plugins/data/common'; import { isLatestTransform } from '../../../common/types/transform'; import { isKeywordDuplicate } from '../../../common/utils/field_utils'; +import { transformHealthServiceProvider } from '../../lib/alerting/transform_health_rule_type/transform_health_service'; enum TRANSFORM_ACTIONS { STOP = 'stop', @@ -90,6 +91,17 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { size: 1000, ...req.params, }); + + if (ctx.alerting) { + const transformHealthService = transformHealthServiceProvider( + ctx.core.elasticsearch.client.asCurrentUser, + ctx.alerting.getRulesClient() + ); + + // @ts-ignore + await transformHealthService.populateTransformsWithAssignedRules(body.transforms); + } + return res.ok({ body }); } catch (e) { return res.customError(wrapError(wrapEsError(e))); diff --git a/x-pack/plugins/transform/server/services/license.ts b/x-pack/plugins/transform/server/services/license.ts index 978912ce08ba..ce28e0365bb2 100644 --- a/x-pack/plugins/transform/server/services/license.ts +++ b/x-pack/plugins/transform/server/services/license.ts @@ -15,6 +15,7 @@ import { } from 'kibana/server'; import { LicensingPluginSetup, LicenseType } from '../../../licensing/server'; +import type { AlertingApiRequestHandlerContext } from '../../../alerting/server'; export interface LicenseStatus { isValid: boolean; @@ -28,6 +29,10 @@ interface SetupSettings { defaultErrorMessage: string; } +type TransformRequestHandlerContext = RequestHandlerContext & { + alerting?: AlertingApiRequestHandlerContext; +}; + export class License { private licenseStatus: LicenseStatus = { isValid: false, @@ -64,7 +69,9 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute( + handler: RequestHandler + ) { const license = this; return function licenseCheck( diff --git a/x-pack/test/security_solution_endpoint/services/endpoint.ts b/x-pack/test/security_solution_endpoint/services/endpoint.ts index 2e774dcd8478..5bcc5c415a0d 100644 --- a/x-pack/test/security_solution_endpoint/services/endpoint.ts +++ b/x-pack/test/security_solution_endpoint/services/endpoint.ts @@ -18,7 +18,7 @@ import { IndexedHostsAndAlertsResponse, indexHostsAndAlerts, } from '../../../plugins/security_solution/common/endpoint/index_data'; -import { TransformPivotConfig } from '../../../plugins/transform/common/types/transform'; +import { TransformConfigUnion } from '../../../plugins/transform/common/types/transform'; import { GetTransformsResponseSchema } from '../../../plugins/transform/common/api_schemas/transforms'; import { catchAndWrapError } from '../../../plugins/security_solution/server/endpoint/utils'; import { installOrUpgradeEndpointFleetPackage } from '../../../plugins/security_solution/common/endpoint/data_loaders/setup_fleet_for_endpoint'; @@ -38,9 +38,9 @@ export class EndpointTestResources extends FtrService { * * @param [endpointPackageVersion] if set, it will be used to get the specific transform this this package version. Else just returns first one found */ - async getTransform(endpointPackageVersion?: string): Promise { + async getTransform(endpointPackageVersion?: string): Promise { const transformId = this.generateTransformId(endpointPackageVersion); - let transform: TransformPivotConfig | undefined; + let transform: TransformConfigUnion | undefined; if (endpointPackageVersion) { await this.transform.api.waitForTransformToExist(transformId);