diff --git a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml index b3ef0780f73ea5..5ea9c77c5d5a26 100644 --- a/.buildkite/pipelines/on_merge_unsupported_ftrs.yml +++ b/.buildkite/pipelines/on_merge_unsupported_ftrs.yml @@ -68,7 +68,7 @@ steps: queue: n2-4-virt depends_on: build timeout_in_minutes: 60 - parallelism: 10 + parallelism: 16 retry: automatic: - exit_status: '*' @@ -80,7 +80,7 @@ steps: queue: n2-4-virt depends_on: build timeout_in_minutes: 60 - parallelism: 6 + parallelism: 10 retry: automatic: - exit_status: '*' diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d4774774e20ba6..48211d6f11b2b8 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1388,6 +1388,9 @@ x-pack/test/security_solution_cypress/cypress/tasks/expandable_flyout @elastic/ /x-pack/plugins/stack_connectors/server/connector_types/sentinelone @elastic/security-defend-workflows /x-pack/plugins/stack_connectors/common/sentinelone @elastic/security-defend-workflows +## Security Solution shared OAS schemas +/x-pack/plugins/security_solution/common/api/model @elastic/security-detection-rule-management @elastic/security-detection-engine + ## Security Solution sub teams - Detection Rule Management /x-pack/plugins/security_solution/common/api/detection_engine/fleet_integrations @elastic/security-detection-rule-management /x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema @elastic/security-detection-rule-management @elastic/security-detection-engine diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.test.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.test.ts index 78bb907c5cc483..906ae93d4ce9d5 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.test.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.test.ts @@ -7,7 +7,6 @@ */ import { errors } from '@elastic/elasticsearch'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { elasticsearchClientMock } from '@kbn/core-elasticsearch-client-server-mocks'; import { retryCallCluster, migrationRetryCallCluster } from './retry_call_cluster'; @@ -69,11 +68,9 @@ describe('retryCallCluster', () => { describe('migrationRetryCallCluster', () => { let client: ReturnType; - let logger: ReturnType; beforeEach(() => { client = elasticsearchClientMock.createElasticsearchClient(); - logger = loggingSystemMock.createLogger(); }); const mockClientPingWithErrorBeforeSuccess = (error: any) => { @@ -88,21 +85,21 @@ describe('migrationRetryCallCluster', () => { new errors.NoLivingConnectionsError('no living connections', {} as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); it('retries ES API calls that rejects with `ConnectionError`', async () => { mockClientPingWithErrorBeforeSuccess(new errors.ConnectionError('connection error', {} as any)); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); it('retries ES API calls that rejects with `TimeoutError`', async () => { mockClientPingWithErrorBeforeSuccess(new errors.TimeoutError('timeout error', {} as any)); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -113,7 +110,7 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -124,7 +121,7 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -135,7 +132,7 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -146,7 +143,7 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -157,7 +154,7 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); @@ -173,65 +170,10 @@ describe('migrationRetryCallCluster', () => { } as any) ); - const result = await migrationRetryCallCluster(() => client.ping(), logger, 1); + const result = await migrationRetryCallCluster(() => client.ping(), 1); expect(result).toEqual(dummyBody); }); - it('logs only once for each unique error message', async () => { - client.ping - .mockImplementationOnce(() => - createErrorReturn( - new errors.ResponseError({ - statusCode: 503, - } as any) - ) - ) - .mockImplementationOnce(() => - createErrorReturn(new errors.ConnectionError('connection error', {} as any)) - ) - .mockImplementationOnce(() => - createErrorReturn( - new errors.ResponseError({ - statusCode: 503, - } as any) - ) - ) - .mockImplementationOnce(() => - createErrorReturn(new errors.ConnectionError('connection error', {} as any)) - ) - .mockImplementationOnce(() => - createErrorReturn( - new errors.ResponseError({ - statusCode: 500, - body: { - error: { - type: 'snapshot_in_progress_exception', - }, - }, - } as any) - ) - ) - .mockImplementationOnce(() => - elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) - ); - - await migrationRetryCallCluster(() => client.ping(), logger, 1); - - expect(loggingSystemMock.collect(logger).warn).toMatchInlineSnapshot(` - Array [ - Array [ - "Unable to connect to Elasticsearch. Error: Response Error", - ], - Array [ - "Unable to connect to Elasticsearch. Error: connection error", - ], - Array [ - "Unable to connect to Elasticsearch. Error: snapshot_in_progress_exception", - ], - ] - `); - }); - it('rejects when ES API calls reject with other errors', async () => { client.ping .mockImplementationOnce(() => @@ -250,9 +192,9 @@ describe('migrationRetryCallCluster', () => { elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) ); - await expect( - migrationRetryCallCluster(() => client.ping(), logger, 1) - ).rejects.toMatchInlineSnapshot(`[ResponseError: I'm a teapot]`); + await expect(migrationRetryCallCluster(() => client.ping(), 1)).rejects.toMatchInlineSnapshot( + `[ResponseError: I'm a teapot]` + ); }); it('stops retrying when ES API calls reject with other errors', async () => { @@ -268,8 +210,8 @@ describe('migrationRetryCallCluster', () => { elasticsearchClientMock.createSuccessTransportRequestPromise({ ...dummyBody }) ); - await expect( - migrationRetryCallCluster(() => client.ping(), logger, 1) - ).rejects.toMatchInlineSnapshot(`[Error: unknown error]`); + await expect(migrationRetryCallCluster(() => client.ping(), 1)).rejects.toMatchInlineSnapshot( + `[Error: unknown error]` + ); }); }); diff --git a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.ts b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.ts index e20639cf4f4053..af4809990db5f6 100644 --- a/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.ts +++ b/packages/core/elasticsearch/core-elasticsearch-server-internal/src/retry_call_cluster.ts @@ -8,7 +8,6 @@ import { defer, throwError, iif, timer } from 'rxjs'; import { concatMap, retryWhen } from 'rxjs/operators'; -import type { Logger } from '@kbn/logging'; const retryResponseStatuses = [ 503, // ServiceUnavailable @@ -60,7 +59,6 @@ export const retryCallCluster = >(apiCaller: () => T) */ export const migrationRetryCallCluster = >( apiCaller: () => T, - log: Logger, delay: number = 2500 ): T => { const previousErrors: string[] = []; @@ -70,7 +68,6 @@ export const migrationRetryCallCluster = >( errors.pipe( concatMap((error) => { if (!previousErrors.includes(error.message)) { - log.warn(`Unable to connect to Elasticsearch. Error: ${error.message}`); previousErrors.push(error.message); } return iif( diff --git a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.ts b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.ts index b742b9f1124ede..f5f34c03f9cde0 100644 --- a/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.ts +++ b/packages/core/saved-objects/core-saved-objects-migration-server-internal/src/kibana_migrator_utils.ts @@ -67,7 +67,6 @@ export async function getCurrentIndexTypesMap({ client.indices.getMapping({ index: mainIndex, }), - logger, retryDelay ); diff --git a/packages/deeplinks/observability/deep_links.ts b/packages/deeplinks/observability/deep_links.ts index 2a682a352cb46e..0dee18d5b0ae5a 100644 --- a/packages/deeplinks/observability/deep_links.ts +++ b/packages/deeplinks/observability/deep_links.ts @@ -43,7 +43,12 @@ export type ObservabilityOverviewLinkId = | 'rules' | 'slos'; -export type MetricsLinkId = 'inventory' | 'metrics-explorer' | 'hosts' | 'settings'; +export type MetricsLinkId = + | 'inventory' + | 'metrics-explorer' + | 'hosts' + | 'settings' + | 'assetDetails'; export type ApmLinkId = | 'services' diff --git a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx index cf1f4b2101f5d8..68d3f354a14f83 100644 --- a/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx +++ b/packages/kbn-search-connectors/components/scheduling/connector_scheduling.tsx @@ -55,7 +55,7 @@ export const SchedulePanel: React.FC = ({ title, description interface ConnectorContentSchedulingProps { children?: React.ReactNode; connector: Connector; - configurationPathOnClick: () => void; + configurationPathOnClick?: () => void; dataTelemetryIdPrefix: string; hasPlatinumLicense: boolean; hasChanges: boolean; diff --git a/packages/kbn-unified-data-table/src/components/data_table.tsx b/packages/kbn-unified-data-table/src/components/data_table.tsx index ea0cad4a37bea4..429d290597d122 100644 --- a/packages/kbn-unified-data-table/src/components/data_table.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table.tsx @@ -40,7 +40,7 @@ import { import type { ToastsStart, IUiSettingsClient } from '@kbn/core/public'; import type { Serializable } from '@kbn/utility-types'; import type { DataTableRecord } from '@kbn/discover-utils/types'; -import { getShouldShowFieldHandler, DOC_HIDE_TIME_COLUMN_SETTING } from '@kbn/discover-utils'; +import { getShouldShowFieldHandler } from '@kbn/discover-utils'; import type { DataViewFieldEditorStart } from '@kbn/data-view-field-editor-plugin/public'; import type { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import type { ThemeServiceStart } from '@kbn/react-kibana-context-common'; @@ -63,7 +63,7 @@ import { getEuiGridColumns, getLeadControlColumns, getVisibleColumns, - hasSourceTimeFieldValue, + canPrependTimeFieldColumn, } from './data_table_columns'; import { UnifiedDataTableContext } from '../table_context'; import { getSchemaDetectors } from './data_table_schema'; @@ -630,15 +630,23 @@ export const UnifiedDataTable = ({ [dataView, onFieldEdited, services.dataViewFieldEditor] ); - const shouldShowTimeField = useMemo( - () => - hasSourceTimeFieldValue(displayedColumns, dataView, columnTypes, showTimeCol, isPlainRecord), - [dataView, displayedColumns, isPlainRecord, showTimeCol, columnTypes] + const timeFieldName = dataView.timeFieldName; + const shouldPrependTimeFieldColumn = useCallback( + (activeColumns: string[]) => + canPrependTimeFieldColumn( + activeColumns, + timeFieldName, + columnTypes, + showTimeCol, + isPlainRecord + ), + [timeFieldName, isPlainRecord, showTimeCol, columnTypes] ); const visibleColumns = useMemo( - () => getVisibleColumns(displayedColumns, dataView, shouldShowTimeField), - [dataView, displayedColumns, shouldShowTimeField] + () => + getVisibleColumns(displayedColumns, dataView, shouldPrependTimeFieldColumn(displayedColumns)), + [dataView, displayedColumns, shouldPrependTimeFieldColumn] ); const getCellValue = useCallback( @@ -741,19 +749,16 @@ export const UnifiedDataTable = ({ ] ); - const hideTimeColumn = useMemo( - () => services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), - [services.uiSettings] - ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( () => ({ visibleColumns, setVisibleColumns: (newColumns: string[]) => { - onSetColumns(newColumns, hideTimeColumn); + const dontModifyColumns = !shouldPrependTimeFieldColumn(newColumns); + onSetColumns(newColumns, dontModifyColumns); }, }), - [visibleColumns, hideTimeColumn, onSetColumns] + [visibleColumns, onSetColumns, shouldPrependTimeFieldColumn] ); /** diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx index 8bca26f164d111..b98f514690aee5 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.test.tsx @@ -13,7 +13,7 @@ import { deserializeHeaderRowHeight, getEuiGridColumns, getVisibleColumns, - hasSourceTimeFieldValue, + canPrependTimeFieldColumn, } from './data_table_columns'; import { dataViewWithTimefieldMock } from '../../__mocks__/data_view_with_timefield'; import { dataViewWithoutTimefieldMock } from '../../__mocks__/data_view_without_timefield'; @@ -122,11 +122,11 @@ describe('Data table columns', function () { }); }); - describe('hasSourceTimeFieldValue', () => { + describe('canPrependTimeFieldColumn', () => { function buildColumnTypes(dataView: DataView) { const columnTypes: Record = {}; for (const field of dataView.fields) { - columnTypes[field.name] = ''; + columnTypes[field.name] = field.type; } return columnTypes; } @@ -135,9 +135,9 @@ describe('Data table columns', function () { it('should forward showTimeCol if no _source columns is passed', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['extension', 'message'], - dataViewWithTimefieldMock, + dataViewWithTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithTimefieldMock), showTimeCol, false @@ -146,26 +146,26 @@ describe('Data table columns', function () { } }); - it('should forward showTimeCol if no _source columns is passed, text-based datasource', () => { + it('should return false if no _source columns is passed, text-based datasource', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['extension', 'message'], - dataViewWithTimefieldMock, + dataViewWithTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithTimefieldMock), showTimeCol, true ) - ).toBe(showTimeCol); + ).toBe(false); } }); it('should forward showTimeCol if _source column is passed', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['_source'], - dataViewWithTimefieldMock, + dataViewWithTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithTimefieldMock), showTimeCol, false @@ -174,76 +174,94 @@ describe('Data table columns', function () { } }); - it('should return true if _source column is passed, text-based datasource', () => { - // ... | DROP @timestamp test case + it('should forward showTimeCol if _source column is passed, text-based datasource', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['_source'], - dataViewWithTimefieldMock, + dataViewWithTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithTimefieldMock), showTimeCol, true ) - ).toBe(true); + ).toBe(showTimeCol); + } + }); + + it('should return false if _source column is passed but time field is not returned, text-based datasource', () => { + // ... | DROP @timestamp test case + const columnTypes = buildColumnTypes(dataViewWithTimefieldMock); + if (dataViewWithTimefieldMock.timeFieldName) { + delete columnTypes[dataViewWithTimefieldMock.timeFieldName]; + } + for (const showTimeCol of [true, false]) { + expect( + canPrependTimeFieldColumn( + ['_source'], + dataViewWithTimefieldMock.timeFieldName, + columnTypes, + showTimeCol, + true + ) + ).toBe(false); } }); }); describe('dataView without timeField', () => { - it('should forward showTimeCol if no _source columns is passed', () => { + it('should return false if no _source columns is passed', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['extension', 'message'], - dataViewWithoutTimefieldMock, + dataViewWithoutTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithoutTimefieldMock), showTimeCol, false ) - ).toBe(showTimeCol); + ).toBe(false); } }); - it('should forward showTimeCol if no _source columns is passed, text-based datasource', () => { + it('should return false if no _source columns is passed, text-based datasource', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['extension', 'message'], - dataViewWithoutTimefieldMock, + dataViewWithoutTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithoutTimefieldMock), showTimeCol, true ) - ).toBe(showTimeCol); + ).toBe(false); } }); - it('should forward showTimeCol if _source column is passed', () => { + it('should return false if _source column is passed', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['_source'], - dataViewWithoutTimefieldMock, + dataViewWithoutTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithoutTimefieldMock), showTimeCol, false ) - ).toBe(showTimeCol); + ).toBe(false); } }); it('should return false if _source column is passed, text-based datasource', () => { for (const showTimeCol of [true, false]) { expect( - hasSourceTimeFieldValue( + canPrependTimeFieldColumn( ['_source'], - dataViewWithoutTimefieldMock, + dataViewWithoutTimefieldMock.timeFieldName, buildColumnTypes(dataViewWithoutTimefieldMock), showTimeCol, true ) - ).toBe(showTimeCol); + ).toBe(false); } }); }); diff --git a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx index d947cbf3738460..985cf10a4e3091 100644 --- a/packages/kbn-unified-data-table/src/components/data_table_columns.tsx +++ b/packages/kbn-unified-data-table/src/components/data_table_columns.tsx @@ -78,6 +78,7 @@ export function getLeadControlColumns(canSetExpandedDoc: boolean) { } function buildEuiGridColumn({ + numberOfColumns, columnName, columnWidth = 0, dataView, @@ -97,6 +98,7 @@ function buildEuiGridColumn({ headerRowHeight, customGridColumnsConfiguration, }: { + numberOfColumns: number; columnName: string; columnWidth: number | undefined; dataView: DataView; @@ -199,7 +201,9 @@ function buildEuiGridColumn({ headerRowHeight={headerRowHeight} /> ); - column.initialWidth = defaultTimeColumnWidth; + if (numberOfColumns > 1) { + column.initialWidth = defaultTimeColumnWidth; + } } if (columnWidth > 0) { @@ -266,9 +270,11 @@ export function getEuiGridColumns({ }) { const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; const headerRowHeight = deserializeHeaderRowHeight(headerRowHeightLines); + const numberOfColumns = columns.length; return columns.map((column, columnIndex) => buildEuiGridColumn({ + numberOfColumns, columnName: column, columnCellActions: columnsCellActions?.[columnIndex], columnWidth: getColWidth(column), @@ -291,24 +297,36 @@ export function getEuiGridColumns({ ); } -export function hasSourceTimeFieldValue( +export function canPrependTimeFieldColumn( columns: string[], - dataView: DataView, + timeFieldName: string | undefined, columnTypes: DataTableColumnTypes | undefined, showTimeCol: boolean, isPlainRecord: boolean ) { - const timeFieldName = dataView.timeFieldName; - if (!isPlainRecord || !columns.includes('_source') || !timeFieldName || !columnTypes) { - return showTimeCol; + if (!showTimeCol || !timeFieldName) { + return false; + } + + if (isPlainRecord) { + return !!columnTypes && timeFieldName in columnTypes && columns.includes('_source'); } - return timeFieldName in columnTypes; + + return true; } -export function getVisibleColumns(columns: string[], dataView: DataView, showTimeCol: boolean) { +export function getVisibleColumns( + columns: string[], + dataView: DataView, + shouldPrependTimeFieldColumn: boolean +) { const timeFieldName = dataView.timeFieldName; - if (showTimeCol && timeFieldName && !columns.find((col) => col === timeFieldName)) { + if ( + shouldPrependTimeFieldColumn && + timeFieldName && + !columns.find((col) => col === timeFieldName) + ) { return [timeFieldName, ...columns]; } diff --git a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx index 1d77cd50afd6c9..22f174cd0dea7f 100644 --- a/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx +++ b/src/plugins/discover/public/application/main/components/layout/discover_documents.tsx @@ -225,13 +225,10 @@ function DiscoverDocumentsComponent({ [stateContainer] ); + // should be aligned with embeddable `showTimeCol` prop const showTimeCol = useMemo( - () => - // for ES|QL we want to show the time column only when is on Document view - (!isTextBasedQuery || !columns?.length) && - !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && - !!dataView.timeFieldName, - [isTextBasedQuery, columns, uiSettings, dataView.timeFieldName] + () => !uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false), + [uiSettings] ); const columnTypes: DataTableColumnTypes | undefined = useMemo( diff --git a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx index 1c7e5467e9516f..f3100bad98a059 100644 --- a/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx +++ b/src/plugins/discover/public/embeddable/saved_search_embeddable.tsx @@ -330,7 +330,6 @@ export class SavedSearchEmbeddable searchProps.totalHitCount = result.records.length; searchProps.isLoading = false; searchProps.isPlainRecord = true; - searchProps.showTimeCol = false; searchProps.isSortEnabled = true; return; diff --git a/test/functional/apps/discover/group3/_time_field_column.ts b/test/functional/apps/discover/group3/_time_field_column.ts index 0719a47ef05212..8ed5188151bad8 100644 --- a/test/functional/apps/discover/group3/_time_field_column.ts +++ b/test/functional/apps/discover/group3/_time_field_column.ts @@ -65,9 +65,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }) { // check in Discover expect(await dataGrid.getHeaderFields()).to.eql( - (hideTimeFieldColumnSetting && !isTextBased) || !hasTimeField - ? ['Document'] - : ['@timestamp', 'Document'] + hideTimeFieldColumnSetting || !hasTimeField ? ['Document'] : ['@timestamp', 'Document'] ); await PageObjects.discover.saveSearch(`${SEARCH_NO_COLUMNS}${savedSearchSuffix}`); await PageObjects.discover.waitUntilSearchingHasFinished(); @@ -80,7 +78,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataGrid.getHeaderFields()).to.eql( !hasTimeField ? ['@timestamp'] - : hideTimeFieldColumnSetting && !isTextBased + : hideTimeFieldColumnSetting ? ['Document'] // legacy behaviour : ['@timestamp', 'Document'] // legacy behaviour ); @@ -95,9 +93,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.unifiedFieldList.clickFieldListItemRemove('@timestamp'); await retry.try(async () => { expect(await dataGrid.getHeaderFields()).to.eql( - (hideTimeFieldColumnSetting && !isTextBased) || !hasTimeField - ? ['Document'] - : ['@timestamp', 'Document'] + hideTimeFieldColumnSetting || !hasTimeField ? ['Document'] : ['@timestamp', 'Document'] ); }); } @@ -112,9 +108,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await retry.try(async () => { expect(await dataGrid.getHeaderFields()).to.eql( - (hideTimeFieldColumnSetting && !isTextBased) || !hasTimeField - ? ['Document'] - : ['@timestamp', 'Document'] + hideTimeFieldColumnSetting || !hasTimeField ? ['Document'] : ['@timestamp', 'Document'] ); }); @@ -130,7 +124,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await dataGrid.getHeaderFields()).to.eql( !hasTimeField ? ['@timestamp'] - : hideTimeFieldColumnSetting && !isTextBased + : hideTimeFieldColumnSetting ? ['Document'] // legacy behaviour : ['@timestamp', 'Document'] // legacy behaviour ); @@ -182,12 +176,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.clickMoveColumnLeft('@timestamp'); await retry.try(async () => { - expect(await dataGrid.getHeaderFields()).to.eql( - // FIXME as a part of https://github.com/elastic/kibana/issues/174074 - isTextBased && !hideTimeFieldColumnSetting - ? ['bytes', 'extension'] - : ['@timestamp', 'bytes', 'extension'] - ); + expect(await dataGrid.getHeaderFields()).to.eql(['@timestamp', 'bytes', 'extension']); }); await PageObjects.unifiedFieldList.clickFieldListItemRemove('@timestamp'); diff --git a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts index 259314c0a8c9ed..857102fc7dd34b 100644 --- a/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts @@ -172,8 +172,7 @@ export const createAgentPolicyHandler: FleetRequestHandler< const user = (await appContextService.getSecurity()?.authc.getCurrentUser(request)) || undefined; const withSysMonitoring = request.query.sys_monitoring ?? false; const monitoringEnabled = request.body.monitoring_enabled; - const force = request.body.force; - const { has_fleet_server: hasFleetServer, ...newPolicy } = request.body; + const { has_fleet_server: hasFleetServer, force, ...newPolicy } = request.body; const spaceId = fleetContext.spaceId; const authorizationHeader = HTTPAuthorizationHeader.parseFromRequest(request, user?.username); diff --git a/x-pack/plugins/fleet/server/services/agent_policy.test.ts b/x-pack/plugins/fleet/server/services/agent_policy.test.ts index b297bb6b128c27..210dcbeb29bb35 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.test.ts @@ -865,7 +865,7 @@ describe('agent policy', () => { expect(esClient.bulk).toBeCalledWith( expect.objectContaining({ index: AGENT_POLICY_INDEX, - body: [ + operations: [ expect.objectContaining({ index: { _id: expect.anything(), diff --git a/x-pack/plugins/fleet/server/services/agent_policy.ts b/x-pack/plugins/fleet/server/services/agent_policy.ts index 5907d07b4da38f..973f9636e1abe4 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy.ts @@ -112,9 +112,10 @@ class AgentPolicyService { soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, action: 'created' | 'updated' | 'deleted', - agentPolicyId: string + agentPolicyId: string, + options?: { skipDeploy?: boolean } ) => { - return agentPolicyUpdateEventHandler(soClient, esClient, action, agentPolicyId); + return agentPolicyUpdateEventHandler(soClient, esClient, action, agentPolicyId, options); }; private async _update( @@ -286,6 +287,7 @@ class AgentPolicyService { id?: string; user?: AuthenticatedUser; authorizationHeader?: HTTPAuthorizationHeader | null; + skipDeploy?: boolean; } = {} ): Promise { // Ensure an ID is provided, so we can include it in the audit logs below @@ -330,7 +332,9 @@ class AgentPolicyService { ); await appContextService.getUninstallTokenService()?.generateTokenForPolicyId(newSo.id); - await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id); + await this.triggerAgentPolicyUpdatedEvent(soClient, esClient, 'created', newSo.id, { + skipDeploy: options.skipDeploy, + }); logger.debug(`Created new agent policy with id ${newSo.id}`); return { id: newSo.id, ...newSo.attributes }; } @@ -1034,7 +1038,7 @@ class AgentPolicyService { const bulkResponse = await esClient.bulk({ index: AGENT_POLICY_INDEX, - body: fleetServerPoliciesBulkBody, + operations: fleetServerPoliciesBulkBody, refresh: 'wait_for', }); diff --git a/x-pack/plugins/fleet/server/services/agent_policy_create.test.ts b/x-pack/plugins/fleet/server/services/agent_policy_create.test.ts index 45d42ba373a7d0..f541212a51ab1d 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_create.test.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_create.test.ts @@ -190,6 +190,35 @@ describe('createAgentPolicyWithPackages', () => { ); }); + it('should call deploy policy once when create policy with system package', async () => { + mockedAgentPolicyService.deployPolicy.mockClear(); + mockedAgentPolicyService.create.mockImplementation((soClient, esClient, newPolicy, options) => { + if (!options?.skipDeploy) { + mockedAgentPolicyService.deployPolicy(soClientMock, 'new_id'); + } + return Promise.resolve({ + ...newPolicy, + id: options?.id || 'new_id', + } as AgentPolicy); + }); + const response = await createAgentPolicyWithPackages({ + esClient: esClientMock, + soClient: soClientMock, + newPolicy: { name: 'Agent policy 1', namespace: 'default' }, + withSysMonitoring: true, + spaceId: 'default', + }); + + expect(response.id).toEqual('new_id'); + expect(mockedBulkInstallPackages).toHaveBeenCalledWith({ + savedObjectsClient: soClientMock, + esClient: esClientMock, + packagesToInstall: ['system'], + spaceId: 'default', + }); + expect(mockedAgentPolicyService.deployPolicy).toHaveBeenCalledTimes(1); + }); + it('should create policy with system and elastic_agent package', async () => { const response = await createAgentPolicyWithPackages({ esClient: esClientMock, diff --git a/x-pack/plugins/fleet/server/services/agent_policy_create.ts b/x-pack/plugins/fleet/server/services/agent_policy_create.ts index a55541c621f83d..c830ab43d41100 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_create.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_create.ts @@ -54,6 +54,7 @@ async function createPackagePolicy( spaceId: string; user: AuthenticatedUser | undefined; authorizationHeader?: HTTPAuthorizationHeader | null; + force?: boolean; } ) { const newPackagePolicy = await packagePolicyService @@ -78,6 +79,7 @@ async function createPackagePolicy( user: options.user, bumpRevision: false, authorizationHeader: options.authorizationHeader, + force: options.force, }); } @@ -140,6 +142,7 @@ export async function createAgentPolicyWithPackages({ user, id: agentPolicyId, authorizationHeader, + skipDeploy: true, // skip deploying the policy until package policies are added }); // Create the fleet server package policy and add it to agent policy. @@ -148,6 +151,7 @@ export async function createAgentPolicyWithPackages({ spaceId, user, authorizationHeader, + force, }); } @@ -157,6 +161,7 @@ export async function createAgentPolicyWithPackages({ spaceId, user, authorizationHeader, + force, }); } diff --git a/x-pack/plugins/fleet/server/services/agent_policy_update.ts b/x-pack/plugins/fleet/server/services/agent_policy_update.ts index 7ff4383afd337b..639cf21cb7833a 100644 --- a/x-pack/plugins/fleet/server/services/agent_policy_update.ts +++ b/x-pack/plugins/fleet/server/services/agent_policy_update.ts @@ -32,7 +32,8 @@ export async function agentPolicyUpdateEventHandler( soClient: SavedObjectsClientContract, esClient: ElasticsearchClient, action: string, - agentPolicyId: string + agentPolicyId: string, + options?: { skipDeploy?: boolean } ) { // `soClient` from ingest `appContextService` is used to create policy change actions // to ensure encrypted SOs are handled correctly @@ -44,7 +45,9 @@ export async function agentPolicyUpdateEventHandler( agentPolicyId, forceRecreate: true, }); - await agentPolicyService.deployPolicy(internalSoClient, agentPolicyId); + if (!options?.skipDeploy) { + await agentPolicyService.deployPolicy(internalSoClient, agentPolicyId); + } } if (action === 'updated') { diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts index 61edf78ec497eb..b729e34fa8bb68 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.test.ts @@ -6,18 +6,18 @@ */ import { createAppContextStartContractMock } from '../../../../mocks'; import { appContextService } from '../../..'; -import { loadFieldsFromYaml } from '../../fields/field'; +import { loadDatastreamsFieldsFromYaml } from '../../fields/field'; import type { PackageInstallContext, RegistryDataStream } from '../../../../../common/types'; import { prepareTemplate, prepareToInstallTemplates } from './install'; jest.mock('../../fields/field', () => ({ ...jest.requireActual('../../fields/field'), - loadFieldsFromYaml: jest.fn(), + loadDatastreamsFieldsFromYaml: jest.fn(), })); -const mockedLoadFieldsFromYaml = loadFieldsFromYaml as jest.MockedFunction< - typeof loadFieldsFromYaml +const mockedLoadFieldsFromYaml = loadDatastreamsFieldsFromYaml as jest.MockedFunction< + typeof loadDatastreamsFieldsFromYaml >; const packageInstallContext = { packageInfo: { name: 'package', version: '0.0.1' }, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts index e1ff612076b3bf..2dce7b73235678 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/template/install.ts @@ -30,7 +30,7 @@ import type { EsAssetReference, ExperimentalDataStreamFeature, } from '../../../../types'; -import { loadFieldsFromYaml, processFields } from '../../fields/field'; +import { loadDatastreamsFieldsFromYaml, processFields } from '../../fields/field'; import { getAssetFromAssetsMap, getPathParts } from '../../archive'; import { FLEET_COMPONENT_TEMPLATES, @@ -509,7 +509,7 @@ export function prepareTemplate({ experimentalDataStreamFeature?: ExperimentalDataStreamFeature; }): { componentTemplates: TemplateMap; indexTemplate: IndexTemplateEntry } { const { name: packageName, version: packageVersion } = packageInstallContext.packageInfo; - const fields = loadFieldsFromYaml(packageInstallContext, dataStream.path); + const fields = loadDatastreamsFieldsFromYaml(packageInstallContext, dataStream.path); const isIndexModeTimeSeries = dataStream.elasticsearch?.index_mode === 'time_series' || diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts index 84ea5fae048749..308a10f1bf94d3 100644 --- a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/install.ts @@ -25,8 +25,7 @@ import { buildComponentTemplates, installComponentAndIndexTemplateForDataStream, } from '../template/install'; -import { isFields, processFields } from '../../fields/field'; -import { generateMappings } from '../template/template'; +import { isFields } from '../../fields/field'; import { getESAssetMetadata } from '../meta'; import { updateEsAssetReferences } from '../../packages/es_assets_reference'; import { getAssetFromAssetsMap, getPathParts } from '../../archive'; @@ -47,6 +46,7 @@ import { isUserSettingsTemplate } from '../template/utils'; import { deleteTransforms } from './remove'; import { getDestinationIndexAliases } from './transform_utils'; +import { loadMappingForTransform } from './mappings'; const DEFAULT_TRANSFORM_TEMPLATES_PRIORITY = 250; enum TRANSFORM_SPECS_TYPES { @@ -183,8 +183,6 @@ const processTransformAssetsPerModule = ( // Handling fields.yml and all other files within 'fields' folder if (fileName === TRANSFORM_SPECS_TYPES.FIELDS || isFields(path)) { - const validFields = processFields(content); - const mappings = generateMappings(validFields); const templateName = getTransformAssetNameForInstallation( installablePackage, transformModuleId, @@ -208,14 +206,6 @@ const processTransformAssetsPerModule = ( } else { destinationIndexTemplates[indexToModify] = template; } - - // If there's already mappings set previously, append it to new - const previousMappings = - transformsSpecifications.get(transformModuleId)?.get('mappings') ?? {}; - - transformsSpecifications.get(transformModuleId)?.set('mappings', { - properties: { ...previousMappings.properties, ...mappings.properties }, - }); } if (fileName === TRANSFORM_SPECS_TYPES.TRANSFORM) { @@ -394,6 +384,20 @@ const processTransformAssetsPerModule = ( version: t.transformVersion, })); + // Load and generate mappings + for (const destinationIndexTemplate of destinationIndexTemplates) { + if (!destinationIndexTemplate.transformModuleId) { + continue; + } + + transformsSpecifications + .get(destinationIndexTemplate.transformModuleId) + ?.set( + 'mappings', + loadMappingForTransform(packageInstallContext, destinationIndexTemplate.transformModuleId) + ); + } + return { indicesToAddRefs, indexTemplatesRefs, diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.test.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.test.ts new file mode 100644 index 00000000000000..f34015bf776976 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.test.ts @@ -0,0 +1,91 @@ +/* + * 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 { loadMappingForTransform } from './mappings'; + +describe('loadMappingForTransform', () => { + it('should return a mappings without properties if there is no fields resource', () => { + const fields = loadMappingForTransform( + { + packageInfo: {} as any, + assetsMap: new Map(), + paths: [], + }, + 'test' + ); + + expect(fields).toEqual({ properties: {} }); + }); + + it('should merge shallow mapping without properties if there is no fields resource', () => { + const fields = loadMappingForTransform( + { + packageInfo: {} as any, + assetsMap: new Map([ + [ + '/package/ti_opencti/2.1.0/elasticsearch/transform/latest_ioc/fields/ecs.yml', + Buffer.from( + ` +- description: Description of the threat feed in a UI friendly format. + name: threat.feed.description + type: keyword +- description: The name of the threat feed in UI friendly format. + name: threat.feed.name + type: keyword` + ), + ], + [ + '/package/ti_opencti/2.1.0/elasticsearch/transform/latest_ioc/fields/ecs-extra.yml', + Buffer.from( + ` +- description: The display name indicator in an UI friendly format + level: extended + name: threat.indicator.name + type: keyword` + ), + ], + ]), + paths: [ + '/package/ti_opencti/2.1.0/elasticsearch/transform/latest_ioc/fields/ecs.yml', + '/package/ti_opencti/2.1.0/elasticsearch/transform/latest_ioc/fields/ecs-extra.yml', + ], + }, + 'latest_ioc' + ); + + expect(fields).toMatchInlineSnapshot(` + Object { + "properties": Object { + "threat": Object { + "properties": Object { + "feed": Object { + "properties": Object { + "description": Object { + "ignore_above": 1024, + "type": "keyword", + }, + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + "indicator": Object { + "properties": Object { + "name": Object { + "ignore_above": 1024, + "type": "keyword", + }, + }, + }, + }, + }, + }, + } + `); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.ts b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.ts new file mode 100644 index 00000000000000..130dae0ecca514 --- /dev/null +++ b/x-pack/plugins/fleet/server/services/epm/elasticsearch/transform/mappings.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { type PackageInstallContext } from '../../../../../common/types/models'; +import { loadTransformFieldsFromYaml, processFields } from '../../fields/field'; +import { generateMappings } from '../template/template'; + +export function loadMappingForTransform( + packageInstallContext: PackageInstallContext, + transformModuleId: string +) { + const fields = loadTransformFieldsFromYaml(packageInstallContext, transformModuleId); + const validFields = processFields(fields); + return generateMappings(validFields); +} diff --git a/x-pack/plugins/fleet/server/services/epm/fields/field.ts b/x-pack/plugins/fleet/server/services/epm/fields/field.ts index 859752d5dead17..b8ca555c95a9b7 100644 --- a/x-pack/plugins/fleet/server/services/epm/fields/field.ts +++ b/x-pack/plugins/fleet/server/services/epm/fields/field.ts @@ -289,13 +289,25 @@ export const isFields = (path: string) => { return path.includes('/fields/'); }; +export const filterForTransformAssets = (transformName: string) => { + return function isTransformAssets(path: string) { + return path.includes(`/transform/${transformName}`); + }; +}; + +function combineFilter(...filters: Array<(path: string) => boolean>) { + return function filterAsset(path: string) { + return filters.every((filter) => filter(path)); + }; +} + /** * loadFieldsFromYaml * * Gets all field files, optionally filtered by dataset, extracts .yml files, merges them together */ -export const loadFieldsFromYaml = ( +export const loadDatastreamsFieldsFromYaml = ( packageInstallContext: PackageInstallContext, datasetName?: string ): Field[] => { @@ -318,3 +330,26 @@ export const loadFieldsFromYaml = ( return acc; }, []); }; + +export const loadTransformFieldsFromYaml = ( + packageInstallContext: PackageInstallContext, + transformName: string +): Field[] => { + // Fetch all field definition files + const fieldDefinitionFiles = getAssetsDataFromAssetsMap( + packageInstallContext.packageInfo, + packageInstallContext.assetsMap, + combineFilter(isFields, filterForTransformAssets(transformName)) + ); + return fieldDefinitionFiles.reduce((acc, file) => { + // Make sure it is defined as it is optional. Should never happen. + if (file.buffer) { + const tmpFields = safeLoad(file.buffer.toString()); + // safeLoad() returns undefined for empty files, we don't want that + if (tmpFields) { + acc = acc.concat(tmpFields); + } + } + return acc; + }, []); +}; diff --git a/x-pack/plugins/fleet/server/services/output.test.ts b/x-pack/plugins/fleet/server/services/output.test.ts index cddd143b9b9e49..13409f7e6f1d4e 100644 --- a/x-pack/plugins/fleet/server/services/output.test.ts +++ b/x-pack/plugins/fleet/server/services/output.test.ts @@ -65,7 +65,7 @@ const CONFIG_WITHOUT_ES_HOSTS = { }, }; -function mockOutputSO(id: string, attributes: any = {}) { +function mockOutputSO(id: string, attributes: any = {}, updatedAt?: string) { return { id: outputIdToUuid(id), type: 'ingest-outputs', @@ -74,6 +74,7 @@ function mockOutputSO(id: string, attributes: any = {}) { output_id: id, ...attributes, }, + updated_at: updatedAt, }; } @@ -146,7 +147,9 @@ function getMockedSoClient( } default: - throw new Error('not found: ' + id); + return mockOutputSO(id, { + type: 'remote_elasticsearch', + }); } }); soClient.update.mockImplementation(async (type, id, data) => { @@ -1868,6 +1871,11 @@ describe('Output Service', () => { }); describe('getLatestOutputHealth', () => { + let soClient: any; + beforeEach(() => { + soClient = getMockedSoClient(); + }); + it('should return unknown state if no hits', async () => { esClientMock.search.mockResolvedValue({ hits: { @@ -1907,6 +1915,51 @@ describe('Output Service', () => { timestamp: '2023-11-30T14:25:31Z', }); }); + + it('should apply range filter if updated_at available', async () => { + const updatedAt = '2023-11-30T14:25:31Z'; + soClient.get.mockResolvedValue( + mockOutputSO( + 'id', + { + type: 'remote_elasticsearch', + }, + updatedAt + ) + ); + + await outputService.getLatestOutputHealth(esClientMock, 'id'); + + expect((esClientMock.search.mock.lastCall?.[0] as any)?.query.bool.must).toEqual([ + { + range: { + '@timestamp': { + gte: updatedAt, + }, + }, + }, + ]); + }); + + it('should not apply range filter if updated_at is not available', async () => { + soClient.get.mockResolvedValue( + mockOutputSO('id', { + type: 'remote_elasticsearch', + }) + ); + + await outputService.getLatestOutputHealth(esClientMock, 'id'); + + expect((esClientMock.search.mock.lastCall?.[0] as any)?.query.bool.must).toEqual([]); + }); + + it('should not apply range filter if output query returns error', async () => { + soClient.get.mockResolvedValue({ error: { message: 'error' } }); + + await outputService.getLatestOutputHealth(esClientMock, 'id'); + + expect((esClientMock.search.mock.lastCall?.[0] as any)?.query.bool.must).toEqual([]); + }); }); describe('backfillAllOutputPresets', () => { diff --git a/x-pack/plugins/fleet/server/services/output.ts b/x-pack/plugins/fleet/server/services/output.ts index 348d16c138bacd..9a339fb33ec78d 100644 --- a/x-pack/plugins/fleet/server/services/output.ts +++ b/x-pack/plugins/fleet/server/services/output.ts @@ -1085,10 +1085,23 @@ class OutputService { } async getLatestOutputHealth(esClient: ElasticsearchClient, id: string): Promise { + const lastUpdateTime = await this.getOutputLastUpdateTime(id); + + const mustFilter = []; + if (lastUpdateTime) { + mustFilter.push({ + range: { + '@timestamp': { + gte: lastUpdateTime, + }, + }, + }); + } + const response = await esClient.search( { index: OUTPUT_HEALTH_DATA_STREAM, - query: { bool: { filter: { term: { output: id } } } }, + query: { bool: { filter: { term: { output: id } }, must: mustFilter } }, sort: { '@timestamp': 'desc' }, size: 1, }, @@ -1109,6 +1122,24 @@ class OutputService { timestamp: latestHit['@timestamp'], }; } + + async getOutputLastUpdateTime(id: string): Promise { + const outputSO = await this.encryptedSoClient.get( + SAVED_OBJECT_TYPE, + outputIdToUuid(id) + ); + + if (outputSO.error) { + appContextService + .getLogger() + .debug( + `Error getting output ${id} SO, using updated_at:undefined, cause: ${outputSO.error.message}` + ); + return undefined; + } + + return outputSO.updated_at; + } } interface OutputHealth { diff --git a/x-pack/plugins/infra/kibana.jsonc b/x-pack/plugins/infra/kibana.jsonc index 5faf928f7d9599..dbabc92fd69c74 100644 --- a/x-pack/plugins/infra/kibana.jsonc +++ b/x-pack/plugins/infra/kibana.jsonc @@ -36,7 +36,17 @@ "visTypeTimeseries", "apmDataAccess" ], - "optionalPlugins": ["spaces", "ml", "home", "embeddable", "osquery", "cloud", "profilingDataAccess", "licenseManagement"], + "optionalPlugins": [ + "spaces", + "ml", + "home", + "embeddable", + "osquery", + "cloud", + "profilingDataAccess", + "licenseManagement", + "serverless" + ], "requiredBundles": [ "unifiedSearch", "observability", @@ -46,7 +56,7 @@ "kibanaReact", "ml", "embeddable", - "controls", + "controls" ] } } diff --git a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx index 260114bb8b11f2..07d91a5e0e8623 100644 --- a/x-pack/plugins/infra/public/components/asset_details/template/page.tsx +++ b/x-pack/plugins/infra/public/components/asset_details/template/page.tsx @@ -5,10 +5,12 @@ * 2.0. */ -import { EuiFlexGroup, EuiPageTemplate } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; +import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; +import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; import { useKibanaContextForPlugin } from '../../../hooks/use_kibana'; import { useKibanaHeader } from '../../../hooks/use_kibana_header'; import { InfraLoadingPanel } from '../../loading'; @@ -24,16 +26,36 @@ import { getIntegrationsAvailable } from '../utils'; export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { const { loading } = useAssetDetailsRenderPropsContext(); const { metadata, loading: metadataLoading } = useMetadataStateContext(); - const { rightSideItems, tabEntries, breadcrumbs } = usePageHeader(tabs, links); + const { rightSideItems, tabEntries, breadcrumbs: headerBreadcrumbs } = usePageHeader(tabs, links); const { asset } = useAssetDetailsRenderPropsContext(); const { actionMenuHeight } = useKibanaHeader(); const trackOnlyOnce = React.useRef(false); const { activeTabId } = useTabSwitcherContext(); const { - services: { telemetry }, + services: { + telemetry, + observabilityShared: { + navigation: { PageTemplate }, + }, + }, } = useKibanaContextForPlugin(); + const parentBreadcrumbResolver = useParentBreadcrumbResolver(); + const breadcrumbOptions = parentBreadcrumbResolver.getBreadcrumbOptions(asset.type); + useMetricsBreadcrumbs( + [ + { + ...breadcrumbOptions.link, + text: breadcrumbOptions.text, + }, + { + text: asset.name, + }, + ], + { deeperContextServerless: true } + ); + useEffect(() => { if (trackOnlyOnce.current) { return; @@ -63,44 +85,35 @@ export const Page = ({ tabs = [], links = [] }: ContentTemplateProps) => { [actionMenuHeight] ); - return loading ? ( - - - - ) : ( - - - - - - - - + {loading ? ( + + + + ) : ( + + )} + ); }; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana.tsx b/x-pack/plugins/infra/public/hooks/use_kibana.tsx index 25ef6595c734ab..e62d6fbbc4e3c9 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana.tsx +++ b/x-pack/plugins/infra/public/hooks/use_kibana.tsx @@ -6,7 +6,7 @@ */ import type { PropsOf } from '@elastic/eui'; -import React, { useMemo, createElement, createContext } from 'react'; +import React, { useMemo, createElement, createContext, useContext } from 'react'; import { CoreStart } from '@kbn/core/public'; import { createKibanaReactContext, @@ -73,6 +73,10 @@ export const useKibanaEnvironmentContextProvider = (kibanaEnvironment?: KibanaEn return Provider; }; +export function useKibanaEnvironmentContext() { + return useContext(KibanaEnvironmentContext); +} + export const createLazyComponentWithKibanaContext = >( coreSetup: InfraClientCoreSetup, lazyComponentFactory: () => Promise<{ default: T }> diff --git a/x-pack/plugins/infra/public/hooks/use_metrics_breadcrumbs.tsx b/x-pack/plugins/infra/public/hooks/use_metrics_breadcrumbs.tsx index 4679d4fe67f7ed..defc8b3210f481 100644 --- a/x-pack/plugins/infra/public/hooks/use_metrics_breadcrumbs.tsx +++ b/x-pack/plugins/infra/public/hooks/use_metrics_breadcrumbs.tsx @@ -5,11 +5,42 @@ * 2.0. */ +import { useEffect, useMemo } from 'react'; import { ChromeBreadcrumb } from '@kbn/core/public'; -import { useBreadcrumbs } from './use_breadcrumbs'; +import { useBreadcrumbs, useLinkProps } from '@kbn/observability-shared-plugin/public'; import { METRICS_APP } from '../../common/constants'; import { metricsTitle } from '../translations'; +import { useKibanaContextForPlugin } from './use_kibana'; -export const useMetricsBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => { - useBreadcrumbs(METRICS_APP, metricsTitle, extraCrumbs); +export const useMetricsBreadcrumbs = ( + extraCrumbs: ChromeBreadcrumb[], + options?: { deeperContextServerless: boolean } +) => { + const { + services: { serverless }, + } = useKibanaContextForPlugin(); + const appLinkProps = useLinkProps({ app: METRICS_APP }); + + const breadcrumbs = useMemo( + () => [ + { + ...appLinkProps, + text: metricsTitle, + }, + ...extraCrumbs, + ], + [appLinkProps, extraCrumbs] + ); + + useBreadcrumbs(breadcrumbs); + + useEffect(() => { + // For deeper context breadcrumbs in serveless, the `serverless` plugin provides its own breadcrumb service. + // https://docs.elastic.dev/kibana-dev-docs/serverless-project-navigation#breadcrumbs + if (serverless && options?.deeperContextServerless) { + // The initial path is already set in the breadcrumbs + const [, ...serverlessBreadcrumbs] = breadcrumbs; + serverless.setBreadcrumbs(serverlessBreadcrumbs); + } + }, [breadcrumbs, options?.deeperContextServerless, serverless]); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_parent_breadcrumb_resolver.ts b/x-pack/plugins/infra/public/hooks/use_parent_breadcrumb_resolver.ts similarity index 88% rename from x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_parent_breadcrumb_resolver.ts rename to x-pack/plugins/infra/public/hooks/use_parent_breadcrumb_resolver.ts index a0d5160e874721..532d3dbe00adbe 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/hooks/use_parent_breadcrumb_resolver.ts +++ b/x-pack/plugins/infra/public/hooks/use_parent_breadcrumb_resolver.ts @@ -8,13 +8,18 @@ import { InventoryItemType } from '@kbn/metrics-data-access-plugin/common/inventory_models/types'; import { useLinkProps } from '@kbn/observability-shared-plugin/public'; import { useLocation } from 'react-router-dom'; -import { hostsTitle, inventoryTitle } from '../../../../translations'; -import { BreadcrumbOptions } from '../types'; +import type { LinkProps } from '@kbn/observability-shared-plugin/public/hooks/use_link_props'; +import { hostsTitle, inventoryTitle } from '../translations'; interface LocationStateProps { originPathname: string; } +interface BreadcrumbOptions { + text: string; + link: LinkProps; +} + export function useParentBreadcrumbResolver() { const hostsLinkProps = useLinkProps({ app: 'metrics', diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx index 4c5b5fef264c54..e21ca872726f55 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/components/search_bar/unified_search_bar.tsx @@ -111,7 +111,7 @@ const StickyContainer = ({ children }: { children: React.ReactNode }) => { top: calc(${actionMenuHeight}px + var(--euiFixedHeadersOffset, 0)); z-index: ${euiTheme.levels.navigation}; background: ${euiTheme.colors.emptyShade}; - padding: ${euiTheme.size.m} ${euiTheme.size.l} 0px; + padding: ${euiTheme.size.l} ${euiTheme.size.l} 0px; margin: -${euiTheme.size.l} -${euiTheme.size.l} 0px; min-height: calc(${euiTheme.size.xxxl} * 2); `} diff --git a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx index 4e97a99e37553a..80dc5a3977c75d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/hosts/index.tsx @@ -6,12 +6,12 @@ */ import { EuiErrorBoundary } from '@elastic/eui'; -import React, { useContext } from 'react'; +import React from 'react'; import { useTrackPageview, FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; import { APP_WRAPPER_CLASS } from '@kbn/core/public'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; -import { KibanaEnvironmentContext } from '../../../hooks/use_kibana'; +import { useKibanaEnvironmentContext } from '../../../hooks/use_kibana'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useSourceContext } from '../../../containers/metrics_source'; @@ -29,7 +29,7 @@ const HOSTS_FEEDBACK_LINK = export const HostsPage = () => { const { isLoading, loadSourceFailureMessage, loadSource, source } = useSourceContext(); - const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext(); useTrackPageview({ app: 'infra_metrics', path: 'hosts' }); useTrackPageview({ app: 'infra_metrics', path: 'hosts', delay: 15000 }); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx index 600291df4d4516..2e5db57604cea2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/flyout_home.tsx @@ -5,19 +5,31 @@ * 2.0. */ -import React, { useState, useCallback, useEffect } from 'react'; -import { EuiFlyoutHeader, EuiTitle, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; +import React, { useState, useCallback, useEffect, useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiText, EuiFlexGroup, EuiFlexItem, EuiCard, EuiIcon } from '@elastic/eui'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCard, + EuiFlexGroup, + EuiFlexItem, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiIcon, + EuiSpacer, + EuiTabs, + EuiTab, + EuiText, + EuiTitle, + useEuiTheme, +} from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiCallOut } from '@elastic/eui'; -import { EuiButton } from '@elastic/eui'; -import { EuiButtonEmpty } from '@elastic/eui'; import moment from 'moment'; -import { EuiTabs } from '@elastic/eui'; -import { EuiTab } from '@elastic/eui'; import { MLJobsAwaitingNodeWarning } from '@kbn/ml-plugin/public'; -import { useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { FeatureFeedbackButton, useLinkProps } from '@kbn/observability-shared-plugin/public'; +import { css } from '@emotion/react'; +import { KibanaEnvironmentContext } from '../../../../../../hooks/use_kibana'; import { SubscriptionSplashPrompt } from '../../../../../../components/subscription_splash_content'; import { useInfraMLCapabilitiesContext } from '../../../../../../containers/ml/infra_ml_capabilities'; import { @@ -36,6 +48,10 @@ interface Props { } type Tab = 'jobs' | 'anomalies'; + +export const INFRA_ML_FLYOUT_FEEDBACK_LINK = + 'https://docs.google.com/forms/d/e/1FAIpQLSfBixH_1HTuqeMCy38iK9w1mB8vl_eVvcLUlSPAPiWKBHeHiQ/viewform'; + export const FlyoutHome = (props: Props) => { const [tab, setTab] = useState('jobs'); const { goToSetup, closeFlyout } = props; @@ -51,6 +67,8 @@ export const FlyoutHome = (props: Props) => { } = useMetricK8sModuleContext(); const { hasInfraMLCapabilities, hasInfraMLReadCapabilities, hasInfraMLSetupCapabilities } = useInfraMLCapabilitiesContext(); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { euiTheme } = useEuiTheme(); const createHosts = useCallback(() => { goToSetup('hosts'); @@ -78,6 +96,14 @@ export const FlyoutHome = (props: Props) => { pathname: '/jobs', }); + // Used for prefilling the feedback form (if both types are enabled do not prefill) + const mlJobTypeByNode = + hostJobSummaries.length > 0 && k8sJobSummaries.length === 0 + ? 'host' + : hostJobSummaries.length === 0 && k8sJobSummaries.length > 0 + ? 'pod' + : undefined; + if (!hasInfraMLCapabilities) { return ; } else if (!hasInfraMLReadCapabilities) { @@ -95,33 +121,62 @@ export const FlyoutHome = (props: Props) => { } else { return ( <> - - -

- + + + +

+ +

+
+
+ + -

-
-
- - - setTab('jobs')}> - {i18n.translate('xpack.infra.ml.anomalyFlyout.jobsTabLabel', { - defaultMessage: 'Jobs', - })} - - setTab('anomalies')} - data-test-subj="anomalyFlyoutAnomaliesTab" + + + + - {i18n.translate('xpack.infra.ml.anomalyFlyout.anomaliesTabLabel', { - defaultMessage: 'Anomalies', - })} - - + setTab('jobs')}> + {i18n.translate('xpack.infra.ml.anomalyFlyout.jobsTabLabel', { + defaultMessage: 'Jobs', + })} + + setTab('anomalies')} + data-test-subj="anomalyFlyoutAnomaliesTab" + > + {i18n.translate('xpack.infra.ml.anomalyFlyout.anomaliesTabLabel', { + defaultMessage: 'Anomalies', + })} + + + { const { goHome } = props; const [startDate, setStartDate] = useState(now.clone().subtract(4, 'weeks')); const [partitionField, setPartitionField] = useState(null); - const h = useMetricHostsModuleContext(); - const k = useMetricK8sModuleContext(); + const host = useMetricHostsModuleContext(); + const kubernetes = useMetricK8sModuleContext(); const [filter, setFilter] = useState(''); const [filterQuery, setFilterQuery] = useState(''); const trackMetric = useUiTracker({ app: 'infra_metrics' }); const { createDerivedIndexPattern } = useSourceContext(); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { euiTheme } = useEuiTheme(); - const indicies = h.sourceConfiguration.indices; + const indices = host.sourceConfiguration.indices; const setupStatus = useMemo(() => { if (props.jobType === 'kubernetes') { - return k.setupStatus; + return kubernetes.setupStatus; } else { - return h.setupStatus; + return host.setupStatus; } - }, [props.jobType, k.setupStatus, h.setupStatus]); + }, [props.jobType, kubernetes.setupStatus, host.setupStatus]); const cleanUpAndSetUpModule = useMemo(() => { if (props.jobType === 'kubernetes') { - return k.cleanUpAndSetUpModule; + return kubernetes.cleanUpAndSetUpModule; } else { - return h.cleanUpAndSetUpModule; + return host.cleanUpAndSetUpModule; } - }, [props.jobType, k.cleanUpAndSetUpModule, h.cleanUpAndSetUpModule]); + }, [props.jobType, kubernetes.cleanUpAndSetUpModule, host.cleanUpAndSetUpModule]); const setUpModule = useMemo(() => { if (props.jobType === 'kubernetes') { - return k.setUpModule; + return kubernetes.setUpModule; } else { - return h.setUpModule; + return host.setUpModule; } - }, [props.jobType, k.setUpModule, h.setUpModule]); + }, [props.jobType, kubernetes.setUpModule, host.setUpModule]); const hasSummaries = useMemo(() => { if (props.jobType === 'kubernetes') { - return k.jobSummaries.length > 0; + return kubernetes.jobSummaries.length > 0; } else { - return h.jobSummaries.length > 0; + return host.jobSummaries.length > 0; } - }, [props.jobType, k.jobSummaries, h.jobSummaries]); + }, [props.jobType, kubernetes.jobSummaries, host.jobSummaries]); const derivedIndexPattern = useMemo( () => createDerivedIndexPattern(), @@ -92,7 +107,7 @@ export const JobSetupScreen = (props: Props) => { const createJobs = useCallback(() => { if (hasSummaries) { cleanUpAndSetUpModule( - indicies, + indices, moment(startDate).toDate().getTime(), undefined, filterQuery, @@ -100,7 +115,7 @@ export const JobSetupScreen = (props: Props) => { ); } else { setUpModule( - indicies, + indices, moment(startDate).toDate().getTime(), undefined, filterQuery, @@ -112,7 +127,7 @@ export const JobSetupScreen = (props: Props) => { filterQuery, setUpModule, hasSummaries, - indicies, + indices, partitionField, startDate, ]); @@ -163,15 +178,34 @@ export const JobSetupScreen = (props: Props) => { return ( <> - -

- + + +

+ +

+
+
+ + -

-
+ +
{setupStatus.type === 'pending' ? ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx index ce36a396b150f9..4dd620553df514 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_kubernetes.tsx @@ -6,11 +6,11 @@ */ import { EuiFlexGroup, EuiFlexItem, EuiGlobalToastList } from '@elastic/eui'; -import React, { useContext } from 'react'; +import React from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import useLocalStorage from 'react-use/lib/useLocalStorage'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; -import { KibanaEnvironmentContext } from '../../../../hooks/use_kibana'; +import { useKibanaEnvironmentContext } from '../../../../hooks/use_kibana'; const KUBERNETES_TOAST_STORAGE_KEY = 'kubernetesToastKey'; const KUBERNETES_FEEDBACK_LINK = 'https://ela.st/k8s-feedback'; @@ -19,7 +19,7 @@ export const SurveyKubernetes = () => { const [isToastSeen, setIsToastSeen] = useLocalStorage(KUBERNETES_TOAST_STORAGE_KEY, false); const markToastAsSeen = () => setIsToastSeen(true); - const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext(); return ( <> diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_section.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_section.tsx index d32fd722c8391d..6bdfc9dbfe2e3d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_section.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/survey_section.tsx @@ -5,9 +5,9 @@ * 2.0. */ -import React, { useContext } from 'react'; +import React from 'react'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; -import { KibanaEnvironmentContext } from '../../../../hooks/use_kibana'; +import { useKibanaEnvironmentContext } from '../../../../hooks/use_kibana'; import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; import { SurveyKubernetes } from './survey_kubernetes'; @@ -16,7 +16,7 @@ const INVENTORY_FEEDBACK_LINK = 'https://ela.st/survey-infra-inventory?usp=pp_ur export const SurveySection = () => { const { nodeType } = useWaffleOptionsContext(); - const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext(); return ( <> diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx index 7414d2ab4bf933..c89c9bafed5f43 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/asset_detail_page.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { useRouteMatch } from 'react-router-dom'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; -import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; import { NoRemoteCluster } from '../../../components/empty_states'; import { SourceErrorPage } from '../../../components/source_error_page'; import { SourceLoadingPage } from '../../../components/source_loading_page'; @@ -16,7 +15,6 @@ import { useSourceContext } from '../../../containers/metrics_source'; import { AssetDetails } from '../../../components/asset_details/asset_details'; import { MetricsPageTemplate } from '../page_template'; import { commonFlyoutTabs } from '../../../common/asset_details_config/asset_details_tabs'; -import { useParentBreadcrumbResolver } from './hooks/use_parent_breadcrumb_resolver'; export const AssetDetailPage = () => { const { isLoading, loadSourceFailureMessage, loadSource, source } = useSourceContext(); @@ -26,19 +24,6 @@ export const AssetDetailPage = () => { const { metricIndicesExist, remoteClustersExist } = source?.status ?? {}; - const parentBreadcrumbResolver = useParentBreadcrumbResolver(); - - const breadcrumbOptions = parentBreadcrumbResolver.getBreadcrumbOptions(nodeType); - useMetricsBreadcrumbs([ - { - ...breadcrumbOptions.link, - text: breadcrumbOptions.text, - }, - { - text: nodeId, - }, - ]); - if (isLoading || !source) return ; if (!remoteClustersExist) { @@ -55,21 +40,14 @@ export const AssetDetailPage = () => { return ; return ( - - - + metricAlias={source.configuration.metricAlias} + /> ); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx index 24fb24af5c9826..04405fee76b2d2 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/index.tsx @@ -16,7 +16,7 @@ import { MetricsTimeProvider } from './hooks/use_metrics_time'; export const NodeDetail = () => { const { params: { type: nodeType }, - } = useRouteMatch<{ type: InventoryItemType }>(); + } = useRouteMatch<{ type: InventoryItemType; node: string }>(); return ( diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx index 9e51d316dd0ec1..39553f849842f1 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/metric_detail_page.tsx @@ -11,7 +11,7 @@ import { useRouteMatch } from 'react-router-dom'; import { findInventoryModel } from '@kbn/metrics-data-access-plugin/common'; import type { InventoryItemType } from '@kbn/metrics-data-access-plugin/common'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; -import { useParentBreadcrumbResolver } from './hooks/use_parent_breadcrumb_resolver'; +import { useParentBreadcrumbResolver } from '../../../hooks/use_parent_breadcrumb_resolver'; import { useMetadata } from '../../../components/asset_details/hooks/use_metadata'; import { useSourceContext } from '../../../containers/metrics_source'; import { InfraLoadingPanel } from '../../../components/loading'; @@ -53,15 +53,18 @@ export const MetricDetailPage = () => { }); const breadcrumbOptions = parentBreadcrumbResolver.getBreadcrumbOptions(nodeType); - useMetricsBreadcrumbs([ - { - ...breadcrumbOptions.link, - text: breadcrumbOptions.text, - }, - { - text: name, - }, - ]); + useMetricsBreadcrumbs( + [ + { + ...breadcrumbOptions.link, + text: breadcrumbOptions.text, + }, + { + text: name, + }, + ], + { deeperContextServerless: true } + ); const [sideNav, setSideNav] = useState([]); diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts index 545b09c2fe00ef..2cf5c8844d726f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/types.ts @@ -8,7 +8,6 @@ import rt from 'io-ts'; import { EuiTheme } from '@kbn/kibana-react-plugin/common'; import { InventoryFormatterTypeRT } from '@kbn/metrics-data-access-plugin/common'; -import { LinkProps } from '@kbn/observability-shared-plugin/public/hooks/use_link_props'; import { MetricsTimeInput } from './hooks/use_metrics_time'; import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; @@ -62,8 +61,3 @@ export type VisSectionProps = rt.TypeOf & { isLiveStreaming?: boolean; stopLiveStreaming?: () => void; }; - -export interface BreadcrumbOptions { - text: string; - link: LinkProps; -} diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index d7764c14c43641..b1cea81cec0526 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -7,9 +7,9 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { useContext, useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useTrackPageview, FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; -import { KibanaEnvironmentContext } from '../../../hooks/use_kibana'; +import { useKibanaEnvironmentContext } from '../../../hooks/use_kibana'; import { SourceLoadingPage } from '../../../components/source_loading_page'; import { useMetricsExplorerViews } from '../../../hooks/use_metrics_explorer_views'; import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; @@ -52,7 +52,7 @@ export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExpl } = useMetricsExplorerState(source, derivedIndexPattern, enabled); const { currentView } = useMetricsExplorerViews(); const { source: sourceContext, metricIndicesExist } = useSourceContext(); - const { kibanaVersion, isCloudEnv, isServerlessEnv } = useContext(KibanaEnvironmentContext); + const { kibanaVersion, isCloudEnv, isServerlessEnv } = useKibanaEnvironmentContext(); useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer' }); useTrackPageview({ app: 'infra_metrics', path: 'metrics_explorer', delay: 15000 }); diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index b406f29fccd4d4..9a42e728bacda4 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -290,6 +290,12 @@ export class Plugin implements InfraClientPluginClass { }), path: '/settings', }, + { + id: 'assetDetails', + title: '', // Internal deep link, not shown in the UI. Title is dynamically set in the app. + path: '/detail', + visibleIn: [], + }, ]; }; diff --git a/x-pack/plugins/infra/public/types.ts b/x-pack/plugins/infra/public/types.ts index fe125a278d8cf5..f2069c215687fa 100644 --- a/x-pack/plugins/infra/public/types.ts +++ b/x-pack/plugins/infra/public/types.ts @@ -47,6 +47,7 @@ import { LicensingPluginSetup, LicensingPluginStart } from '@kbn/licensing-plugi import { ObservabilityAIAssistantPluginStart } from '@kbn/observability-ai-assistant-plugin/public'; import type { CloudSetup } from '@kbn/cloud-plugin/public'; import type { LicenseManagementUIPluginSetup } from '@kbn/license-management-plugin/public'; +import type { ServerlessPluginStart } from '@kbn/serverless/public'; import type { UnwrapPromise } from '../common/utility_types'; import { InventoryViewsServiceStart } from './services/inventory_views'; import { MetricsExplorerViewsServiceStart } from './services/metrics_explorer_views'; @@ -100,6 +101,7 @@ export interface InfraClientStartDeps { share: SharePluginStart; spaces: SpacesPluginStart; storage: IStorageWrapper; + serverless?: ServerlessPluginStart; triggersActionsUi: TriggersAndActionsUIPublicPluginStart; uiActions: UiActionsStart; unifiedSearch: UnifiedSearchPublicPluginStart; diff --git a/x-pack/plugins/infra/tsconfig.json b/x-pack/plugins/infra/tsconfig.json index acce31a2e36fec..a14063ef6aec77 100644 --- a/x-pack/plugins/infra/tsconfig.json +++ b/x-pack/plugins/infra/tsconfig.json @@ -86,6 +86,7 @@ "@kbn/core-ui-settings-browser", "@kbn/core-saved-objects-api-server", "@kbn/securitysolution-io-ts-utils", + "@kbn/serverless", "@kbn/core-lifecycle-server", "@kbn/elastic-agent-utils" ], diff --git a/x-pack/plugins/observability/public/embeddable/slo/error_budget/slo_error_budget_embeddable.tsx b/x-pack/plugins/observability/public/embeddable/slo/error_budget/slo_error_budget_embeddable.tsx index cf5b824289cbdb..8e47234492faf4 100644 --- a/x-pack/plugins/observability/public/embeddable/slo/error_budget/slo_error_budget_embeddable.tsx +++ b/x-pack/plugins/observability/public/embeddable/slo/error_budget/slo_error_budget_embeddable.tsx @@ -7,14 +7,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; - +import { Router } from '@kbn/shared-ux-router'; import { Embeddable as AbstractEmbeddable, EmbeddableOutput, IContainer, } from '@kbn/embeddable-plugin/public'; import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; - import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { type CoreStart, @@ -23,6 +22,7 @@ import { NotificationsStart, } from '@kbn/core/public'; import { Subject } from 'rxjs'; +import { createBrowserHistory } from 'history'; import type { SloErrorBudgetEmbeddableInput } from './types'; import { SloErrorBudget } from './slo_error_budget_burn_down'; export const SLO_ERROR_BUDGET_EMBEDDABLE = 'SLO_ERROR_BUDGET_EMBEDDABLE'; @@ -79,11 +79,13 @@ export class SLOErrorBudgetEmbeddable extends AbstractEmbeddable< const I18nContext = this.deps.i18n.Context; ReactDOM.render( - - - - - + + + + + + + , node ); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx index ea24804833e46a..2b69514fd47183 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/public/components/chat/chat_item_content_inline_prompt_editor.tsx @@ -28,6 +28,9 @@ interface Props { const textContainerClassName = css` padding: 4px 0; + img { + max-width: 100%; + } `; const editorContainerClassName = css` diff --git a/x-pack/plugins/observability_solution/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx b/x-pack/plugins/observability_solution/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx index d10d8262ee3d6f..282dfbe35770c5 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx +++ b/x-pack/plugins/observability_solution/observability_shared/public/components/feature_feedback_button/feature_feedback_button.tsx @@ -12,6 +12,9 @@ import { FormattedMessage } from '@kbn/i18n-react'; const KIBANA_VERSION_QUERY_PARAM = 'entry.548460210'; const KIBANA_DEPLOYMENT_TYPE_PARAM = 'entry.573002982'; const SANITIZED_PATH_PARAM = 'entry.1876422621'; +const ML_JOB_TYPE = 'entry.170406579'; + +type NodeType = 'host' | 'pod'; const getDeploymentType = (isCloudEnv?: boolean, isServerlessEnv?: boolean): string | undefined => { if (isServerlessEnv) { @@ -23,18 +26,23 @@ const getDeploymentType = (isCloudEnv?: boolean, isServerlessEnv?: boolean): str return 'Self-Managed (you manage)'; }; +const getMLJobType = (mlJobType: NodeType) => + mlJobType === 'pod' ? 'Pod Anomalies' : 'Host Anomalies'; + const getSurveyFeedbackURL = ({ formUrl, formConfig, kibanaVersion, deploymentType, sanitizedPath, + mlJobType, }: { formUrl: string; formConfig?: FormConfig; kibanaVersion?: string; deploymentType?: string; sanitizedPath?: string; + mlJobType?: string; }) => { const url = new URL(formUrl); if (kibanaVersion) { @@ -55,6 +63,9 @@ const getSurveyFeedbackURL = ({ sanitizedPath ); } + if (mlJobType) { + url.searchParams.append(formConfig?.mlJobTypeParam || ML_JOB_TYPE, mlJobType); + } return url.href; }; @@ -63,6 +74,7 @@ interface FormConfig { kibanaVersionQueryParam?: string; kibanaDeploymentTypeQueryParam?: string; sanitizedPathQueryParam?: string; + mlJobTypeParam?: string; } interface FeatureFeedbackButtonProps { @@ -75,6 +87,7 @@ interface FeatureFeedbackButtonProps { isCloudEnv?: boolean; isServerlessEnv?: boolean; sanitizedPath?: string; + nodeType?: NodeType; formConfig?: FormConfig; } @@ -88,6 +101,7 @@ export const FeatureFeedbackButton = ({ isCloudEnv, isServerlessEnv, sanitizedPath, + nodeType, surveyButtonText = ( { let ruleId: string; - let ruleName: string; - before(() => { - loadRule().then((data) => { - ruleId = data.id; - ruleName = data.name; - }); - }); - after(() => { + afterEach(() => { cleanupRule(ruleId); }); beforeEach(() => { + loadRule().then((data) => { + ruleId = data.id; + }); cy.login(ServerlessRoleName.SOC_MANAGER); }); it(`response actions should ${enabled ? 'be available ' : 'not be available'}`, () => { - cy.visit('/app/security/rules'); - clickRuleName(ruleName); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.getBySel('editRuleSettingsLink').click(); + cy.intercept('GET', `/api/detection_engine/rules?id=${ruleId}`).as('getRule'); + cy.visit(`/app/security/rules/id/${ruleId}/edit`); cy.getBySel('globalLoadingIndicator').should('not.exist'); + + // 2 calls are made to get the rule, so we need to wait for both since only on the second one's success the UI is updated + cy.wait('@getRule', { timeout: 2 * 60 * 1000 }) + .its('response.statusCode') + .should('eq', 200); + cy.wait('@getRule', { timeout: 2 * 60 * 1000 }) + .its('response.statusCode') + .should('eq', 200); + closeDateTabIfVisible(); cy.getBySel('edit-rule-actions-tab').click(); - cy.getBySel('globalLoadingIndicator').should('not.exist'); - cy.contains('Response actions are run on each rule execution.'); - cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + + // In rare cases, the button is not clickable due to the page not being fully loaded + recurse( + () => { + cy.getBySel(OSQUERY_RESPONSE_ACTION_ADD_BUTTON).click(); + + return cy.getBySel('alertActionAccordion').should(Cypress._.noop); + }, + ($el) => $el.length === 1, + { limit: 5, delay: 2000 } + ); + + // At this point we should have the response actions available or not if (enabled) { cy.getBySel(ENDPOINT_RESPONSE_ACTION_ADD_BUTTON).click(); cy.contains('Query is a required field'); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen.ts index 76ffcf4edf686c..043958a8567292 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.gen.ts @@ -16,7 +16,8 @@ import { z } from 'zod'; * version: 2023-10-31 */ -import { NonEmptyString } from '../model/rule_schema/common_attributes.gen'; +import { AlertIds } from '../../model/alert.gen'; +import { NonEmptyString } from '../../model/primitives.gen'; export type AlertAssignees = z.infer; export const AlertAssignees = z.object({ @@ -30,12 +31,6 @@ export const AlertAssignees = z.object({ remove: z.array(NonEmptyString), }); -/** - * A list of alerts ids. - */ -export type AlertIds = z.infer; -export const AlertIds = z.array(NonEmptyString).min(1); - export type SetAlertAssigneesRequestBody = z.infer; export const SetAlertAssigneesRequestBody = z.object({ /** diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.schema.yaml index 6c3663402118ab..77fd51fe99494e 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/alert_assignees/set_alert_assignees_route.schema.yaml @@ -23,7 +23,7 @@ paths: $ref: '#/components/schemas/AlertAssignees' description: Details about the assignees to assign and unassign. ids: - $ref: '#/components/schemas/AlertIds' + $ref: '../../model/alert.schema.yaml#/components/schemas/AlertIds' description: List of alerts ids to assign and unassign passed assignees. responses: 200: @@ -42,17 +42,10 @@ components: add: type: array items: - $ref: '../model/rule_schema/common_attributes.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: A list of users ids to assign. remove: type: array items: - $ref: '../model/rule_schema/common_attributes.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: A list of users ids to unassign. - - AlertIds: - type: array - items: - $ref: '../model/rule_schema/common_attributes.schema.yaml#/components/schemas/NonEmptyString' - minItems: 1 - description: A list of alerts ids. diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts index e3401d4cf16bab..2c76c7939eac18 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.gen.ts @@ -17,20 +17,7 @@ import { isValidDateMath } from '@kbn/zod-helpers'; * version: not applicable */ -/** - * A string that is not empty and does not contain only whitespace - */ -export type NonEmptyString = z.infer; -export const NonEmptyString = z - .string() - .min(1) - .regex(/^(?! *$).+$/); - -/** - * A universally unique identifier - */ -export type UUID = z.infer; -export const UUID = z.string().uuid(); +import { UUID, NonEmptyString } from '../../../model/primitives.gen'; export type RuleObjectId = z.infer; export const RuleObjectId = UUID; diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml index 1387072bf99e88..2d16ba7c971480 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/common_attributes.schema.yaml @@ -6,19 +6,8 @@ paths: {} components: x-codegen-enabled: true schemas: - NonEmptyString: - type: string - pattern: ^(?! *$).+$ - minLength: 1 - description: A string that is not empty and does not contain only whitespace - - UUID: - type: string - format: uuid - description: A universally unique identifier - RuleObjectId: - $ref: '#/components/schemas/UUID' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/UUID' RuleSignatureId: type: string @@ -289,9 +278,9 @@ components: type: object properties: name: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' type: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' ecs: type: boolean required: @@ -332,11 +321,11 @@ components: type: object properties: package: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' version: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' integration: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' required: - package - version @@ -354,7 +343,7 @@ components: field_names: type: array items: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' minItems: 1 required: - field_names @@ -426,7 +415,7 @@ components: params: $ref: '#/components/schemas/RuleActionParams' uuid: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' alerts_filter: $ref: '#/components/schemas/RuleActionAlertsFilter' frequency: @@ -453,10 +442,10 @@ components: type: object properties: id: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: ID of the exception container list_id: - $ref: '#/components/schemas/NonEmptyString' + $ref: '../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' description: List ID of the exception container type: $ref: '#/components/schemas/ExceptionListType' @@ -527,4 +516,4 @@ components: missingFieldsStrategy: $ref: '#/components/schemas/AlertSuppressionMissingFieldsStrategy' required: - - groupBy \ No newline at end of file + - groupBy diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.gen.ts index 2c8b36121cb15a..106a959f371b24 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.gen.ts @@ -16,7 +16,7 @@ import { z } from 'zod'; * version: not applicable */ -import { NonEmptyString } from '../common_attributes.gen'; +import { NonEmptyString } from '../../../../model/primitives.gen'; export type NewTermsFields = z.infer; export const NewTermsFields = z.array(z.string()).min(1).max(3); diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.schema.yaml index 4281cd3121f405..9f6bfa45089df1 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/new_terms_attributes.schema.yaml @@ -13,4 +13,4 @@ components: minItems: 1 maxItems: 3 HistoryWindowStart: - $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.gen.ts b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.gen.ts index 1ab2b29f3f9c22..fb1b16a574d547 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.gen.ts +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.gen.ts @@ -16,7 +16,7 @@ import { z } from 'zod'; * version: not applicable */ -import { NonEmptyString } from '../common_attributes.gen'; +import { NonEmptyString } from '../../../../model/primitives.gen'; /** * Query to execute diff --git a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.schema.yaml b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.schema.yaml index aa0cfd68dc0678..de43ecfeb073d2 100644 --- a/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/detection_engine/model/rule_schema/specific_attributes/threat_match_attributes.schema.yaml @@ -22,13 +22,13 @@ components: type: object properties: field: - $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' type: type: string enum: - mapping value: - $ref: '../common_attributes.schema.yaml#/components/schemas/NonEmptyString' + $ref: '../../../../model/primitives.schema.yaml#/components/schemas/NonEmptyString' required: - field - type diff --git a/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.gen.ts b/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.gen.ts index ea25624cd8aa80..d0a949367f6c75 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.gen.ts +++ b/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.gen.ts @@ -16,6 +16,8 @@ import { z } from 'zod'; * version: 2023-10-31 */ +import { AlertIds } from '../../../model/alert.gen'; + export type Id = z.infer; export const Id = z.string(); @@ -115,12 +117,6 @@ export const Types = z.array(Type); export type EndpointIds = z.infer; export const EndpointIds = z.array(z.string().min(1)).min(1); -/** - * If defined, any case associated with the given IDs will be updated (cannot contain empty strings) - */ -export type AlertIds = z.infer; -export const AlertIds = z.array(z.string().min(1)).min(1); - /** * Case IDs to be updated (cannot contain empty strings) */ diff --git a/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.schema.yaml b/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.schema.yaml index 8d7da517753398..bbac984a7fb15e 100644 --- a/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.schema.yaml +++ b/x-pack/plugins/security_solution/common/api/endpoint/model/schema/common.schema.yaml @@ -123,13 +123,6 @@ components: minLength: 1 minItems: 1 description: List of endpoint IDs (cannot contain empty strings) - AlertIds: - type: array - items: - type: string - minLength: 1 - minItems: 1 - description: If defined, any case associated with the given IDs will be updated (cannot contain empty strings) CaseIds: type: array items: @@ -151,7 +144,7 @@ components: endpoint_ids: $ref: '#/components/schemas/EndpointIds' alert_ids: - $ref: '#/components/schemas/AlertIds' + $ref: '../../../model/alert.schema.yaml#/components/schemas/AlertIds' case_ids: $ref: '#/components/schemas/CaseIds' comment: diff --git a/x-pack/plugins/security_solution/common/api/model/alert.gen.ts b/x-pack/plugins/security_solution/common/api/model/alert.gen.ts new file mode 100644 index 00000000000000..2dcc0da54891e3 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/model/alert.gen.ts @@ -0,0 +1,25 @@ +/* + * 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 { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Shared Alert Primitives Schema + * version: not applicable + */ + +import { NonEmptyString } from './primitives.gen'; + +/** + * A list of alerts ids. + */ +export type AlertIds = z.infer; +export const AlertIds = z.array(NonEmptyString).min(1); diff --git a/x-pack/plugins/security_solution/common/api/model/alert.schema.yaml b/x-pack/plugins/security_solution/common/api/model/alert.schema.yaml new file mode 100644 index 00000000000000..f28508dc620f2d --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/model/alert.schema.yaml @@ -0,0 +1,14 @@ +openapi: 3.0.0 +info: + title: Shared Alert Primitives Schema + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + AlertIds: + type: array + items: + $ref: './primitives.schema.yaml#/components/schemas/NonEmptyString' + minItems: 1 + description: A list of alerts ids. diff --git a/x-pack/plugins/security_solution/common/api/model/index.ts b/x-pack/plugins/security_solution/common/api/model/index.ts new file mode 100644 index 00000000000000..3f1fc085d3d8d7 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/model/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 './primitives.gen'; +export * from './alert.gen'; diff --git a/x-pack/plugins/security_solution/common/api/model/primitives.gen.ts b/x-pack/plugins/security_solution/common/api/model/primitives.gen.ts new file mode 100644 index 00000000000000..8c6860cd8fb992 --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/model/primitives.gen.ts @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { z } from 'zod'; + +/* + * NOTICE: Do not edit this file manually. + * This file is automatically generated by the OpenAPI Generator, @kbn/openapi-generator. + * + * info: + * title: Shared Primitives Schema + * version: not applicable + */ + +/** + * A string that is not empty and does not contain only whitespace + */ +export type NonEmptyString = z.infer; +export const NonEmptyString = z + .string() + .min(1) + .regex(/^(?! *$).+$/); + +/** + * A universally unique identifier + */ +export type UUID = z.infer; +export const UUID = z.string().uuid(); diff --git a/x-pack/plugins/security_solution/common/api/model/primitives.schema.yaml b/x-pack/plugins/security_solution/common/api/model/primitives.schema.yaml new file mode 100644 index 00000000000000..177ad2ed30ecca --- /dev/null +++ b/x-pack/plugins/security_solution/common/api/model/primitives.schema.yaml @@ -0,0 +1,18 @@ +openapi: 3.0.0 +info: + title: Shared Primitives Schema + version: 'not applicable' +paths: {} +components: + x-codegen-enabled: true + schemas: + NonEmptyString: + type: string + pattern: ^(?! *$).+$ + minLength: 1 + description: A string that is not empty and does not contain only whitespace + + UUID: + type: string + format: uuid + description: A universally unique identifier diff --git a/x-pack/plugins/security_solution/common/experimental_features.ts b/x-pack/plugins/security_solution/common/experimental_features.ts index b98e5f3161034e..5465d8fc2c5078 100644 --- a/x-pack/plugins/security_solution/common/experimental_features.ts +++ b/x-pack/plugins/security_solution/common/experimental_features.ts @@ -21,6 +21,7 @@ export const allowedExperimentalValues = Object.freeze({ kubernetesEnabled: true, chartEmbeddablesEnabled: true, donutChartEmbeddablesEnabled: false, // Depends on https://github.com/elastic/kibana/issues/136409 item 2 - 6 + /** * This is used for enabling the end-to-end tests for the security_solution telemetry. * We disable the telemetry since we don't have specific roles or permissions around it and @@ -70,7 +71,7 @@ export const allowedExperimentalValues = Object.freeze({ */ responseActionUploadEnabled: true, - /* + /** * Enables Automated Endpoint Process actions */ automatedProcessActionsEnabled: false, @@ -84,22 +85,25 @@ export const allowedExperimentalValues = Object.freeze({ * Enables top charts on Alerts Page */ alertsPageChartsEnabled: true, + /** * Enables the alert type column in KPI visualizations on Alerts Page */ alertTypeEnabled: false, + /** * Enables expandable flyout in create rule page, alert preview */ expandableFlyoutInCreateRuleEnabled: true, + /** * Enables expandable flyout for event type documents */ expandableEventFlyoutEnabled: false, - /* + + /** * Enables new Set of filters on the Alerts page. - * - **/ + */ alertsPageFiltersEnabled: true, /** @@ -107,23 +111,20 @@ export const allowedExperimentalValues = Object.freeze({ */ assistantModelEvaluation: false, - /* + /** * Enables the new user details flyout displayed on the Alerts table. - * - **/ + */ newUserDetailsFlyout: true, - /* + /** * Enables the Managed User section inside the new user details flyout. * To see this section you also need newUserDetailsFlyout flag enabled. - * - **/ + */ newUserDetailsFlyoutManagedUser: false, - /* + /** * Enables the new host details flyout displayed on the Alerts table. - * - **/ + */ newHostDetailsFlyout: true, /** @@ -163,7 +164,7 @@ export const allowedExperimentalValues = Object.freeze({ */ alertSuppressionForIndicatorMatchRuleEnabled: false, - /* + /** * Enables experimental Experimental S1 integration data to be available in Analyzer */ sentinelOneDataInAnalyzerEnabled: false, @@ -173,12 +174,12 @@ export const allowedExperimentalValues = Object.freeze({ */ sentinelOneManualHostActionsEnabled: true, - /* + /** * Enables experimental Crowdstrike integration data to be available in Analyzer */ crowdstrikeDataInAnalyzerEnabled: false, - /* + /** * Enables experimental "Updates" tab in the prebuilt rule upgrade flyout. * This tab shows the JSON diff between the installed prebuilt rule * version and the latest available version. @@ -190,26 +191,27 @@ export const allowedExperimentalValues = Object.freeze({ * Expires: on Feb 20, 2024 */ jsonPrebuiltRulesDiffingEnabled: true, - /* - * Disables discover esql tab within timeline - * - */ - timelineEsqlTabDisabled: false, - - /* - * Disables date pickers and sourcerer in analyzer if needed. - * - */ - analyzerDatePickersAndSourcererDisabled: false, /** * Enables per-field rule diffs tab in the prebuilt rule upgrade flyout * * Ticket: https://github.com/elastic/kibana/issues/166489 * Owners: https://github.com/orgs/elastic/teams/security-detection-rule-management - * Added: on Feb 12, 2023 in https://github.com/elastic/kibana/pull/174564 + * Added: on Feb 12, 2024 in https://github.com/elastic/kibana/pull/174564 + * Turned: on Feb 23, 2024 in https://github.com/elastic/kibana/pull/177495 + * Expires: on Apr 23, 2024 */ - perFieldPrebuiltRulesDiffingEnabled: false, + perFieldPrebuiltRulesDiffingEnabled: true, + + /** + * Disables discover esql tab within timeline + */ + timelineEsqlTabDisabled: false, + + /** + * Disables date pickers and sourcerer in analyzer if needed. + */ + analyzerDatePickersAndSourcererDisabled: false, }); type ExperimentalConfigKeys = Array; diff --git a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx index 9b3a362e634953..17a2e9f0f81f95 100644 --- a/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx +++ b/x-pack/plugins/security_solution/public/entity_analytics/components/risk_score_preview_table.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { EuiInMemoryTable } from '@elastic/eui'; import type { EuiBasicTableColumn } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; import type { RiskSeverity } from '../../../common/search_strategy'; import { RiskScoreLevel } from './severity/common'; @@ -31,7 +32,12 @@ export const RiskScorePreviewTable = ({ const columns: RiskScoreColumn[] = [ { field: 'id_value', - name: 'Name', + name: ( + + ), render: (itemName: string) => { return type === RiskScoreEntity.host ? ( @@ -42,7 +48,13 @@ export const RiskScorePreviewTable = ({ }, { field: 'calculated_level', - name: 'Level', + name: ( + + ), + render: (risk: RiskSeverity | null) => { if (risk != null) { return ; @@ -53,8 +65,12 @@ export const RiskScorePreviewTable = ({ }, { field: 'calculated_score_norm', - // align: 'right', - name: 'Score norm', + name: ( + + ), render: (scoreNorm: number | null) => { if (scoreNorm != null) { return Math.round(scoreNorm * 100) / 100; diff --git a/x-pack/plugins/serverless_observability/public/navigation_tree.ts b/x-pack/plugins/serverless_observability/public/navigation_tree.ts index dd36baefdb1037..4a1a180f0cc2a3 100644 --- a/x-pack/plugins/serverless_observability/public/navigation_tree.ts +++ b/x-pack/plugins/serverless_observability/public/navigation_tree.ts @@ -155,6 +155,10 @@ export const navigationTree: NavigationTreeDefinition = { return pathNameSerialized.startsWith(prepend('/app/apm/dependencies')); }, }, + { + link: 'apm:settings', + sideNavStatus: 'hidden', // only to be considered in the breadcrumbs + }, ], }, { @@ -176,6 +180,14 @@ export const navigationTree: NavigationTreeDefinition = { return pathNameSerialized.startsWith(prepend('/app/metrics/hosts')); }, }, + { + link: 'metrics:settings', + sideNavStatus: 'hidden', // only to be considered in the breadcrumbs + }, + { + link: 'metrics:assetDetails', + sideNavStatus: 'hidden', // only to be considered in the breadcrumbs + }, ], }, { diff --git a/x-pack/plugins/serverless_search/common/i18n_string.ts b/x-pack/plugins/serverless_search/common/i18n_string.ts index b961f3a4aed5e5..cf0dbad5277c8d 100644 --- a/x-pack/plugins/serverless_search/common/i18n_string.ts +++ b/x-pack/plugins/serverless_search/common/i18n_string.ts @@ -89,3 +89,9 @@ export const CONFIGURATION_LABEL = i18n.translate( defaultMessage: 'Configuration', } ); +export const SCHEDULING_LABEL = i18n.translate( + 'xpack.serverlessSearch.connectors.schedulingLabel', + { + defaultMessage: 'Scheduling', + } +); diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx new file mode 100644 index 00000000000000..12961bfc4a093e --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/conector_scheduling_tab/connector_scheduling.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useState } from 'react'; +import { Connector, ConnectorStatus } from '@kbn/search-connectors'; +import { ConnectorSchedulingComponent } from '@kbn/search-connectors/components/scheduling/connector_scheduling'; +import { useConnectorScheduling } from '../../../hooks/api/use_update_connector_scheduling'; + +interface ConnectorSchedulingPanels { + connector: Connector; +} +export const ConnectorScheduling: React.FC = ({ connector }) => { + const [hasChanges, setHasChanges] = useState(false); + const { isLoading, mutate } = useConnectorScheduling(connector.id); + const hasIncrementalSyncFeature = connector?.features?.incremental_sync ?? false; + const shouldShowIncrementalSync = + hasIncrementalSyncFeature && (connector?.features?.incremental_sync?.enabled ?? false); + return ( + + ); +}; diff --git a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_configuration.tsx b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_configuration.tsx index 87607ce1d6267a..9513ca197bb66a 100644 --- a/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_configuration.tsx +++ b/x-pack/plugins/serverless_search/public/application/components/connectors/connector_config/connector_configuration.tsx @@ -17,12 +17,17 @@ import { EuiTabbedContentTab, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { CONFIGURATION_LABEL, OVERVIEW_LABEL } from '../../../../../common/i18n_string'; +import { + CONFIGURATION_LABEL, + OVERVIEW_LABEL, + SCHEDULING_LABEL, +} from '../../../../../common/i18n_string'; import { ConnectorLinkElasticsearch } from './connector_link'; import { ConnectorConfigFields } from './connector_config_fields'; import { ConnectorIndexName } from './connector_index_name'; import { ConnectorConfigurationPanels } from './connector_config_panels'; import { ConnectorOverview } from './connector_overview'; +import { ConnectorScheduling } from '../conector_scheduling_tab/connector_scheduling'; interface ConnectorConfigurationProps { connector: Connector; @@ -108,6 +113,16 @@ export const ConnectorConfiguration: React.FC = ({ id: 'configuration', name: CONFIGURATION_LABEL, }, + { + content: ( + <> + + + + ), + id: 'scheduling', + name: SCHEDULING_LABEL, + }, ]; return currentStep === 'connected' ? ( diff --git a/x-pack/plugins/serverless_search/public/application/hooks/api/use_update_connector_scheduling.tsx b/x-pack/plugins/serverless_search/public/application/hooks/api/use_update_connector_scheduling.tsx new file mode 100644 index 00000000000000..e11991a5c53b46 --- /dev/null +++ b/x-pack/plugins/serverless_search/public/application/hooks/api/use_update_connector_scheduling.tsx @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ /* + * 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 { SchedulingConfiguraton } from '@kbn/search-connectors'; +import { useMutation } from '@tanstack/react-query'; +import { useKibanaServices } from '../use_kibana'; + +export const useConnectorScheduling = (connectorId: string) => { + const { http } = useKibanaServices(); + return useMutation({ + mutationFn: async (configuration: SchedulingConfiguraton) => { + return await http.post(`/internal/serverless_search/connectors/${connectorId}/scheduling`, { + body: JSON.stringify({ ...configuration }), + }); + }, + }); +}; diff --git a/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts b/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts index d7ca0329e97019..767387d85e3258 100644 --- a/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts +++ b/x-pack/plugins/serverless_search/server/routes/connectors_routes.ts @@ -17,6 +17,7 @@ import { updateConnectorConfiguration, updateConnectorIndexName, updateConnectorNameAndDescription, + updateConnectorScheduling, updateConnectorServiceType, } from '@kbn/search-connectors'; import { RouteDependencies } from '../plugin'; @@ -337,4 +338,28 @@ export const registerConnectorsRoutes = ({ http, router }: RouteDependencies) => }); } ); + router.post( + { + path: '/internal/serverless_search/connectors/{connectorId}/scheduling', + validate: { + body: schema.object({ + access_control: schema.object({ enabled: schema.boolean(), interval: schema.string() }), + full: schema.object({ enabled: schema.boolean(), interval: schema.string() }), + incremental: schema.object({ enabled: schema.boolean(), interval: schema.string() }), + }), + params: schema.object({ + connectorId: schema.string(), + }), + }, + }, + async (context, request, response) => { + const { client } = (await context.core).elasticsearch; + await updateConnectorScheduling( + client.asCurrentUser, + request.params.connectorId, + request.body + ); + return response.ok(); + } + ); }; diff --git a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts index b537f836acfe72..69ff7a03822b2f 100644 --- a/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts +++ b/x-pack/test/fleet_api_integration/apis/agent_policy/agent_policy.ts @@ -232,6 +232,32 @@ export default function (providerContext: FtrProviderContext) { }); }); + it('should create .fleet-policies document with inputs', async () => { + const res = await supertest + .post(`/api/fleet/agent_policies?sys_monitoring=true`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'test-policy-with-system', + namespace: 'default', + force: true, // using force to bypass package verification error + }) + .expect(200); + + const policyDocRes = await es.search({ + index: '.fleet-policies', + query: { + term: { + policy_id: res.body.item.id, + }, + }, + }); + + expect(policyDocRes?.hits?.hits.length).to.eql(1); + const source = policyDocRes?.hits?.hits[0]?._source as any; + expect(source?.revision_idx).to.eql(1); + expect(source?.data?.inputs.length).to.eql(3); + }); + it('should return a 400 with an empty namespace', async () => { await supertest .post(`/api/fleet/agent_policies`) diff --git a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts index fcbdd0981780f0..efe17758ea53b4 100644 --- a/x-pack/test/fleet_api_integration/apis/outputs/crud.ts +++ b/x-pack/test/fleet_api_integration/apis/outputs/crud.ts @@ -274,7 +274,7 @@ export default function (providerContext: FtrProviderContext) { document: { state: 'HEALTHY', message: '', - '@timestamp': '' + Date.parse('2023-11-29T14:25:31Z'), + '@timestamp': new Date(Date.now() - 1).toISOString(), output: defaultOutputId, }, }); @@ -285,7 +285,7 @@ export default function (providerContext: FtrProviderContext) { document: { state: 'DEGRADED', message: 'connection error', - '@timestamp': '' + Date.parse('2023-11-30T14:25:31Z'), + '@timestamp': new Date().toISOString(), output: defaultOutputId, }, }); @@ -297,7 +297,7 @@ export default function (providerContext: FtrProviderContext) { state: 'HEALTHY', message: '', '@timestamp': '' + Date.parse('2023-11-31T14:25:31Z'), - output: 'remote2', + output: ESOutputId, }, }); }); @@ -310,6 +310,13 @@ export default function (providerContext: FtrProviderContext) { expect(outputHealth.message).to.equal('connection error'); expect(outputHealth.timestamp).not.to.be.empty(); }); + it('should not return output health if older than output last updated time', async () => { + const { body: outputHealth } = await supertest + .get(`/api/fleet/outputs/${ESOutputId}/health`) + .expect(200); + + expect(outputHealth.state).to.equal('UNKNOWN'); + }); }); describe('PUT /outputs/{outputId}', () => { diff --git a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/alert_assignees.ts b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/alert_assignees.ts index 59c70d5d6bd9e8..265dade199959c 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/alert_assignees.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/detections_response/utils/alerts/alert_assignees.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertIds } from '@kbn/security-solution-plugin/common/api/detection_engine'; +import { AlertIds } from '@kbn/security-solution-plugin/common/api/model'; import { SetAlertAssigneesRequestBody } from '@kbn/security-solution-plugin/common/api/detection_engine'; export const setAlertAssignees = ({