From bb841fd09da2d5726d3a5a4ff913feac70835d4e Mon Sep 17 00:00:00 2001 From: Jesus Wahrman Date: Mon, 18 Nov 2024 14:29:27 +0100 Subject: [PATCH 01/50] remove all code related to savedObjectClientContrat that wasn't being used --- src/plugins/saved_objects/public/index.ts | 2 +- .../saved_object/helpers/apply_es_resp.ts | 101 --------------- .../helpers/check_for_duplicate_title.ts | 65 ---------- .../helpers/confirm_modal_promise.tsx | 51 -------- .../saved_object/helpers/create_source.ts | 74 ----------- .../display_duplicate_title_confirm_modal.ts | 38 ------ .../helpers/field_mapping/index.ts | 11 -- .../field_mapping/mapping_setup.test.ts | 45 ------- .../helpers/field_mapping/mapping_setup.ts | 35 ------ .../helpers/field_mapping/types.ts | 20 --- .../helpers/find_object_by_title.test.ts | 38 ------ .../helpers/find_object_by_title.ts | 45 ------- .../helpers/hydrate_index_pattern.ts | 40 ------ .../helpers/initialize_saved_object.ts | 50 -------- .../saved_object/helpers/save_saved_object.ts | 107 +--------------- .../helpers/save_with_confirmation.test.ts | 118 ------------------ .../helpers/save_with_confirmation.ts | 79 ------------ .../helpers/serialize_saved_object.ts | 55 -------- .../public/saved_object/index.ts | 2 - src/plugins/saved_objects/public/types.ts | 14 +-- 20 files changed, 3 insertions(+), 987 deletions(-) delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/confirm_modal_promise.tsx delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/create_source.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts delete mode 100644 src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts diff --git a/src/plugins/saved_objects/public/index.ts b/src/plugins/saved_objects/public/index.ts index 6d7a013cf59ca..c0c8ae33f6b92 100644 --- a/src/plugins/saved_objects/public/index.ts +++ b/src/plugins/saved_objects/public/index.ts @@ -11,7 +11,7 @@ import { SavedObjectsPublicPlugin } from './plugin'; export type { OnSaveProps, OriginSaveModalProps, SaveModalState, SaveResult } from './save_modal'; export { SavedObjectSaveModal, SavedObjectSaveModalOrigin, showSaveModal } from './save_modal'; -export { checkForDuplicateTitle, saveWithConfirmation, isErrorNonFatal } from './saved_object'; +export { isErrorNonFatal } from './saved_object'; export type { SavedObjectSaveOpts, SavedObject, SavedObjectConfig } from './types'; export const plugin = () => new SavedObjectsPublicPlugin(); diff --git a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts b/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts deleted file mode 100644 index 1bd1a7de29ec8..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/apply_es_resp.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { cloneDeep, defaults, forOwn, assign } from 'lodash'; -import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; -import { injectSearchSourceReferences, parseSearchSourceJSON } from '@kbn/data-plugin/public'; -import type { DataView } from '@kbn/data-views-plugin/public'; -import { EsResponse, SavedObject, SavedObjectConfig, SavedObjectKibanaServices } from '../../types'; -import { expandShorthand } from './field_mapping'; - -/** - * A given response of and ElasticSearch containing a plain saved object is applied to the given - * savedObject - */ -export async function applyESResp( - resp: EsResponse, - savedObject: SavedObject, - config: SavedObjectConfig, - dependencies: SavedObjectKibanaServices -) { - const mapping = expandShorthand(config.mapping ?? {}); - const savedObjectType = config.type || ''; - savedObject._source = cloneDeep(resp._source); - if (typeof resp.found === 'boolean' && !resp.found) { - throw new SavedObjectNotFound(savedObjectType, savedObject.id || ''); - } - - const meta = resp._source.kibanaSavedObjectMeta || {}; - delete resp._source.kibanaSavedObjectMeta; - - if (!config.indexPattern && savedObject._source.indexPattern) { - config.indexPattern = savedObject._source.indexPattern as DataView; - delete savedObject._source.indexPattern; - } - - // assign the defaults to the response - defaults(savedObject._source, savedObject.defaults); - - // transform the source using _deserializers - forOwn(mapping, (fieldMapping, fieldName) => { - if (fieldMapping._deserialize && typeof fieldName === 'string') { - savedObject._source[fieldName] = fieldMapping._deserialize( - savedObject._source[fieldName] as string - ); - } - }); - - // Give obj all of the values in _source.fields - assign(savedObject, savedObject._source); - savedObject.lastSavedTitle = savedObject.title; - - if (meta.searchSourceJSON) { - try { - let searchSourceValues = parseSearchSourceJSON(meta.searchSourceJSON); - - if (config.searchSource) { - searchSourceValues = injectSearchSourceReferences( - searchSourceValues as any, - resp.references - ); - savedObject.searchSource = await dependencies.search.searchSource.create( - searchSourceValues - ); - } else { - savedObject.searchSourceFields = searchSourceValues; - } - } catch (error) { - if ( - error.constructor.name === 'SavedObjectNotFound' && - error.savedObjectType === 'index-pattern' - ) { - // if parsing the search source fails because the index pattern wasn't found, - // remember the reference - this is required for error handling on legacy imports - savedObject.unresolvedIndexPatternReference = { - name: 'kibanaSavedObjectMeta.searchSourceJSON.index', - id: JSON.parse(meta.searchSourceJSON).index, - type: 'index-pattern', - }; - } - - throw error; - } - } - - const injectReferences = config.injectReferences; - if (injectReferences && resp.references && resp.references.length > 0) { - injectReferences(savedObject, resp.references); - } - - if (typeof config.afterESResp === 'function') { - savedObject = await config.afterESResp(savedObject); - } - - return savedObject; -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts b/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts deleted file mode 100644 index 78f7a5cb31d8e..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/check_for_duplicate_title.ts +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObject, SavedObjectKibanaServices, StartServices } from '../../types'; -import { findObjectByTitle } from './find_object_by_title'; -import { SAVE_DUPLICATE_REJECTED } from '../../constants'; -import { displayDuplicateTitleConfirmModal } from './display_duplicate_title_confirm_modal'; - -/** - * check for an existing SavedObject with the same title in ES - * returns Promise when it's no duplicate, or the modal displaying the warning - * that's there's a duplicate is confirmed, else it returns a rejected Promise - * @param savedObject - * @param isTitleDuplicateConfirmed - * @param onTitleDuplicate - * @param services - * @param startServices - */ -export async function checkForDuplicateTitle( - savedObject: Pick< - SavedObject, - 'id' | 'title' | 'getDisplayName' | 'lastSavedTitle' | 'copyOnSave' | 'getEsType' - >, - isTitleDuplicateConfirmed: boolean, - onTitleDuplicate: (() => void) | undefined, - services: Pick, - startServices: StartServices -): Promise { - const { savedObjectsClient, overlays } = services; - // Don't check for duplicates if user has already confirmed save with duplicate title - if (isTitleDuplicateConfirmed) { - return true; - } - - // Don't check if the user isn't updating the title, otherwise that would become very annoying to have - // to confirm the save every time, except when copyOnSave is true, then we do want to check. - if (savedObject.title === savedObject.lastSavedTitle && !savedObject.copyOnSave) { - return true; - } - - const duplicate = await findObjectByTitle( - savedObjectsClient, - savedObject.getEsType(), - savedObject.title - ); - - if (!duplicate || duplicate.id === savedObject.id) { - return true; - } - - if (onTitleDuplicate) { - onTitleDuplicate(); - return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); - } - - // TODO: make onTitleDuplicate a required prop and remove UI components from this class - // Need to leave here until all users pass onTitleDuplicate. - return displayDuplicateTitleConfirmModal(savedObject, overlays, startServices); -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/confirm_modal_promise.tsx b/src/plugins/saved_objects/public/saved_object/helpers/confirm_modal_promise.tsx deleted file mode 100644 index afdafaa5483c3..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/confirm_modal_promise.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { CoreStart, OverlayStart } from '@kbn/core/public'; -import { i18n } from '@kbn/i18n'; -import { EuiConfirmModal } from '@elastic/eui'; -import { toMountPoint } from '@kbn/react-kibana-mount'; - -type StartServices = Pick; - -export function confirmModalPromise( - message = '', - title = '', - confirmBtnText = '', - overlays: OverlayStart, - startServices: StartServices -): Promise { - return new Promise((resolve, reject) => { - const cancelButtonText = i18n.translate('savedObjects.confirmModal.cancelButtonLabel', { - defaultMessage: 'Cancel', - }); - - const modal = overlays.openModal( - toMountPoint( - { - modal.close(); - reject(); - }} - onConfirm={() => { - modal.close(); - resolve(true); - }} - confirmButtonText={confirmBtnText} - cancelButtonText={cancelButtonText} - title={title} - > - {message} - , - startServices - ) - ); - }); -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts b/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts deleted file mode 100644 index 42d3c956cdad2..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/create_source.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { SavedObjectAttributes } from '@kbn/core/public'; -import { SavedObject, SavedObjectKibanaServices, StartServices } from '../../types'; -import { OVERWRITE_REJECTED } from '../../constants'; -import { confirmModalPromise } from './confirm_modal_promise'; - -/** - * Attempts to create the current object using the serialized source. If an object already - * exists, a warning message requests an overwrite confirmation. - * @param source - serialized version of this object (return value from this._serialize()) - * What will be indexed into elasticsearch. - * @param savedObject - savedObject - * @param esType - type of the saved object - * @param options - options to pass to the saved object create method - * @param services - provides Kibana services savedObjectsClient and overlays - * @returns {Promise} - A promise that is resolved with the objects id if the object is - * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with - * a confirmRejected = true parameter so that case can be handled differently than - * a create or index error. - * @resolved {SavedObject} - */ -export async function createSource( - source: SavedObjectAttributes, - savedObject: SavedObject, - esType: string, - options = {}, - services: SavedObjectKibanaServices, - startServices: StartServices -) { - const { savedObjectsClient, overlays } = services; - try { - return await savedObjectsClient.create(esType, source, options); - } catch (err) { - // record exists, confirm overwriting - if (get(err, 'res.status') === 409) { - const confirmMessage = i18n.translate( - 'savedObjects.confirmModal.overwriteConfirmationMessage', - { - defaultMessage: 'Are you sure you want to overwrite {title}?', - values: { title: savedObject.title }, - } - ); - - const title = i18n.translate('savedObjects.confirmModal.overwriteTitle', { - defaultMessage: 'Overwrite {name}?', - values: { name: savedObject.getDisplayName() }, - }); - const confirmButtonText = i18n.translate('savedObjects.confirmModal.overwriteButtonLabel', { - defaultMessage: 'Overwrite', - }); - - return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays, startServices) - .then(() => - savedObjectsClient.create( - esType, - source, - savedObject.creationOpts({ overwrite: true, ...options }) - ) - ) - .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); - } - return await Promise.reject(err); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts b/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts deleted file mode 100644 index 3dec7f93f1d19..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/display_duplicate_title_confirm_modal.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { OverlayStart } from '@kbn/core/public'; -import { SAVE_DUPLICATE_REJECTED } from '../../constants'; -import { confirmModalPromise } from './confirm_modal_promise'; -import { SavedObject, StartServices } from '../../types'; - -export function displayDuplicateTitleConfirmModal( - savedObject: Pick, - overlays: OverlayStart, - startServices: StartServices -): Promise { - const confirmMessage = i18n.translate( - 'savedObjects.confirmModal.saveDuplicateConfirmationMessage', - { - defaultMessage: `A {name} with the title ''{title}'' already exists. Would you like to save anyway?`, - values: { title: savedObject.title, name: savedObject.getDisplayName() }, - } - ); - - const confirmButtonText = i18n.translate('savedObjects.confirmModal.saveDuplicateButtonLabel', { - defaultMessage: 'Save {name}', - values: { name: savedObject.getDisplayName() }, - }); - try { - return confirmModalPromise(confirmMessage, '', confirmButtonText, overlays, startServices); - } catch (_) { - return Promise.reject(new Error(SAVE_DUPLICATE_REJECTED)); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts deleted file mode 100644 index 850352da2b188..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { FieldMappingSpec, MappingObject } from './types'; -export { expandShorthand } from './mapping_setup'; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts deleted file mode 100644 index dbf7d5c38eec3..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { expandShorthand } from './mapping_setup'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; - -describe('mapping_setup', () => { - it('allows shortcuts for field types by just setting the value to the type name', () => { - const mapping = expandShorthand({ foo: ES_FIELD_TYPES.BOOLEAN }); - - expect(mapping.foo.type).toBe('boolean'); - }); - - it('can set type as an option', () => { - const mapping = expandShorthand({ foo: { type: ES_FIELD_TYPES.INTEGER } }); - - expect(mapping.foo.type).toBe('integer'); - }); - - describe('when type is json', () => { - it('returned object is type text', () => { - const mapping = expandShorthand({ foo: 'json' }); - - expect(mapping.foo.type).toBe('text'); - }); - - it('returned object has _serialize function', () => { - const mapping = expandShorthand({ foo: 'json' }); - - expect(mapping.foo._serialize).toBeInstanceOf(Function); - }); - - it('returned object has _deserialize function', () => { - const mapping = expandShorthand({ foo: 'json' }); - - expect(mapping.foo._serialize).toBeInstanceOf(Function); - }); - }); -}); diff --git a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts deleted file mode 100644 index cc4de351eb3c0..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/mapping_setup.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { mapValues, isString } from 'lodash'; -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; -import { FieldMappingSpec, MappingObject } from './types'; - -// import from ./common/types to prevent circular dependency of kibana_utils <-> data plugin - -/** @private */ -type ShorthandFieldMapObject = FieldMappingSpec | ES_FIELD_TYPES | 'json'; - -/** @public */ -export const expandShorthand = (sh: Record): MappingObject => { - return mapValues(sh, (val: ShorthandFieldMapObject) => { - const fieldMap = isString(val) ? { type: val } : val; - const json: FieldMappingSpec = { - type: ES_FIELD_TYPES.TEXT, - _serialize(v) { - if (v) return JSON.stringify(v); - }, - _deserialize(v) { - if (v) return JSON.parse(v); - }, - }; - - return fieldMap.type === 'json' ? json : fieldMap; - }) as MappingObject; -}; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts b/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts deleted file mode 100644 index 4a1b3e0d3892d..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/field_mapping/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ES_FIELD_TYPES } from '@kbn/data-plugin/public'; - -/** @public */ -export interface FieldMappingSpec { - type: ES_FIELD_TYPES; - _serialize?: (mapping: any) => string | undefined; - _deserialize?: (mapping: string) => any | undefined; -} - -/** @public */ -export type MappingObject = Record; diff --git a/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts b/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts deleted file mode 100644 index c570165ae1f81..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.test.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { findObjectByTitle } from './find_object_by_title'; -import { SavedObjectsClientContract, SavedObject } from '@kbn/core/public'; -import { simpleSavedObjectMock } from '@kbn/core/public/mocks'; - -describe('findObjectByTitle', () => { - const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; - - beforeEach(() => { - savedObjectsClient.find = jest.fn(); - }); - - it('returns undefined if title is not provided', async () => { - const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', ''); - expect(match).toBeUndefined(); - }); - - it('matches any case', async () => { - const indexPattern = simpleSavedObjectMock.create(savedObjectsClient, { - attributes: { title: 'foo' }, - } as SavedObject); - savedObjectsClient.find = jest.fn().mockImplementation(() => - Promise.resolve({ - savedObjects: [indexPattern], - }) - ); - const match = await findObjectByTitle(savedObjectsClient, 'index-pattern', 'FOO'); - expect(match).toEqual(indexPattern); - }); -}); diff --git a/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts b/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts deleted file mode 100644 index 2ec7cf89d7d34..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/find_object_by_title.ts +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - SavedObjectsClientContract, - SimpleSavedObject, - SavedObjectAttributes, -} from '@kbn/core/public'; - -/** - * Returns an object matching a given title - * - * @param savedObjectsClient {SavedObjectsClientContract} - * @param type {string} - * @param title {string} - * @returns {Promise} - */ -export async function findObjectByTitle( - savedObjectsClient: SavedObjectsClientContract, - type: string, - title: string -): Promise | void> { - if (!title) { - return; - } - - // Elastic search will return the most relevant results first, which means exact matches should come - // first, and so we shouldn't need to request everything. Using 10 just to be on the safe side. - const response = await savedObjectsClient.find({ - type, - perPage: 10, - search: `"${title}"`, - searchFields: ['title'], - fields: ['title'], - }); - return response.savedObjects.find( - (obj) => obj.get('title').toLowerCase() === title.toLowerCase() - ); -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts b/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts deleted file mode 100644 index 42de37dcb71d1..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/hydrate_index_pattern.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { DataViewsContract } from '@kbn/data-views-plugin/public'; -import { SavedObject, SavedObjectConfig } from '../../types'; - -/** - * After creation or fetching from ES, ensure that the searchSources index indexPattern - * is an bonafide IndexPattern object. - * - * @return {Promise} - */ -export async function hydrateIndexPattern( - id: string, - savedObject: SavedObject, - dataViews: DataViewsContract, - config: SavedObjectConfig -) { - const indexPattern = config.indexPattern; - - if (!savedObject.searchSource) { - return null; - } - - const index = id || indexPattern || savedObject.searchSource.getOwnField('index'); - - if (typeof index !== 'string' || !index) { - return null; - } - - const indexObj = await dataViews.get(index); - savedObject.searchSource.setField('index', indexObj); - return indexObj; -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts deleted file mode 100644 index a7428156176a5..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/initialize_saved_object.ts +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { cloneDeep, assign } from 'lodash'; -import { SavedObjectsClientContract } from '@kbn/core/public'; -import { SavedObject, SavedObjectConfig } from '../../types'; - -/** - * Initialize saved object - */ -export async function intializeSavedObject( - savedObject: SavedObject, - savedObjectsClient: SavedObjectsClientContract, - config: SavedObjectConfig -) { - const esType = config.type; - // ensure that the esType is defined - if (!esType) throw new Error('You must define a type name to use SavedObject objects.'); - - if (!savedObject.id) { - // just assign the defaults and be done - assign(savedObject, savedObject.defaults); - await savedObject.hydrateIndexPattern!(); - if (typeof config.afterESResp === 'function') { - savedObject = await config.afterESResp(savedObject); - } - return savedObject; - } - - const resp = await savedObjectsClient.get(esType, savedObject.id); - const respMapped = { - _id: resp.id, - _type: resp.type, - _source: cloneDeep(resp.attributes), - references: resp.references, - found: !!resp._version, - }; - await savedObject.applyESResp(respMapped); - if (typeof config.init === 'function') { - await config.init.call(savedObject); - } - - return savedObject; -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/save_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/save_saved_object.ts index 76ac6bc3c0b33..429a96eb19f46 100644 --- a/src/plugins/saved_objects/public/saved_object/helpers/save_saved_object.ts +++ b/src/plugins/saved_objects/public/saved_object/helpers/save_saved_object.ts @@ -7,118 +7,13 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - SavedObject, - SavedObjectConfig, - SavedObjectKibanaServices, - SavedObjectSaveOpts, - StartServices, -} from '../../types'; import { OVERWRITE_REJECTED, SAVE_DUPLICATE_REJECTED } from '../../constants'; -import { createSource } from './create_source'; -import { checkForDuplicateTitle } from './check_for_duplicate_title'; /** * @param error {Error} the error * @return {boolean} */ -export function isErrorNonFatal(error: { message: string }) { +export function isErrorNonFatal(error: { message: string }): boolean { if (!error) return false; return error.message === OVERWRITE_REJECTED || error.message === SAVE_DUPLICATE_REJECTED; } - -/** - * Saves this object. - * - * @param {string} [esType] - * @param {SavedObject} [savedObject] - * @param {SavedObjectConfig} [config] - * @param {object} [options={}] - * @property {boolean} [options.confirmOverwrite=false] - If true, attempts to create the source so it - * can confirm an overwrite if a document with the id already exists. - * @property {boolean} [options.isTitleDuplicateConfirmed=false] - If true, save allowed with duplicate title - * @property {func} [options.onTitleDuplicate] - function called if duplicate title exists. - * When not provided, confirm modal will be displayed asking user to confirm or cancel save. - * @param {SavedObjectKibanaServices} [services] - * @param {StartServices} [startServices] - * @return {Promise} - * @resolved {String} - The id of the doc - */ -export async function saveSavedObject( - savedObject: SavedObject, - config: SavedObjectConfig, - { - confirmOverwrite = false, - isTitleDuplicateConfirmed = false, - onTitleDuplicate, - }: SavedObjectSaveOpts = {}, - services: SavedObjectKibanaServices, - startServices: StartServices -): Promise { - const { savedObjectsClient, chrome } = services; - - const esType = config.type || ''; - const extractReferences = config.extractReferences; - // Save the original id in case the save fails. - const originalId = savedObject.id; - // Read https://github.com/elastic/kibana/issues/9056 and - // https://github.com/elastic/kibana/issues/9012 for some background into why this copyOnSave variable - // exists. - // The goal is to move towards a better rename flow, but since our users have been conditioned - // to expect a 'save as' flow during a rename, we are keeping the logic the same until a better - // UI/UX can be worked out. - if (savedObject.copyOnSave) { - delete savedObject.id; - } - - // Here we want to extract references and set them within "references" attribute - let { attributes, references } = savedObject._serialize(); - if (extractReferences) { - ({ attributes, references } = extractReferences({ attributes, references })); - } - if (!references) throw new Error('References not returned from extractReferences'); - - try { - await checkForDuplicateTitle( - savedObject, - isTitleDuplicateConfirmed, - onTitleDuplicate, - services, - startServices - ); - savedObject.isSaving = true; - const resp = confirmOverwrite - ? await createSource( - attributes, - savedObject, - esType, - savedObject.creationOpts({ references }), - services, - startServices - ) - : await savedObjectsClient.create( - esType, - attributes, - savedObject.creationOpts({ references, overwrite: true }) - ); - - savedObject.id = resp.id; - if (savedObject.showInRecentlyAccessed && savedObject.getFullPath) { - chrome.recentlyAccessed.add( - savedObject.getFullPath(), - savedObject.title, - String(savedObject.id) - ); - } - savedObject.isSaving = false; - savedObject.lastSavedTitle = savedObject.title; - return savedObject.id; - } catch (err) { - savedObject.isSaving = false; - savedObject.id = originalId; - if (isErrorNonFatal(err)) { - return ''; - } - return Promise.reject(err); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts b/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts deleted file mode 100644 index 00c8a01026ef4..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.test.ts +++ /dev/null @@ -1,118 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { SavedObjectAttributes, SavedObjectsCreateOptions, OverlayStart } from '@kbn/core/public'; -import { SavedObjectsClientContract } from '@kbn/core/public'; -import { analyticsServiceMock, i18nServiceMock, themeServiceMock } from '@kbn/core/public/mocks'; -import { saveWithConfirmation } from './save_with_confirmation'; -import * as deps from './confirm_modal_promise'; -import { OVERWRITE_REJECTED } from '../../constants'; - -describe('saveWithConfirmation', () => { - const savedObjectsClient: SavedObjectsClientContract = {} as SavedObjectsClientContract; - const overlays: OverlayStart = {} as OverlayStart; - const source: SavedObjectAttributes = {} as SavedObjectAttributes; - const options: SavedObjectsCreateOptions = {} as SavedObjectsCreateOptions; - const savedObject = { - getEsType: () => 'test type', - title: 'test title', - displayName: 'test display name', - }; - const startServices = { - analytics: analyticsServiceMock.createAnalyticsServiceStart(), - i18n: i18nServiceMock.createStartContract(), - theme: themeServiceMock.createStartContract(), - }; - - beforeEach(() => { - savedObjectsClient.create = jest.fn(); - jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.resolve({} as any)); - }); - - test('should call create of savedObjectsClient', async () => { - await saveWithConfirmation( - source, - savedObject, - options, - { savedObjectsClient, overlays }, - startServices - ); - expect(savedObjectsClient.create).toHaveBeenCalledWith( - savedObject.getEsType(), - source, - options - ); - }); - - test('should call confirmModalPromise when such record exists', async () => { - savedObjectsClient.create = jest - .fn() - .mockImplementation((type, src, opt) => - opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) - ); - - await saveWithConfirmation( - source, - savedObject, - options, - { savedObjectsClient, overlays }, - startServices - ); - expect(deps.confirmModalPromise).toHaveBeenCalledWith( - expect.any(String), - expect.any(String), - expect.any(String), - overlays, - expect.objectContaining({ - analytics: expect.any(Object), - i18n: expect.any(Object), - theme: expect.any(Object), - }) - ); - }); - - test('should call create of savedObjectsClient when overwriting confirmed', async () => { - savedObjectsClient.create = jest - .fn() - .mockImplementation((type, src, opt) => - opt && opt.overwrite ? Promise.resolve({} as any) : Promise.reject({ res: { status: 409 } }) - ); - - await saveWithConfirmation( - source, - savedObject, - options, - { savedObjectsClient, overlays }, - startServices - ); - expect(savedObjectsClient.create).toHaveBeenLastCalledWith(savedObject.getEsType(), source, { - overwrite: true, - ...options, - }); - }); - - test('should reject when overwriting denied', async () => { - savedObjectsClient.create = jest.fn().mockReturnValue(Promise.reject({ res: { status: 409 } })); - jest.spyOn(deps, 'confirmModalPromise').mockReturnValue(Promise.reject()); - - expect.assertions(1); - await expect( - saveWithConfirmation( - source, - savedObject, - options, - { - savedObjectsClient, - overlays, - }, - startServices - ) - ).rejects.toThrow(OVERWRITE_REJECTED); - }); -}); diff --git a/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts b/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts deleted file mode 100644 index d80e2179b5329..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/save_with_confirmation.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { get } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { - SavedObjectAttributes, - SavedObjectsCreateOptions, - OverlayStart, - SavedObjectsClientContract, -} from '@kbn/core/public'; -import { OVERWRITE_REJECTED } from '../../constants'; -import type { StartServices } from '../../types'; -import { confirmModalPromise } from './confirm_modal_promise'; - -/** - * Attempts to create the current object using the serialized source. If an object already - * exists, a warning message requests an overwrite confirmation. - * @param source - serialized version of this object what will be indexed into elasticsearch. - * @param savedObject - a simple object that contains properties title and displayName, and getEsType method - * @param options - options to pass to the saved object create method - * @param services - provides Kibana services savedObjectsClient and overlays - * @returns {Promise} - A promise that is resolved with the objects id if the object is - * successfully indexed. If the overwrite confirmation was rejected, an error is thrown with - * a confirmRejected = true parameter so that case can be handled differently than - * a create or index error. - * @resolved {SavedObject} - */ -export async function saveWithConfirmation( - source: SavedObjectAttributes, - savedObject: { - getEsType(): string; - title: string; - displayName: string; - }, - options: SavedObjectsCreateOptions, - services: { savedObjectsClient: SavedObjectsClientContract; overlays: OverlayStart }, - startServices: StartServices -) { - const { savedObjectsClient, overlays } = services; - try { - return await savedObjectsClient.create(savedObject.getEsType(), source, options); - } catch (err) { - // record exists, confirm overwriting - if (get(err, 'res.status') === 409) { - const confirmMessage = i18n.translate( - 'savedObjects.confirmModal.overwriteConfirmationMessage', - { - defaultMessage: 'Are you sure you want to overwrite {title}?', - values: { title: savedObject.title }, - } - ); - - const title = i18n.translate('savedObjects.confirmModal.overwriteTitle', { - defaultMessage: 'Overwrite {name}?', - values: { name: savedObject.displayName }, - }); - const confirmButtonText = i18n.translate('savedObjects.confirmModal.overwriteButtonLabel', { - defaultMessage: 'Overwrite', - }); - - return confirmModalPromise(confirmMessage, title, confirmButtonText, overlays, startServices) - .then(() => - savedObjectsClient.create(savedObject.getEsType(), source, { - overwrite: true, - ...options, - }) - ) - .catch(() => Promise.reject(new Error(OVERWRITE_REJECTED))); - } - return await Promise.reject(err); - } -} diff --git a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts b/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts deleted file mode 100644 index 3150f6ad3f005..0000000000000 --- a/src/plugins/saved_objects/public/saved_object/helpers/serialize_saved_object.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { forOwn } from 'lodash'; -import { extractSearchSourceReferences } from '@kbn/data-plugin/public'; -import { SavedObject, SavedObjectConfig } from '../../types'; -import { expandShorthand } from './field_mapping'; - -export function serializeSavedObject(savedObject: SavedObject, config: SavedObjectConfig) { - // mapping definition for the fields that this object will expose - const mapping = expandShorthand(config.mapping ?? {}); - const attributes = {} as Record; - const references = []; - - forOwn(mapping, (fieldMapping, fieldName) => { - if (typeof fieldName !== 'string') { - return; - } - // @ts-ignore - const savedObjectFieldVal = savedObject[fieldName]; - if (savedObjectFieldVal != null) { - attributes[fieldName] = fieldMapping._serialize - ? fieldMapping._serialize(savedObjectFieldVal) - : savedObjectFieldVal; - } - }); - - if (savedObject.searchSource) { - const { searchSourceJSON, references: searchSourceReferences } = - savedObject.searchSource.serialize(); - attributes.kibanaSavedObjectMeta = { searchSourceJSON }; - references.push(...searchSourceReferences); - } - - if (savedObject.searchSourceFields) { - const [searchSourceFields, searchSourceReferences] = extractSearchSourceReferences( - savedObject.searchSourceFields - ); - const searchSourceJSON = JSON.stringify(searchSourceFields); - attributes.kibanaSavedObjectMeta = { searchSourceJSON }; - references.push(...searchSourceReferences); - } - - if (savedObject.unresolvedIndexPatternReference) { - references.push(savedObject.unresolvedIndexPatternReference); - } - - return { attributes, references }; -} diff --git a/src/plugins/saved_objects/public/saved_object/index.ts b/src/plugins/saved_objects/public/saved_object/index.ts index 560178fdf4f36..68639d030317e 100644 --- a/src/plugins/saved_objects/public/saved_object/index.ts +++ b/src/plugins/saved_objects/public/saved_object/index.ts @@ -7,6 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { checkForDuplicateTitle } from './helpers/check_for_duplicate_title'; -export { saveWithConfirmation } from './helpers/save_with_confirmation'; export { isErrorNonFatal } from './helpers/save_saved_object'; diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index c34ef878b5d7e..d921b64d106f9 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -8,15 +8,11 @@ */ import { - ChromeStart, CoreStart, - OverlayStart, - SavedObjectsClientContract, SavedObjectAttributes, SavedObjectReference, } from '@kbn/core/public'; -import { ISearchSource, ISearchStart, SerializedSearchSourceFields } from '@kbn/data-plugin/public'; -import { DataViewsContract } from '@kbn/data-views-plugin/public'; +import { ISearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; /** @@ -62,14 +58,6 @@ export interface SavedObjectCreationOpts { overwrite?: boolean; } -export interface SavedObjectKibanaServices { - savedObjectsClient: SavedObjectsClientContract; - dataViews: DataViewsContract; - search: ISearchStart; - chrome: ChromeStart; - overlays: OverlayStart; -} - export type StartServices = Pick; export interface SavedObjectAttributesAndRefs { From e2393e5304938bb7c055e228f841776a8aa78c3a Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 18 Nov 2024 14:38:24 +0000 Subject: [PATCH 02/50] [CI] Auto-commit changed files from 'node scripts/notice' --- src/plugins/saved_objects/tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/saved_objects/tsconfig.json b/src/plugins/saved_objects/tsconfig.json index ccec7b43ad9f1..83e113a7e4e17 100644 --- a/src/plugins/saved_objects/tsconfig.json +++ b/src/plugins/saved_objects/tsconfig.json @@ -7,7 +7,6 @@ "kbn_references": [ "@kbn/core", "@kbn/data-plugin", - "@kbn/kibana-utils-plugin", "@kbn/i18n", "@kbn/data-views-plugin", "@kbn/i18n-react", From 0584227c0b58a38162155c3f05abc039eb852967 Mon Sep 17 00:00:00 2001 From: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Date: Mon, 18 Nov 2024 15:29:27 +0000 Subject: [PATCH 03/50] [CI] Auto-commit changed files from 'node scripts/eslint --no-cache --fix' --- src/plugins/saved_objects/public/types.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/plugins/saved_objects/public/types.ts b/src/plugins/saved_objects/public/types.ts index d921b64d106f9..0919c24ab2c62 100644 --- a/src/plugins/saved_objects/public/types.ts +++ b/src/plugins/saved_objects/public/types.ts @@ -7,11 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { - CoreStart, - SavedObjectAttributes, - SavedObjectReference, -} from '@kbn/core/public'; +import { CoreStart, SavedObjectAttributes, SavedObjectReference } from '@kbn/core/public'; import { ISearchSource, SerializedSearchSourceFields } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/common'; From 6cd339947ea2fe3620971a3c176a8d0f3c42f3f2 Mon Sep 17 00:00:00 2001 From: Jesus Wahrman Date: Mon, 18 Nov 2024 19:00:15 +0100 Subject: [PATCH 04/50] fix translation files with script --- .../plugins/translations/translations/fr-FR.json | 16 +++++----------- .../plugins/translations/translations/ja-JP.json | 16 +++++----------- .../plugins/translations/translations/zh-CN.json | 12 +++++------- 3 files changed, 15 insertions(+), 29 deletions(-) diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index c9d88a7c0f8ed..a0c5e0e47f608 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6689,12 +6689,6 @@ "reporting.share.screenCapturePanelContent.optimizeForPrintingLabel": "Optimiser pour l'impression", "reporting.shareContextMenu.ExportsButtonLabel": "PDF", "reporting.shareContextMenu.ExportsButtonLabelPNG": "Export PNG", - "savedObjects.confirmModal.cancelButtonLabel": "Annuler", - "savedObjects.confirmModal.overwriteButtonLabel": "Écraser", - "savedObjects.confirmModal.overwriteConfirmationMessage": "Êtes-vous sûr de vouloir écraser {title} ?", - "savedObjects.confirmModal.overwriteTitle": "Écraser {name} ?", - "savedObjects.confirmModal.saveDuplicateButtonLabel": "Enregistrer {name}", - "savedObjects.confirmModal.saveDuplicateConfirmationMessage": "Il y a déjà une occurrence de {name} avec le titre \"{title}\". Voulez-vous tout de même enregistrer ?", "savedObjects.overwriteRejectedDescription": "La confirmation d'écrasement a été rejetée.", "savedObjects.saveDuplicateRejectedDescription": "La confirmation d'enregistrement avec un doublon de titre a été rejetée.", "savedObjects.saveModal.cancelButtonLabel": "Annuler", @@ -11712,8 +11706,8 @@ "xpack.apm.serviceIcons.service": "Service", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "Architecture", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, =0 {Zone de disponibilité} one {Zone de disponibilité} other {Zones de disponibilité}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}}", "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, =0 {Type de déclencheur} one {Type de déclencheur} other {Types de déclencheurs}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, =0 {Nom de fonction} one {Nom de fonction} other {Noms de fonction}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, =0{Type de machine} one {Type de machine} other {Types de machines}}", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "ID de projet", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "Fournisseur cloud", @@ -28301,8 +28295,8 @@ "xpack.maps.source.esSearch.descendingLabel": "décroissant", "xpack.maps.source.esSearch.extentFilterLabel": "Filtre dynamique pour les données de la zone de carte visible", "xpack.maps.source.esSearch.fieldNotFoundMsg": "Impossible de trouver \"{fieldName}\" dans le modèle d'indexation \"{indexPatternName}\".", - "xpack.maps.source.esSearch.geoFieldLabel": "Champ géospatial", "xpack.maps.source.esSearch.geofieldLabel": "Champ géospatial", + "xpack.maps.source.esSearch.geoFieldLabel": "Champ géospatial", "xpack.maps.source.esSearch.geoFieldTypeLabel": "Type de champ géospatial", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "Votre vue de données pointe vers plusieurs index. Un seul index est autorisé par vue de données.", "xpack.maps.source.esSearch.indexZeroLengthEditError": "Votre vue de données ne pointe vers aucun index.", @@ -38078,8 +38072,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibana ne permet qu'un maximum de {maxNumber} {maxNumber, plural, =1 {alerte} other {alertes}} par exécution de règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "Nom obligatoire.", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "Ajouter un guide d'investigation sur les règles...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "Ajouter le guide de configuration de règle...", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "Fournissez des instructions sur les conditions préalables à la règle, telles que les intégrations requises, les étapes de configuration et tout ce qui est nécessaire au bon fonctionnement de la règle.", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "Guide de configuration", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "Une balise ne doit pas être vide", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "Le remplacement du préfixe d'indicateur ne peut pas être vide.", @@ -43870,8 +43864,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "Sélectionner un SLO", "xpack.slo.sloEmbeddable.displayName": "Aperçu du SLO", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "Le SLO a été supprimé. Vous pouvez supprimer sans risque le widget du tableau de bord.", - "xpack.slo.sLOGridItem.targetFlexItemLabel": "Cible {target}", "xpack.slo.sloGridItem.targetFlexItemLabel": "Cible {target}", + "xpack.slo.sLOGridItem.targetFlexItemLabel": "Cible {target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "Personnaliser le filtre", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "Facultatif", "xpack.slo.sloGroupConfiguration.customFilterText": "Personnaliser le filtre", @@ -45401,8 +45395,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - Données de gestion des cas", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "Éditeur de code", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "Corps", - "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", "xpack.stackConnectors.components.d3security.connectorTypeTitle": "Données D3", + "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "Type d'événement", "xpack.stackConnectors.components.d3security.invalidActionText": "Nom d'action non valide.", "xpack.stackConnectors.components.d3security.requiredActionText": "L'action est requise.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fffed2d59a462..86704cb692733 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6682,12 +6682,6 @@ "reporting.share.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化", "reporting.shareContextMenu.ExportsButtonLabel": "PDF", "reporting.shareContextMenu.ExportsButtonLabelPNG": "PNGエクスポート", - "savedObjects.confirmModal.cancelButtonLabel": "キャンセル", - "savedObjects.confirmModal.overwriteButtonLabel": "上書き", - "savedObjects.confirmModal.overwriteConfirmationMessage": "{title}を上書きしてよろしいですか?", - "savedObjects.confirmModal.overwriteTitle": "{name} を上書きしますか?", - "savedObjects.confirmModal.saveDuplicateButtonLabel": "{name} を保存", - "savedObjects.confirmModal.saveDuplicateConfirmationMessage": "''{title}''というタイトルの {name} がすでに存在します。保存しますか?", "savedObjects.overwriteRejectedDescription": "上書き確認が拒否されました", "savedObjects.saveDuplicateRejectedDescription": "重複ファイルの保存確認が拒否されました", "savedObjects.saveModal.cancelButtonLabel": "キャンセル", @@ -11695,8 +11689,8 @@ "xpack.apm.serviceIcons.service": "サービス", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "アーキテクチャー", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性ゾーン}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {関数名}}", "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {トリガータイプ}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {関数名}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {コンピュータータイプ} }\n", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "プロジェクト ID", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "クラウドプロバイダー", @@ -28273,8 +28267,8 @@ "xpack.maps.source.esSearch.descendingLabel": "降順", "xpack.maps.source.esSearch.extentFilterLabel": "マップの表示範囲でデータを動的にフィルタリング", "xpack.maps.source.esSearch.fieldNotFoundMsg": "インデックスパターン''{indexPatternName}''に''{fieldName}''が見つかりません。", - "xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド", "xpack.maps.source.esSearch.geofieldLabel": "地理空間フィールド", + "xpack.maps.source.esSearch.geoFieldLabel": "地理空間フィールド", "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空間フィールドタイプ", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "データビューは複数のインデックスを参照しています。データビューごとに1つのインデックスのみが許可されています。", "xpack.maps.source.esSearch.indexZeroLengthEditError": "データビューはどのインデックスも参照していません。", @@ -38045,8 +38039,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "Kibanaで許可される最大数は、1回の実行につき、{maxNumber} {maxNumber, plural, other {アラート}}です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名前が必要です。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "ルール調査ガイドを追加...", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "ルールセットアップガイドを追加...", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "必要な統合、構成ステップ、ルールが正常に動作するために必要な他のすべての項目といった、ルール前提条件に関する指示を入力します。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "セットアップガイド", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "タグを空にすることはできません", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "インジケータープレフィックスの無効化を空にすることはできません", @@ -43834,8 +43828,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "SLOを選択", "xpack.slo.sloEmbeddable.displayName": "SLO概要", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLOが削除されました。ウィジェットをダッシュボードから安全に削除できます。", - "xpack.slo.sLOGridItem.targetFlexItemLabel": "目標{target}", "xpack.slo.sloGridItem.targetFlexItemLabel": "目標{target}", + "xpack.slo.sLOGridItem.targetFlexItemLabel": "目標{target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "カスタムフィルター", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "オプション", "xpack.slo.sloGroupConfiguration.customFilterText": "カスタムフィルター", @@ -45360,8 +45354,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webフック - ケース管理データ", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "コードエディター", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "本文", - "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3セキュリティ", "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3データ", + "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3セキュリティ", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "イベントタイプ", "xpack.stackConnectors.components.d3security.invalidActionText": "無効なアクション名です。", "xpack.stackConnectors.components.d3security.requiredActionText": "アクションが必要です。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d8de21af735a..10d49d7024598 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6601,8 +6601,6 @@ "reporting.share.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化", "reporting.shareContextMenu.ExportsButtonLabel": "PDF", "reporting.shareContextMenu.ExportsButtonLabelPNG": "PNG 导出", - "savedObjects.confirmModal.cancelButtonLabel": "取消", - "savedObjects.confirmModal.overwriteButtonLabel": "覆盖", "savedObjects.overwriteRejectedDescription": "已拒绝覆盖确认", "savedObjects.saveDuplicateRejectedDescription": "已拒绝使用重复标题保存确认", "savedObjects.saveModal.cancelButtonLabel": "取消", @@ -11461,8 +11459,8 @@ "xpack.apm.serviceIcons.service": "服务", "xpack.apm.serviceIcons.serviceDetails.cloud.architecture": "架构", "xpack.apm.serviceIcons.serviceDetails.cloud.availabilityZoneLabel": "{zones, plural, other {可用性区域}}", - "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {功能名称}}", "xpack.apm.serviceIcons.serviceDetails.cloud.faasTriggerTypeLabel": "{triggerTypes, plural, other {触发类型}}", + "xpack.apm.serviceIcons.serviceDetails.cloud.functionNameLabel": "{functionNames, plural, other {功能名称}}", "xpack.apm.serviceIcons.serviceDetails.cloud.machineTypesLabel": "{machineTypes, plural, other {机器类型}}", "xpack.apm.serviceIcons.serviceDetails.cloud.projectIdLabel": "项目 ID", "xpack.apm.serviceIcons.serviceDetails.cloud.providerLabel": "云服务提供商", @@ -27778,8 +27776,8 @@ "xpack.maps.source.esSearch.convertToGeoJsonErrorMsg": "无法将搜索响应转换成 geoJson 功能集合,错误:{errorMsg}", "xpack.maps.source.esSearch.descendingLabel": "降序", "xpack.maps.source.esSearch.extentFilterLabel": "在可见地图区域中动态筛留数据", - "xpack.maps.source.esSearch.geoFieldLabel": "地理空间字段", "xpack.maps.source.esSearch.geofieldLabel": "地理空间字段", + "xpack.maps.source.esSearch.geoFieldLabel": "地理空间字段", "xpack.maps.source.esSearch.geoFieldTypeLabel": "地理空间字段类型", "xpack.maps.source.esSearch.indexOverOneLengthEditError": "您的数据视图指向多个索引。每个数据视图只允许一个索引。", "xpack.maps.source.esSearch.indexZeroLengthEditError": "您的数据视图未指向任何索引。", @@ -37436,8 +37434,8 @@ "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.maxAlertsFieldLessThanWarning": "每次规则运行时,Kibana 最多只允许 {maxNumber} 个{maxNumber, plural, other {告警}}。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.nameFieldRequiredError": "名称必填。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.noteHelpText": "添加规则调查指南......", - "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。", "xpack.securitySolution.detectionEngine.createRule.stepAboutrule.setupHelpText": "添加规则设置指南......", + "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupHelpText": "提供有关规则先决条件的说明,如所需集成、配置步骤,以及规则正常运行所需的任何其他内容。", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.setupLabel": "设置指南", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.tagFieldEmptyError": "标签不得为空", "xpack.securitySolution.detectionEngine.createRule.stepAboutRule.threatIndicatorPathFieldEmptyError": "指标前缀覆盖不得为空", @@ -43177,8 +43175,8 @@ "xpack.slo.sloEmbeddable.config.sloSelector.placeholder": "选择 SLO", "xpack.slo.sloEmbeddable.displayName": "SLO 概览", "xpack.slo.sloEmbeddable.overview.sloNotFoundText": "SLO 已删除。您可以放心从仪表板中删除小组件。", - "xpack.slo.sLOGridItem.targetFlexItemLabel": "目标 {target}", "xpack.slo.sloGridItem.targetFlexItemLabel": "目标 {target}", + "xpack.slo.sLOGridItem.targetFlexItemLabel": "目标 {target}", "xpack.slo.sloGroupConfiguration.customFiltersLabel": "定制筛选", "xpack.slo.sloGroupConfiguration.customFiltersOptional": "可选", "xpack.slo.sloGroupConfiguration.customFilterText": "定制筛选", @@ -44655,8 +44653,8 @@ "xpack.stackConnectors.components.casesWebhookxpack.stackConnectors.components.casesWebhook.connectorTypeTitle": "Webhook - 案例管理数据", "xpack.stackConnectors.components.d3security.bodyCodeEditorAriaLabel": "代码编辑器", "xpack.stackConnectors.components.d3security.bodyFieldLabel": "正文", - "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", "xpack.stackConnectors.components.d3security.connectorTypeTitle": "D3 数据", + "xpack.stackConnectors.components.d3Security.connectorTypeTitle": "D3 Security", "xpack.stackConnectors.components.d3security.eventTypeFieldLabel": "事件类型", "xpack.stackConnectors.components.d3security.invalidActionText": "操作名称无效。", "xpack.stackConnectors.components.d3security.requiredActionText": "'操作'必填。", From e7c019a7bf7d0e681a2cd5d4c82d8d1b16229702 Mon Sep 17 00:00:00 2001 From: Dzmitry Lemechko Date: Mon, 18 Nov 2024 12:12:23 +0100 Subject: [PATCH 05/50] [ftr] update docs with correct auth arguments (#200532) ## Summary Update FTR docs both in `x-pack/test_serverless` and `x-pack/test/api_integration/deployment_agnostic/` paths to show relevant example for authentication with Cookie header. --- .../deployment_agnostic/README.md | 31 +++++++++++++------ x-pack/test_serverless/README.md | 4 +-- 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/x-pack/test/api_integration/deployment_agnostic/README.md b/x-pack/test/api_integration/deployment_agnostic/README.md index b029d0d167002..3bc1c70dda1ab 100644 --- a/x-pack/test/api_integration/deployment_agnostic/README.md +++ b/x-pack/test/api_integration/deployment_agnostic/README.md @@ -108,7 +108,7 @@ Kibana provides both public and internal APIs, each requiring authentication wit Recommendations: - use `roleScopedSupertest` service to create supertest instance scoped to specific role and pre-defined request headers - `roleScopedSupertest.getSupertestWithRoleScope()` authenticate requests with API key by default -- pass `withCookieHeader: true` to use Cookie header for requests authentication +- pass `useCookieHeader: true` to use Cookie header for requests authentication - don't forget to invalidate API key using `destroy()` on supertest scoped instance in `after` hook Add test files to `x-pack/test//deployment_agnostic/apis/`: @@ -117,25 +117,36 @@ test example ```ts export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); - let supertestWithAdminScope: SupertestWithRoleScopeType; + let supertestViewerWithApiKey: SupertestWithRoleScopeType; + let supertestEditorWithCookieCredentials: SupertestWithRoleScopeType; - describe('compression', () => { + describe('test suite', () => { before(async () => { - supertestWithAdminScope = await roleScopedSupertest.getSupertestWithRoleScope('admin', { + supertestViewerWithApiKey = await roleScopedSupertest.getSupertestWithRoleScope('viewer', { withInternalHeaders: true, withCustomHeaders: { 'accept-encoding': 'gzip' }, }); + supertestEditorWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope('editor', { + withInternalHeaders: true, + useCookieHeader: true, + }); }); after(async () => { // always invalidate API key for the scoped role in the end - await supertestWithAdminScope.destroy(); + await supertestViewerWithApiKey.destroy(); + // supertestEditorWithCookieCredentials.destroy() has no effect because Cookie session is cached per SAML role + // and valid for the whole FTR config run, no need to call it }); - describe('against an application page', () => { - it(`uses compression when there isn't a referer`, async () => { - const response = await supertestWithAdminScope.get('/app/kibana'); - expect(response.header).to.have.property('content-encoding', 'gzip'); - }); + it(`uses compression when there isn't a referer`, async () => { + const response = await supertestViewerWithApiKey.get('/app/kibana'); + expect(response.header).to.have.property('content-encoding', 'gzip'); }); + + it(`can run rule with Editor privileges`, async () => { + const response = await supertestEditorWithCookieCredentials + .post(`/internal/alerting/rule/${ruleId}/_run_soon`) + .expect(204); + }); }); } ``` diff --git a/x-pack/test_serverless/README.md b/x-pack/test_serverless/README.md index 44f871273aca2..17e5a9056f5d8 100644 --- a/x-pack/test_serverless/README.md +++ b/x-pack/test_serverless/README.md @@ -154,7 +154,7 @@ Kibana provides both public and internal APIs, each requiring authentication wit Recommendations: - use `roleScopedSupertest` service to create a supertest instance scoped to a specific role and predefined request headers - `roleScopedSupertest.getSupertestWithRoleScope()` authenticates requests with an API key by default -- pass `withCookieHeader: true` to use Cookie header for request authentication +- pass `useCookieHeader: true` to use Cookie header for request authentication - don't forget to invalidate API keys by using `destroy()` on the supertest scoped instance in the `after` hook ``` @@ -183,7 +183,7 @@ describe("my internal APIs test suite", async function() { before(async () => { supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope('admin', { - withCookieHeader: true, // to avoid generating API key and use Cookie header instead + useCookieHeader: true, // to avoid generating API key and use Cookie header instead withInternalHeaders: true, }); }); From 49d0aaf29bc5301369d930c098f3d05c451bf817 Mon Sep 17 00:00:00 2001 From: Sergi Romeu Date: Mon, 18 Nov 2024 12:32:26 +0100 Subject: [PATCH 06/50] [APM] Migrate `/service_maps` to deployment agnostic test (#199984) ## Summary Closes https://github.com/elastic/kibana/issues/198983 Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `service_maps` test folder to Deployment-agnostic testing strategy. ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` ## Checks - [ ] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/observability/apm/index.ts | 1 + .../observability/apm/service_maps/index.ts | 15 ++ .../apm/service_maps/service_maps.spec.ts | 156 ++++++++++++++++++ .../service_maps_kuery_filter.spec.ts | 137 +++++++++++++++ .../tests/service_maps/service_maps.spec.ts | 78 --------- .../service_maps_kuery_filter.spec.ts | 135 --------------- .../service_maps/service_maps.ts | 84 ---------- .../test_suites/observability/index.ts | 1 - 8 files changed, 309 insertions(+), 298 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps.spec.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps_kuery_filter.spec.ts delete mode 100644 x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts delete mode 100644 x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/service_maps/service_maps.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index b46b2c3f10637..a18edd23c509e 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -29,6 +29,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./observability_overview')); loadTestFile(require.resolve('./latency')); loadTestFile(require.resolve('./infrastructure')); + loadTestFile(require.resolve('./service_maps')); loadTestFile(require.resolve('./inspect')); loadTestFile(require.resolve('./service_groups')); loadTestFile(require.resolve('./diagnostics')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/index.ts new file mode 100644 index 0000000000000..97681cae7def9 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('service_maps', () => { + loadTestFile(require.resolve('./service_maps.spec.ts')); + loadTestFile(require.resolve('./service_maps_kuery_filter.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps.spec.ts new file mode 100644 index 0000000000000..809c10b2f01e8 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps.spec.ts @@ -0,0 +1,156 @@ +/* + * 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 { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import expect from 'expect'; +import { serviceMap, timerange } from '@kbn/apm-synthtrace-client'; +import { Readable } from 'node:stream'; +import type { SupertestReturnType } from '../../../../../../apm_api_integration/common/apm_api_supertest'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +type DependencyResponse = SupertestReturnType<'GET /internal/apm/service-map/dependency'>; +type ServiceNodeResponse = + SupertestReturnType<'GET /internal/apm/service-map/service/{serviceName}'>; + +export default function ({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2024-06-01T00:00:00.000Z').getTime(); + const end = new Date('2024-06-01T00:01:00.000Z').getTime(); + + describe('APM Service maps', () => { + describe('without data', () => { + it('returns an empty list', async () => { + const response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map`, + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.elements.length).toBe(0); + }); + + describe('/internal/apm/service-map/service/{serviceName} without data', () => { + let response: ServiceNodeResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/service/{serviceName}`, + params: { + path: { serviceName: 'opbeans-node' }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + }); + + it('retuns status code 200', () => { + expect(response.status).toBe(200); + }); + + it('returns an object with nulls', async () => { + [ + response.body.currentPeriod?.failedTransactionsRate?.value, + response.body.currentPeriod?.memoryUsage?.value, + response.body.currentPeriod?.cpuUsage?.value, + response.body.currentPeriod?.transactionStats?.latency?.value, + response.body.currentPeriod?.transactionStats?.throughput?.value, + ].forEach((value) => { + expect(value).toEqual(null); + }); + }); + }); + + describe('/internal/apm/service-map/dependency', () => { + let response: DependencyResponse; + before(async () => { + response = await apmApiClient.readUser({ + endpoint: `GET /internal/apm/service-map/dependency`, + params: { + query: { + dependencyName: 'postgres', + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + }); + + it('retuns status code 200', () => { + expect(response.status).toBe(200); + }); + + it('returns undefined values', () => { + expect(response.body.currentPeriod).toEqual({ transactionStats: {} }); + }); + }); + }); + + describe('with synthtrace data', () => { + let synthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + synthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const events = timerange(start, end) + .interval('10s') + .rate(3) + .generator( + serviceMap({ + services: [ + { 'frontend-rum': 'rum-js' }, + { 'frontend-node': 'nodejs' }, + { advertService: 'java' }, + ], + definePaths([rum, node, adv]) { + return [ + [ + [rum, 'fetchAd'], + [node, 'GET /nodejs/adTag'], + [adv, 'APIRestController#getAd'], + ['elasticsearch', 'GET ad-*/_search'], + ], + ]; + }, + }) + ); + + return synthtraceEsClient.index(Readable.from(Array.from(events))); + }); + + after(async () => { + await synthtraceEsClient.clean(); + }); + + it('returns service map elements', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/service-map', + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + }, + }, + }); + + expect(response.status).toBe(200); + expect(response.body.elements.length).toBeGreaterThan(0); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps_kuery_filter.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps_kuery_filter.spec.ts new file mode 100644 index 0000000000000..9a14b3690a81b --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/service_maps/service_maps_kuery_filter.spec.ts @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { timerange, serviceMap } from '@kbn/apm-synthtrace-client'; +import { + APIClientRequestParamsOf, + APIReturnType, +} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const start = new Date('2023-01-01T00:00:00.000Z').getTime(); + const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; + + async function callApi( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/service-map'>['params'] + > + ) { + return await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/service-map', + params: { + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + } + + describe('service map kuery filter', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + + const events = timerange(start, end) + .interval('15m') + .rate(1) + .generator( + serviceMap({ + services: [ + { 'synthbeans-go': 'go' }, + { 'synthbeans-java': 'java' }, + { 'synthbeans-node': 'nodejs' }, + ], + definePaths([go, java, node]) { + return [ + [go, java], + [java, go, 'redis'], + [node, 'redis'], + { + path: [node, java, go, 'elasticsearch'], + transaction: (t) => t.defaults({ 'labels.name': 'node-java-go-es' }), + }, + [go, node, java], + ]; + }, + }) + ); + await apmSynthtraceEsClient.index(events); + }); + + after(() => apmSynthtraceEsClient.clean()); + + it('returns full service map when no kuery is defined', async () => { + const { status, body } = await callApi(); + + expect(status).to.be(200); + + const { nodes, edges } = partitionElements(body.elements); + + expect(getIds(nodes)).to.eql([ + '>elasticsearch', + '>redis', + 'synthbeans-go', + 'synthbeans-java', + 'synthbeans-node', + ]); + expect(getIds(edges)).to.eql([ + 'synthbeans-go~>elasticsearch', + 'synthbeans-go~>redis', + 'synthbeans-go~synthbeans-java', + 'synthbeans-go~synthbeans-node', + 'synthbeans-java~synthbeans-go', + 'synthbeans-node~>redis', + 'synthbeans-node~synthbeans-java', + ]); + }); + + it('returns only service nodes and connections filtered by given kuery', async () => { + const { status, body } = await callApi({ + query: { kuery: `labels.name: "node-java-go-es"` }, + }); + + expect(status).to.be(200); + + const { nodes, edges } = partitionElements(body.elements); + + expect(getIds(nodes)).to.eql([ + '>elasticsearch', + 'synthbeans-go', + 'synthbeans-java', + 'synthbeans-node', + ]); + expect(getIds(edges)).to.eql([ + 'synthbeans-go~>elasticsearch', + 'synthbeans-java~synthbeans-go', + 'synthbeans-node~synthbeans-java', + ]); + }); + }); +} + +type ConnectionElements = APIReturnType<'GET /internal/apm/service-map'>['elements']; + +function partitionElements(elements: ConnectionElements) { + const edges = elements.filter(({ data }) => 'source' in data && 'target' in data); + const nodes = elements.filter((element) => !edges.includes(element)); + return { edges, nodes }; +} + +function getIds(elements: ConnectionElements) { + return elements.map(({ data }) => data.id).sort(); +} diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts index 83965595020da..ae7a08b0664c1 100644 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts +++ b/x-pack/test/apm_api_integration/tests/service_maps/service_maps.spec.ts @@ -52,84 +52,6 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); }); - registry.when('Service Map without data', { config: 'trial', archives: [] }, () => { - describe('/internal/apm/service-map without data', () => { - it('returns an empty list', async () => { - const response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/service-map`, - params: { - query: { - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - - expect(response.status).to.be(200); - expect(response.body.elements.length).to.be(0); - }); - }); - - describe('/internal/apm/service-map/service/{serviceName} without data', () => { - let response: ServiceNodeResponse; - before(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/service-map/service/{serviceName}`, - params: { - path: { serviceName: 'opbeans-node' }, - query: { - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - }); - - it('retuns status code 200', () => { - expect(response.status).to.be(200); - }); - - it('returns an object with nulls', async () => { - [ - response.body.currentPeriod?.failedTransactionsRate?.value, - response.body.currentPeriod?.memoryUsage?.value, - response.body.currentPeriod?.cpuUsage?.value, - response.body.currentPeriod?.transactionStats?.latency?.value, - response.body.currentPeriod?.transactionStats?.throughput?.value, - ].forEach((value) => { - expect(value).to.be.eql(null); - }); - }); - }); - - describe('/internal/apm/service-map/dependency', () => { - let response: DependencyResponse; - before(async () => { - response = await apmApiClient.readUser({ - endpoint: `GET /internal/apm/service-map/dependency`, - params: { - query: { - dependencyName: 'postgres', - start: metadata.start, - end: metadata.end, - environment: 'ENVIRONMENT_ALL', - }, - }, - }); - }); - - it('retuns status code 200', () => { - expect(response.status).to.be(200); - }); - - it('returns undefined values', () => { - expect(response.body.currentPeriod).to.eql({ transactionStats: {} }); - }); - }); - }); - registry.when('Service Map with data', { config: 'trial', archives: ['apm_8.0.0'] }, () => { describe('/internal/apm/service-map with data', () => { let response: ServiceMapResponse; diff --git a/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts b/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts deleted file mode 100644 index b87e0de70495e..0000000000000 --- a/x-pack/test/apm_api_integration/tests/service_maps/service_maps_kuery_filter.spec.ts +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { timerange, serviceMap } from '@kbn/apm-synthtrace-client'; -import { - APIClientRequestParamsOf, - APIReturnType, -} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); - - const start = new Date('2023-01-01T00:00:00.000Z').getTime(); - const end = new Date('2023-01-01T00:15:00.000Z').getTime() - 1; - - async function callApi( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/service-map'>['params'] - > - ) { - return await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/service-map', - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - } - - registry.when('Service Map', { config: 'trial', archives: [] }, () => { - describe('optional kuery param', () => { - before(async () => { - const events = timerange(start, end) - .interval('15m') - .rate(1) - .generator( - serviceMap({ - services: [ - { 'synthbeans-go': 'go' }, - { 'synthbeans-java': 'java' }, - { 'synthbeans-node': 'nodejs' }, - ], - definePaths([go, java, node]) { - return [ - [go, java], - [java, go, 'redis'], - [node, 'redis'], - { - path: [node, java, go, 'elasticsearch'], - transaction: (t) => t.defaults({ 'labels.name': 'node-java-go-es' }), - }, - [go, node, java], - ]; - }, - }) - ); - await apmSynthtraceEsClient.index(events); - }); - - after(() => apmSynthtraceEsClient.clean()); - - it('returns full service map when no kuery is defined', async () => { - const { status, body } = await callApi(); - - expect(status).to.be(200); - - const { nodes, edges } = partitionElements(body.elements); - - expect(getIds(nodes)).to.eql([ - '>elasticsearch', - '>redis', - 'synthbeans-go', - 'synthbeans-java', - 'synthbeans-node', - ]); - expect(getIds(edges)).to.eql([ - 'synthbeans-go~>elasticsearch', - 'synthbeans-go~>redis', - 'synthbeans-go~synthbeans-java', - 'synthbeans-go~synthbeans-node', - 'synthbeans-java~synthbeans-go', - 'synthbeans-node~>redis', - 'synthbeans-node~synthbeans-java', - ]); - }); - - it('returns only service nodes and connections filtered by given kuery', async () => { - const { status, body } = await callApi({ - query: { kuery: `labels.name: "node-java-go-es"` }, - }); - - expect(status).to.be(200); - - const { nodes, edges } = partitionElements(body.elements); - - expect(getIds(nodes)).to.eql([ - '>elasticsearch', - 'synthbeans-go', - 'synthbeans-java', - 'synthbeans-node', - ]); - expect(getIds(edges)).to.eql([ - 'synthbeans-go~>elasticsearch', - 'synthbeans-java~synthbeans-go', - 'synthbeans-node~synthbeans-java', - ]); - }); - }); - }); -} - -type ConnectionElements = APIReturnType<'GET /internal/apm/service-map'>['elements']; - -function partitionElements(elements: ConnectionElements) { - const edges = elements.filter(({ data }) => 'source' in data && 'target' in data); - const nodes = elements.filter((element) => !edges.includes(element)); - return { edges, nodes }; -} - -function getIds(elements: ConnectionElements) { - return elements.map(({ data }) => data.id).sort(); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/service_maps/service_maps.ts b/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/service_maps/service_maps.ts deleted file mode 100644 index 1c849164d54c5..0000000000000 --- a/x-pack/test_serverless/api_integration/test_suites/observability/apm_api_integration/service_maps/service_maps.ts +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import expect from 'expect'; -import { serviceMap, timerange } from '@kbn/apm-synthtrace-client'; -import { Readable } from 'stream'; -import type { InternalRequestHeader, RoleCredentials } from '../../../../../shared/services'; -import { APMFtrContextProvider } from '../common/services'; - -export default function ({ getService }: APMFtrContextProvider) { - const apmApiClient = getService('apmApiClient'); - const svlUserManager = getService('svlUserManager'); - const svlCommonApi = getService('svlCommonApi'); - const synthtrace = getService('synthtrace'); - - const start = new Date('2024-06-01T00:00:00.000Z').getTime(); - const end = new Date('2024-06-01T00:01:00.000Z').getTime(); - - describe('APM Service maps', () => { - let roleAuthc: RoleCredentials; - let internalReqHeader: InternalRequestHeader; - let synthtraceEsClient: ApmSynthtraceEsClient; - - before(async () => { - synthtraceEsClient = await synthtrace.createSynthtraceEsClient(); - - const events = timerange(start, end) - .interval('10s') - .rate(3) - .generator( - serviceMap({ - services: [ - { 'frontend-rum': 'rum-js' }, - { 'frontend-node': 'nodejs' }, - { advertService: 'java' }, - ], - definePaths([rum, node, adv]) { - return [ - [ - [rum, 'fetchAd'], - [node, 'GET /nodejs/adTag'], - [adv, 'APIRestController#getAd'], - ['elasticsearch', 'GET ad-*/_search'], - ], - ]; - }, - }) - ); - - internalReqHeader = svlCommonApi.getInternalRequestHeader(); - roleAuthc = await svlUserManager.createM2mApiKeyWithRoleScope('admin'); - - return synthtraceEsClient.index(Readable.from(Array.from(events))); - }); - - after(async () => { - await svlUserManager.invalidateM2mApiKeyWithRoleScope(roleAuthc); - return synthtraceEsClient.clean(); - }); - - it('returns service map elements', async () => { - const response = await apmApiClient.slsUser({ - endpoint: 'GET /internal/apm/service-map', - params: { - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - }, - }, - roleAuthc, - internalReqHeader, - }); - - expect(response.status).toBe(200); - expect(response.body.elements.length).toBeGreaterThan(0); - }); - }); -} diff --git a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts index bfe3fd4cbb2c6..89543982f2d44 100644 --- a/x-pack/test_serverless/api_integration/test_suites/observability/index.ts +++ b/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @@ -12,7 +12,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { this.tags(['esGate']); loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts')); - loadTestFile(require.resolve('./apm_api_integration/service_maps/service_maps')); loadTestFile(require.resolve('./apm_api_integration/traces/critical_path')); loadTestFile(require.resolve('./cases')); loadTestFile(require.resolve('./synthetics')); From eec0b2c55dec43b2eb9813b7c9e0f4d19adfca48 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 11:33:52 +0000 Subject: [PATCH 07/50] [Ownership] Assign test files to ml-ui team (#200212) ## Summary Assign test files to ml-ui team Contributes to: #192979 --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Graham Hudgins Co-authored-by: Irene Blanco Co-authored-by: Pablo Machado Co-authored-by: Robert Oskamp Co-authored-by: Joe Reuter Co-authored-by: Maxim Palenov Co-authored-by: Dmitrii Shevchenko Co-authored-by: Nicolas Chaulet Co-authored-by: Ania Kowalska <63072419+akowalska622@users.noreply.github.com> Co-authored-by: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Co-authored-by: Sergi Romeu Co-authored-by: Nathan Reese Co-authored-by: Lisa Cawley Co-authored-by: Viduni Wickramarachchi Co-authored-by: wajihaparvez --- .github/CODEOWNERS | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index eb08ada7da2c4..2ca51c6debaef 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1350,8 +1350,11 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation # Machine Learning +/x-pack/test/stack_functional_integration/apps/ml @elastic/ml-ui +/x-pack/test/functional/fixtures/kbn_archiver/ml @elastic/ml-ui /x-pack/test/api_integration/apis/file_upload @elastic/ml-ui /x-pack/test/accessibility/apps/group2/ml.ts @elastic/ml-ui +/x-pack/test/accessibility/apps/group2/ml_* @elastic/ml-ui /x-pack/test/accessibility/apps/group3/ml_embeddables_in_dashboard.ts @elastic/ml-ui /x-pack/test/api_integration/apis/ml/ @elastic/ml-ui /x-pack/test/api_integration_basic/apis/ml/ @elastic/ml-ui @@ -1359,6 +1362,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/es_archives/ml/ @elastic/ml-ui /x-pack/test/functional/services/ml/ @elastic/ml-ui /x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui +/x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/config.ts @elastic/ml-ui /x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/ml/ @elastic/ml-ui /x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/ @elastic/ml-ui /x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/ @elastic/ml-ui @@ -1369,7 +1373,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/api_integration/services/ml.ts @elastic/ml-ui # Additional plugins and packages maintained by the ML team. -/test/examples/response_stream @elastic/ml-ui +/test/examples/response_stream/*.ts @elastic/ml-ui /x-pack/test/accessibility/apps/group2/transform.ts @elastic/ml-ui /x-pack/test/api_integration/apis/aiops/ @elastic/ml-ui /x-pack/test/api_integration/apis/transform/ @elastic/ml-ui @@ -1380,6 +1384,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/apps/aiops @elastic/ml-ui /x-pack/test/functional/apps/transform/ @elastic/ml-ui /x-pack/test/functional/services/transform/ @elastic/ml-ui +/x-pack/test/functional/services/aiops @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui # Maps From 0f2a96d31175687b2767e9fc2f8944861961221b Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Mon, 18 Nov 2024 12:37:49 +0100 Subject: [PATCH 08/50] [Papercut] Fix KQL parsing error text styling (#199985) ## Summary This PR fixes styling for `KQL parsing error` text on various elements on `Maps` and `Dashboard` to have the invalid syntax and ASCII arrow on new line. Closes: #49377 ![dashboard1](https://github.com/user-attachments/assets/c607c0e0-24d8-4bd9-8106-d5c94fe1197d) ![dashboard2](https://github.com/user-attachments/assets/cb4b58af-6d8b-4609-9a4e-b948b1ec9340) ![dashboard3](https://github.com/user-attachments/assets/5a6c883e-76dc-4f9d-8db6-94c3e0bc359a) ![maps](https://github.com/user-attachments/assets/83d897a4-6856-4c47-8b14-18a1fff1de9d) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- packages/kbn-react-hooks/README.md | 20 ++++++++++++++++-- packages/kbn-react-hooks/index.ts | 1 + .../src/use_error_text_style/index.ts | 10 +++++++++ .../use_error_text_style.ts | 21 +++++++++++++++++++ .../components/control_error.tsx | 4 +++- src/plugins/controls/tsconfig.json | 1 + .../presentation_panel_error.tsx | 5 ++++- src/plugins/presentation_panel/tsconfig.json | 3 ++- .../embeddable/visualize_embeddable.tsx | 8 ++++--- src/plugins/visualizations/tsconfig.json | 3 ++- .../layer_toc/toc_entry/legend_details.tsx | 7 +++++-- x-pack/plugins/maps/tsconfig.json | 3 ++- 12 files changed, 74 insertions(+), 12 deletions(-) create mode 100644 packages/kbn-react-hooks/src/use_error_text_style/index.ts create mode 100644 packages/kbn-react-hooks/src/use_error_text_style/use_error_text_style.ts diff --git a/packages/kbn-react-hooks/README.md b/packages/kbn-react-hooks/README.md index 792df6534c8d8..6eba8db3641d6 100644 --- a/packages/kbn-react-hooks/README.md +++ b/packages/kbn-react-hooks/README.md @@ -4,7 +4,7 @@ A utility package, `@kbn/react-hooks`, provides custom react hooks for simple ab ## Custom Hooks -### [useBoolean](./src/useBoolean) +### [useBoolean](./src/use_boolean/use_boolean.ts) Simplify handling boolean value with predefined handlers. @@ -25,4 +25,20 @@ function App() { ); } -``` \ No newline at end of file +``` + +### [useErrorTextStyle](./src/use_error_text_style/use_error_text_style.ts) + +Returns styles used for styling error text. + +```tsx +function App() { + const errorTextStyle = useErrorTextStyle(); + + return ( +
+ Error message +
+ ); +} +``` diff --git a/packages/kbn-react-hooks/index.ts b/packages/kbn-react-hooks/index.ts index 51c15bd0f10d2..0c8550091ef5a 100644 --- a/packages/kbn-react-hooks/index.ts +++ b/packages/kbn-react-hooks/index.ts @@ -8,4 +8,5 @@ */ export { useBoolean } from './src/use_boolean'; +export { useErrorTextStyle } from './src/use_error_text_style'; export type { UseBooleanHandlers, UseBooleanResult } from './src/use_boolean'; diff --git a/packages/kbn-react-hooks/src/use_error_text_style/index.ts b/packages/kbn-react-hooks/src/use_error_text_style/index.ts new file mode 100644 index 0000000000000..3cbc69f2b988d --- /dev/null +++ b/packages/kbn-react-hooks/src/use_error_text_style/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export * from './use_error_text_style'; diff --git a/packages/kbn-react-hooks/src/use_error_text_style/use_error_text_style.ts b/packages/kbn-react-hooks/src/use_error_text_style/use_error_text_style.ts new file mode 100644 index 0000000000000..cb21a95c6ae71 --- /dev/null +++ b/packages/kbn-react-hooks/src/use_error_text_style/use_error_text_style.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { css } from '@emotion/react'; +import { useEuiTheme } from '@elastic/eui'; + +export const useErrorTextStyle = () => { + const { euiTheme } = useEuiTheme(); + const errorTextStyle = css` + font-family: ${euiTheme.font.familyCode}; + white-space: break-spaces; + `; + + return errorTextStyle; +}; diff --git a/src/plugins/controls/public/control_group/components/control_error.tsx b/src/plugins/controls/public/control_group/components/control_error.tsx index 2ef6b06faeedd..80d33d8225b3b 100644 --- a/src/plugins/controls/public/control_group/components/control_error.tsx +++ b/src/plugins/controls/public/control_group/components/control_error.tsx @@ -12,12 +12,14 @@ import React, { useState } from 'react'; import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import { Markdown } from '@kbn/shared-ux-markdown'; +import { useErrorTextStyle } from '@kbn/react-hooks'; interface ControlErrorProps { error: Error | string; } export const ControlError = ({ error }: ControlErrorProps) => { + const errorTextStyle = useErrorTextStyle(); const [isPopoverOpen, setPopoverOpen] = useState(false); const errorMessage = error instanceof Error ? error.message : error; @@ -47,7 +49,7 @@ export const ControlError = ({ error }: ControlErrorProps) => { className="controlPanel errorEmbeddableCompact__popover" closePopover={() => setPopoverOpen(false)} > - + {errorMessage} diff --git a/src/plugins/controls/tsconfig.json b/src/plugins/controls/tsconfig.json index 41ab33dc18969..7759a0fdc7935 100644 --- a/src/plugins/controls/tsconfig.json +++ b/src/plugins/controls/tsconfig.json @@ -39,6 +39,7 @@ "@kbn/presentation-panel-plugin", "@kbn/shared-ux-utility", "@kbn/std", + "@kbn/react-hooks", ], "exclude": ["target/**/*"] } diff --git a/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx b/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx index 5e6ac14d14328..4973e2599ddad 100644 --- a/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx +++ b/src/plugins/presentation_panel/public/panel_component/presentation_panel_error.tsx @@ -16,6 +16,7 @@ import { renderSearchError } from '@kbn/search-errors'; import { Markdown } from '@kbn/shared-ux-markdown'; import { Subscription } from 'rxjs'; import { i18n } from '@kbn/i18n'; +import { useErrorTextStyle } from '@kbn/react-hooks'; import { editPanelAction } from '../panel_actions/panel_actions'; import { getErrorCallToAction } from './presentation_panel_strings'; import { DefaultPresentationPanelApi } from './types'; @@ -27,6 +28,8 @@ export const PresentationPanelError = ({ error: ErrorLike; api?: DefaultPresentationPanelApi; }) => { + const errorTextStyle = useErrorTextStyle(); + const [isEditable, setIsEditable] = useState(false); const handleErrorClick = useMemo( () => (isEditable ? () => editPanelAction?.execute({ embeddable: api }) : undefined), @@ -82,7 +85,7 @@ export const PresentationPanelError = ({ + {error.message?.length ? error.message diff --git a/src/plugins/presentation_panel/tsconfig.json b/src/plugins/presentation_panel/tsconfig.json index 9b867c0ada43c..255e4fd4eccde 100644 --- a/src/plugins/presentation_panel/tsconfig.json +++ b/src/plugins/presentation_panel/tsconfig.json @@ -28,7 +28,8 @@ "@kbn/data-views-plugin", "@kbn/panel-loader", "@kbn/search-errors", - "@kbn/shared-ux-markdown" + "@kbn/shared-ux-markdown", + "@kbn/react-hooks" ], "exclude": ["target/**/*"] } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx index 52c76a426dc14..7b48521265d6f 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.tsx @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { EuiEmptyPrompt, EuiFlexGroup, EuiLoadingChart } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiFlexGroup, EuiLoadingChart, EuiText } from '@elastic/eui'; import { isChartSizeEvent } from '@kbn/chart-expressions-common'; import { APPLY_FILTER_TRIGGER } from '@kbn/data-plugin/public'; import type { DataView } from '@kbn/data-views-plugin/public'; @@ -38,6 +38,7 @@ import { apiPublishesSearchSession } from '@kbn/presentation-publishing/interfac import { get, isEmpty, isEqual, isNil, omitBy } from 'lodash'; import React, { useEffect, useRef } from 'react'; import { BehaviorSubject, switchMap } from 'rxjs'; +import { useErrorTextStyle } from '@kbn/react-hooks'; import { VISUALIZE_APP_NAME, VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { getInspector, getUiActions, getUsageCollection } from '../services'; @@ -454,6 +455,7 @@ export const getVisualizeEmbeddableFactory: (deps: { const hasRendered = useStateFromPublishingSubject(hasRendered$); const domNode = useRef(null); const { error, isLoading } = useExpressionRenderer(domNode, expressionParams); + const errorTextStyle = useErrorTextStyle(); useEffect(() => { return () => { @@ -495,9 +497,9 @@ export const getVisualizeEmbeddableFactory: (deps: { } body={ -

+ {error.name}: {error.message} -

+
} /> )} diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 06d3931fe8e08..51deaf4139aa2 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -72,7 +72,8 @@ "@kbn/presentation-containers", "@kbn/search-response-warnings", "@kbn/embeddable-enhanced-plugin", - "@kbn/content-management-utils" + "@kbn/content-management-utils", + "@kbn/react-hooks" ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/legend_details.tsx b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/legend_details.tsx index 3f9e5c8f11627..37257746006cd 100644 --- a/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/legend_details.tsx +++ b/x-pack/plugins/maps/public/connected_components/right_side_controls/layer_control/layer_toc/toc_entry/legend_details.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { Adapters } from '@kbn/inspector-plugin/common/adapters'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; +import { useErrorTextStyle } from '@kbn/react-hooks'; import type { ILayer } from '../../../../../classes/layers/layer'; interface Props { @@ -16,13 +17,15 @@ interface Props { } export function LegendDetails({ inspectorAdapters, layer }: Props) { + const errorTextStyle = useErrorTextStyle(); + const errors = layer.getErrors(inspectorAdapters); if (errors.length) { return ( <> {errors.map(({ title, body }, index) => (
- + {body} @@ -37,7 +40,7 @@ export function LegendDetails({ inspectorAdapters, layer }: Props) { <> {warnings.map(({ title, body }, index) => (
- + {body} diff --git a/x-pack/plugins/maps/tsconfig.json b/x-pack/plugins/maps/tsconfig.json index dfee193ed0a25..e82e35b5ccbfd 100644 --- a/x-pack/plugins/maps/tsconfig.json +++ b/x-pack/plugins/maps/tsconfig.json @@ -90,7 +90,8 @@ "@kbn/react-kibana-context-render", "@kbn/embeddable-enhanced-plugin", "@kbn/presentation-containers", - "@kbn/field-utils" + "@kbn/field-utils", + "@kbn/react-hooks" ], "exclude": [ "target/**/*", From f752ef07cb51547491b21d44cdd610b0ebbefc64 Mon Sep 17 00:00:00 2001 From: Tomasz Kajtoch Date: Mon, 18 Nov 2024 12:42:30 +0100 Subject: [PATCH 09/50] Add basic support for experimental theme switching (#199748) ## Summary This PR adds basic support for theme switching to test experimental themes internally. It defines a new `theme:name` setting which is read-only (and thus hidden) by default, and needs `uiSettings.experimental.themeSwitcherEnabled: true` config setting to become visible. The implementation and the way the theme switcher feature is enabled will likely change in the upcoming weeks when we iron out the details, but the way it works right now is sufficient for testing and that's what the immediate goal is. Please note this PR includes mostly setup work, and no additional themes have been added here. To make reviewing slightly easier, these will be added separately. The feature and settings should be undocumented for the time being and disabled by default. --- As you might notice, there's a new setting added despite already having `theme:version`. We decided to introduce a new setting instead of resurrecting an existing one for better naming and flexibility and to reduce the risk of breaking things that might still use the old setting value (hardcoded to `v8` at the moment). The current plan is to remove `theme:version` in 9.0. The theme_loader got refactored to only bundle active themes coming from `KBN_OPTIMIZER_THEMES` (defaulting to `v8light` and `v8dark`) instead of `ALL_THEMES` that would significantly increase bundle sizes even when the experimental themes were not enabled. Considering there's a SASS to CSS-in-JS migration happening right now, we don't need to worry about Kibana bundles growing in size when a new theme is officially added. ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) - [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Clint Andrew Hall Co-authored-by: Rudolf Meijering Co-authored-by: Elastic Machine Co-authored-by: Tiago Costa --- .../core/base/core-base-common/BUILD.bazel | 35 +++++++ .../src/injected_metadata_service.mock.ts | 1 + .../src/types.ts | 1 + .../rendering_service.test.ts.snap | 18 ++++ .../src/bootstrap/bootstrap_renderer.test.ts | 60 ++++++++---- .../src/bootstrap/bootstrap_renderer.ts | 7 +- .../src/bootstrap/get_theme_tag.test.ts | 8 +- .../src/bootstrap/get_theme_tag.ts | 12 +-- .../src/rendering_service.tsx | 4 + .../src/core_theme_provider.test.tsx | 6 +- .../src/theme_service.test.ts | 10 ++ .../src/theme_service.ts | 21 ++-- .../src/theme_service.mock.ts | 1 + .../theme/core-theme-browser/src/types.ts | 4 + .../core-ui-settings-common/index.ts | 13 +++ .../core-ui-settings-common/src/theme.ts | 97 +++++++++++++++++++ .../src/settings/index.ts | 1 + .../src/settings/theme.ts | 46 +++++++-- .../src/ui_settings_config.ts | 5 + .../src/ui_settings_service.ts | 9 +- .../settings/setting_ids/index.ts | 1 + packages/kbn-optimizer/limits.yml | 2 +- packages/kbn-optimizer/src/common/index.ts | 1 - .../src/common/theme_tags.test.ts | 79 --------------- .../kbn-optimizer/src/common/theme_tags.ts | 55 ----------- .../kbn-optimizer/src/common/worker_config.ts | 2 +- .../kbn-optimizer/src/log_optimizer_state.ts | 7 +- .../src/optimizer/optimizer_cache_key.test.ts | 2 +- .../src/optimizer/optimizer_config.test.ts | 4 +- .../src/optimizer/optimizer_config.ts | 11 +-- .../kbn-optimizer/src/worker/theme_loader.ts | 58 ++++++----- packages/kbn-optimizer/tsconfig.json | 1 + packages/kbn-storybook/src/lib/decorators.tsx | 4 +- packages/kbn-ui-shared-deps-src/BUILD.bazel | 1 + .../kbn-ui-shared-deps-src/src/definitions.js | 1 + packages/kbn-ui-shared-deps-src/src/entry.js | 1 + packages/kbn-ui-theme/src/theme.ts | 2 + .../__mocks__/services.ts | 2 +- .../react/kibana_context/common/BUILD.bazel | 36 +++++++ .../kibana_context/common/color_mode.test.ts | 4 +- packages/react/kibana_context/common/index.ts | 2 + .../react/kibana_context/common/kibana.jsonc | 2 +- packages/react/kibana_context/common/theme.ts | 26 +++++ packages/react/kibana_context/common/types.ts | 4 + .../react/kibana_context/root/BUILD.bazel | 37 +++++++ .../kibana_context/root/eui_provider.test.tsx | 6 +- .../kibana_context/root/eui_provider.tsx | 20 +++- .../react/kibana_context/root/kibana.jsonc | 2 +- .../root/root_provider.test.tsx | 6 +- .../react/kibana_context/theme/BUILD.bazel | 38 ++++++++ .../react/kibana_context/theme/kibana.jsonc | 2 +- .../theme/theme_provider.test.tsx | 9 +- .../kibana_mount/to_mount_point.test.tsx | 6 +- .../public/__mocks__/theme.ts | 2 +- .../public/services/theme/theme.test.tsx | 4 +- .../discover/public/__mocks__/services.ts | 2 +- .../public/components/guide_panel.test.tsx | 2 +- .../public/components/guide_panel.tsx | 2 +- .../public/components/overview/overview.tsx | 2 +- .../public/dark_mode/use_dark_mode.test.tsx | 4 +- .../server/collectors/management/schema.ts | 4 + .../server/collectors/management/types.ts | 1 + .../theme/kibana_theme_provider.test.tsx | 6 +- src/plugins/telemetry/schema/oss_plugins.json | 6 ++ .../fleet/.storybook/context/index.tsx | 2 +- .../workspace_panel/workspace_panel.tsx | 5 +- .../datatable/components/table_basic.tsx | 5 +- .../datatable/visualization.tsx | 5 +- .../partition/visualization.tsx | 5 +- .../tagcloud/tagcloud_visualization.tsx | 5 +- .../visualizations/xy/visualization.tsx | 5 +- .../infra/public/hooks/use_is_dark_mode.ts | 2 +- .../test_utils/use_global_storybook_theme.tsx | 4 +- .../test_utils/use_global_storybook_theme.tsx | 4 +- .../test_utils/use_global_storybook_theme.tsx | 4 +- .../test_utils/use_global_storybook_theme.tsx | 4 +- .../kibana_react.storybook_decorator.tsx | 1 + .../kibana_react.storybook_decorator.tsx | 1 + .../user_profile/user_profile.test.tsx | 4 +- .../toolbar/components/inspect/modal.test.tsx | 2 +- 80 files changed, 597 insertions(+), 289 deletions(-) create mode 100644 packages/core/base/core-base-common/BUILD.bazel create mode 100644 packages/core/ui-settings/core-ui-settings-common/src/theme.ts delete mode 100644 packages/kbn-optimizer/src/common/theme_tags.test.ts delete mode 100644 packages/kbn-optimizer/src/common/theme_tags.ts create mode 100644 packages/react/kibana_context/common/BUILD.bazel create mode 100644 packages/react/kibana_context/common/theme.ts create mode 100644 packages/react/kibana_context/root/BUILD.bazel create mode 100644 packages/react/kibana_context/theme/BUILD.bazel diff --git a/packages/core/base/core-base-common/BUILD.bazel b/packages/core/base/core-base-common/BUILD.bazel new file mode 100644 index 0000000000000..30c3b1ae616f4 --- /dev/null +++ b/packages/core/base/core-base-common/BUILD.bazel @@ -0,0 +1,35 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", +] + +js_library( + name = "core-base-common", + package_name = "@kbn/core-base-common", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts index 804134cabd4b9..68fa84022ce34 100644 --- a/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts +++ b/packages/core/injected-metadata/core-injected-metadata-browser-mocks/src/injected_metadata_service.mock.ts @@ -57,6 +57,7 @@ const createSetupContractMock = () => { setupContract.getPlugins.mockReturnValue([]); setupContract.getTheme.mockReturnValue({ darkMode: false, + name: 'amsterdam', version: 'v8', stylesheetPaths: { default: ['light-1.css'], diff --git a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts index 1ee75dbfc0d5d..e988420720900 100644 --- a/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts +++ b/packages/core/injected-metadata/core-injected-metadata-common-internal/src/types.ts @@ -41,6 +41,7 @@ export interface InjectedMetadataExternalUrlPolicy { /** @internal */ export interface InjectedMetadataTheme { darkMode: DarkModeValue; + name: string; version: ThemeVersion; stylesheetPaths: { default: string[]; diff --git a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap index c858b6a8470d2..8b1dc7ef53e15 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap +++ b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap @@ -68,6 +68,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -149,6 +150,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -234,6 +236,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -315,6 +318,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -396,6 +400,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -481,6 +486,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -562,6 +568,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -643,6 +650,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -724,6 +732,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -814,6 +823,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -895,6 +905,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -985,6 +996,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1071,6 +1083,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1152,6 +1165,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1242,6 +1256,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1328,6 +1343,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1414,6 +1430,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", @@ -1502,6 +1519,7 @@ Object { "serverBasePath": "/mock-server-basepath", "theme": Object { "darkMode": "theme:darkMode", + "name": "theme:name", "stylesheetPaths": Object { "dark": Array [ "/style-1.css", diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts index 597e4159e4cc7..25d7e241325f3 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts @@ -34,6 +34,18 @@ const createPackageInfo = (parts: Partial = {}): PackageInfo => ({ ...parts, }); +const getClientGetMockImplementation = + ({ darkMode, name }: { darkMode?: boolean; name?: string } = {}) => + (key: string) => { + switch (key) { + case 'theme:darkMode': + return Promise.resolve(darkMode ?? false); + case 'theme:name': + return Promise.resolve(name ?? 'amsterdam'); + } + return Promise.resolve(); + }; + const createUiPlugins = (): UiPlugins => ({ public: new Map(), internal: new Map(), @@ -59,6 +71,7 @@ describe('bootstrapRenderer', () => { getPluginsBundlePathsMock.mockReturnValue(new Map()); renderTemplateMock.mockReturnValue('__rendered__'); getJsDependencyPathsMock.mockReturnValue([]); + uiSettingsClient.get.mockImplementation(getClientGetMockImplementation()); renderer = bootstrapRendererFactory({ auth, @@ -91,13 +104,17 @@ describe('bootstrapRenderer', () => { uiSettingsClient, }); - expect(uiSettingsClient.get).toHaveBeenCalledTimes(1); + expect(uiSettingsClient.get).toHaveBeenCalledTimes(2); expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode'); + expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:name'); }); it('calls getThemeTag with the values from the UiSettingsClient (true/dark) when the UserSettingsService is not provided', async () => { - uiSettingsClient.get.mockResolvedValue(true); - + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -107,13 +124,13 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); it('calls getThemeTag with the values from the UiSettingsClient (false/light) when the UserSettingsService is not provided', async () => { - uiSettingsClient.get.mockResolvedValue(false); + uiSettingsClient.get.mockImplementation(getClientGetMockImplementation({})); const request = httpServerMock.createKibanaRequest(); @@ -124,7 +141,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -150,7 +167,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -166,7 +183,6 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(true); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -176,7 +192,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -192,7 +208,6 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(false); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -202,7 +217,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); @@ -218,7 +233,11 @@ describe('bootstrapRenderer', () => { userSettingsService, }); - uiSettingsClient.get.mockResolvedValue(true); + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); await renderer({ @@ -228,7 +247,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -250,12 +269,17 @@ describe('bootstrapRenderer', () => { uiSettingsClient, }); - expect(uiSettingsClient.get).toHaveBeenCalledTimes(1); + expect(uiSettingsClient.get).toHaveBeenCalledTimes(2); expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:darkMode'); + expect(uiSettingsClient.get).toHaveBeenCalledWith('theme:name'); }); it('calls getThemeTag with the correct parameters', async () => { - uiSettingsClient.get.mockResolvedValue(true); + uiSettingsClient.get.mockImplementation( + getClientGetMockImplementation({ + darkMode: true, + }) + ); const request = httpServerMock.createKibanaRequest(); @@ -266,7 +290,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }); }); @@ -283,7 +307,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'system', darkMode: false, }); }); @@ -318,7 +342,7 @@ describe('bootstrapRenderer', () => { expect(getThemeTagMock).toHaveBeenCalledTimes(1); expect(getThemeTagMock).toHaveBeenCalledWith({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }); }); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts index 8aa0d2a6c0387..5b8c267532d0b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts @@ -9,7 +9,6 @@ import { createHash } from 'crypto'; import { PackageInfo } from '@kbn/config'; -import { ThemeVersion } from '@kbn/ui-shared-deps-npm'; import type { KibanaRequest, HttpAuth } from '@kbn/core-http-server'; import { type DarkModeValue, parseDarkModeValue } from '@kbn/core-ui-settings-common'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-server'; @@ -59,7 +58,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ return async function bootstrapRenderer({ uiSettingsClient, request, isAnonymousPage = false }) { let darkMode: DarkModeValue = false; - const themeVersion: ThemeVersion = 'v8'; + let themeName: string = 'amsterdam'; try { const authenticated = isAuthenticated(request); @@ -72,6 +71,8 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ } else { darkMode = parseDarkModeValue(await uiSettingsClient.get('theme:darkMode')); } + + themeName = await uiSettingsClient.get('theme:name'); } } catch (e) { // just use the default values in case of connectivity issues with ES @@ -83,7 +84,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ } const themeTag = getThemeTag({ - themeVersion, + name: !themeName || themeName === 'amsterdam' ? 'v8' : themeName, darkMode, }); const bundlesHref = getBundlesHref(baseHref); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts index 0f9839e8cda89..216e87269818b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.test.ts @@ -10,18 +10,18 @@ import { getThemeTag } from './get_theme_tag'; describe('getThemeTag', () => { - it('returns the correct value for version:v8 and darkMode:false', () => { + it('returns the correct value for name:v8 and darkMode:false', () => { expect( getThemeTag({ - themeVersion: 'v8', + name: 'v8', darkMode: false, }) ).toEqual('v8light'); }); - it('returns the correct value for version:v8 and darkMode:true', () => { + it('returns the correct value for name:v8 and darkMode:true', () => { expect( getThemeTag({ - themeVersion: 'v8', + name: 'v8', darkMode: true, }) ).toEqual('v8dark'); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts index 97f5c17ef240b..f89bd41404633 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/get_theme_tag.ts @@ -7,18 +7,10 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; - /** * Computes the themeTag that will be used on the client-side as `__kbnThemeTag__` * @see `packages/kbn-ui-shared-deps-src/theme.ts` */ -export const getThemeTag = ({ - themeVersion, - darkMode, -}: { - themeVersion: ThemeVersion; - darkMode: boolean; -}) => { - return `${themeVersion}${darkMode ? 'dark' : 'light'}`; +export const getThemeTag = ({ name, darkMode }: { name: string; darkMode: boolean }) => { + return `${name}${darkMode ? 'dark' : 'light'}`; }; diff --git a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx index ace0399f242af..a92c3dac485b5 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx +++ b/packages/core/rendering/core-rendering-server-internal/src/rendering_service.tsx @@ -22,6 +22,7 @@ import type { CustomBranding } from '@kbn/core-custom-branding-common'; import { type DarkModeValue, parseDarkModeValue, + parseThemeNameValue, type UiSettingsParams, type UserProvidedValues, } from '@kbn/core-ui-settings-common'; @@ -211,6 +212,8 @@ export class RenderingService { darkMode = getSettingValue('theme:darkMode', settings, parseDarkModeValue); } + const themeName = getSettingValue('theme:name', settings, parseThemeNameValue); + const themeStylesheetPaths = (mode: boolean) => getThemeStylesheetPaths({ darkMode: mode, @@ -274,6 +277,7 @@ export class RenderingService { }, theme: { darkMode, + name: themeName, version: themeVersion, stylesheetPaths: { default: themeStylesheetPaths(false), diff --git a/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx b/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx index 3f4aebe797172..a3e4516b07510 100644 --- a/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx +++ b/packages/core/theme/core-theme-browser-internal/src/core_theme_provider.test.tsx @@ -50,7 +50,7 @@ describe('CoreThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: CoreTheme = { darkMode: true }; + const coreTheme: CoreTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -64,7 +64,7 @@ describe('CoreThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -77,7 +77,7 @@ describe('CoreThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts b/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts index 45c90d90d522a..575d98fe40c8d 100644 --- a/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts +++ b/packages/core/theme/core-theme-browser-internal/src/theme_service.test.ts @@ -45,6 +45,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: false, stylesheetPaths: { dark: ['dark-1.css'], @@ -58,6 +59,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); }); @@ -88,6 +90,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: true, stylesheetPaths: { dark: ['dark-1.css'], @@ -101,6 +104,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); }); @@ -131,6 +135,7 @@ describe('ThemeService', () => { beforeEach(() => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: 'system', stylesheetPaths: { dark: ['dark-1.css'], @@ -150,6 +155,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8light'); @@ -177,6 +183,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: false, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8light'); @@ -196,6 +203,7 @@ describe('ThemeService', () => { expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); expect(window.__kbnThemeTag__).toEqual('v8dark'); @@ -244,6 +252,7 @@ describe('ThemeService', () => { it('exposes a `theme$` observable with the values provided by the injected metadata', async () => { injectedMetadata.getTheme.mockReturnValue({ version: 'v8', + name: 'amsterdam', darkMode: true, stylesheetPaths: { dark: [], @@ -255,6 +264,7 @@ describe('ThemeService', () => { const theme = await firstValueFrom(theme$); expect(theme).toEqual({ darkMode: true, + name: 'amsterdam', }); }); }); diff --git a/packages/core/theme/core-theme-browser-internal/src/theme_service.ts b/packages/core/theme/core-theme-browser-internal/src/theme_service.ts index 7bc51c9a0c34a..e79a19550bb8d 100644 --- a/packages/core/theme/core-theme-browser-internal/src/theme_service.ts +++ b/packages/core/theme/core-theme-browser-internal/src/theme_service.ts @@ -28,16 +28,21 @@ export class ThemeService { public setup({ injectedMetadata }: ThemeServiceSetupDeps): ThemeServiceSetup { const themeMetadata = injectedMetadata.getTheme(); + this.themeMetadata = themeMetadata; - let theme: CoreTheme; + let darkMode: boolean; if (themeMetadata.darkMode === 'system' && browsersSupportsSystemTheme()) { - theme = { darkMode: systemThemeIsDark() }; + darkMode = systemThemeIsDark(); } else { - const darkMode = themeMetadata.darkMode === 'system' ? false : themeMetadata.darkMode; - theme = { darkMode }; + darkMode = themeMetadata.darkMode === 'system' ? false : themeMetadata.darkMode; } + const theme: CoreTheme = { + darkMode, + name: themeMetadata.name, + }; + this.applyTheme(theme); this.contract = { @@ -73,11 +78,13 @@ export class ThemeService { }); _setDarkMode(darkMode); - updateKbnThemeTag(darkMode); + updateKbnThemeTag(theme); } } -const updateKbnThemeTag = (darkMode: boolean) => { +const updateKbnThemeTag = (theme: CoreTheme) => { + const name = theme.name === 'amsterdam' ? 'v8' : theme.name; + const globals: any = typeof window === 'undefined' ? {} : window; - globals.__kbnThemeTag__ = darkMode ? 'v8dark' : 'v8light'; + globals.__kbnThemeTag__ = `${name}${theme.darkMode ? 'dark' : 'light'}`; }; diff --git a/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts b/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts index beee2320d7cca..e3d2b66645794 100644 --- a/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts +++ b/packages/core/theme/core-theme-browser-mocks/src/theme_service.mock.ts @@ -14,6 +14,7 @@ import type { ThemeService } from '@kbn/core-theme-browser-internal'; const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createThemeMock = (): CoreTheme => { diff --git a/packages/core/theme/core-theme-browser/src/types.ts b/packages/core/theme/core-theme-browser/src/types.ts index 161758ec362f3..365cde9f814ac 100644 --- a/packages/core/theme/core-theme-browser/src/types.ts +++ b/packages/core/theme/core-theme-browser/src/types.ts @@ -17,6 +17,10 @@ import { Observable } from 'rxjs'; export interface CoreTheme { /** is dark mode enabled or not */ readonly darkMode: boolean; + /** + * Name of the active theme + */ + readonly name: string; } /** diff --git a/packages/core/ui-settings/core-ui-settings-common/index.ts b/packages/core/ui-settings/core-ui-settings-common/index.ts index b7adb288008df..d290b9065c546 100644 --- a/packages/core/ui-settings/core-ui-settings-common/index.ts +++ b/packages/core/ui-settings/core-ui-settings-common/index.ts @@ -17,5 +17,18 @@ export type { GetUiSettingsContext, } from './src/ui_settings'; export { type DarkModeValue, parseDarkModeValue } from './src/dark_mode'; +export { + DEFAULT_THEME_TAGS, + SUPPORTED_THEME_TAGS, + DEFAULT_THEME_NAME, + SUPPORTED_THEME_NAMES, + FALLBACK_THEME_TAG, + parseThemeTags, + hasNonDefaultThemeTags, + parseThemeNameValue, + type ThemeName, + type ThemeTag, + type ThemeTags, +} from './src/theme'; export { TIMEZONE_OPTIONS } from './src/timezones'; diff --git a/packages/core/ui-settings/core-ui-settings-common/src/theme.ts b/packages/core/ui-settings/core-ui-settings-common/src/theme.ts new file mode 100644 index 0000000000000..7bf9cbfe9486a --- /dev/null +++ b/packages/core/ui-settings/core-ui-settings-common/src/theme.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +export const DEFAULT_THEME_NAME = 'amsterdam'; +export const SUPPORTED_THEME_NAMES = ['amsterdam', 'borealis']; + +export type ThemeName = (typeof SUPPORTED_THEME_NAMES)[number]; + +/** + * Theme tags of the Amsterdam theme + */ +export const ThemeAmsterdamTags = ['v8light', 'v8dark'] as const; + +/** + * Theme tags of the experimental Borealis theme + */ +export const ThemeBorealisTags = ['borealislight', 'borealisdark'] as const; + +/** + * An array of all theme tags supported by Kibana. Note that this list doesn't + * reflect what theme tags are available in a Kibana build. + */ +export const SUPPORTED_THEME_TAGS = [...ThemeAmsterdamTags, ...ThemeBorealisTags] as const; + +export type ThemeTag = (typeof SUPPORTED_THEME_TAGS)[number]; +export type ThemeTags = readonly ThemeTag[]; + +/** + * An array of theme tags available in Kibana by default when not customized + * using KBN_OPTIMIZER_THEMES environment variable. + */ +export const DEFAULT_THEME_TAGS: ThemeTags = ThemeAmsterdamTags; + +export const FALLBACK_THEME_TAG: ThemeTag = 'v8light'; + +const isValidTag = (tag: unknown) => + SUPPORTED_THEME_TAGS.includes(tag as (typeof SUPPORTED_THEME_TAGS)[number]); + +export function parseThemeTags(input?: unknown): ThemeTags { + if (!input) { + return DEFAULT_THEME_TAGS; + } + + if (input === '*') { + // TODO: Replace with SUPPORTED_THEME_TAGS when Borealis is in public beta + return DEFAULT_THEME_TAGS; + } + + let rawTags: string[]; + if (typeof input === 'string') { + rawTags = input.split(',').map((tag) => tag.trim()); + } else if (Array.isArray(input)) { + rawTags = input; + } else { + throw new Error('Invalid theme tags, must be an array of strings'); + } + + if (!rawTags.length) { + throw new Error( + `Invalid theme tags, you must specify at least one of [${SUPPORTED_THEME_TAGS.join(', ')}]` + ); + } + + const invalidTags = rawTags.filter((t) => !isValidTag(t)); + if (invalidTags.length) { + throw new Error( + `Invalid theme tags [${invalidTags.join(', ')}], options: [${SUPPORTED_THEME_TAGS.join( + ', ' + )}]` + ); + } + + return rawTags as ThemeTags; +} + +export const hasNonDefaultThemeTags = (tags: ThemeTags) => + tags.length !== DEFAULT_THEME_TAGS.length || + tags.some((tag) => !DEFAULT_THEME_TAGS.includes(tag as (typeof DEFAULT_THEME_TAGS)[number])); + +export const parseThemeNameValue = (value: unknown): ThemeName => { + if (typeof value !== 'string') { + return DEFAULT_THEME_NAME; + } + + const themeName = value.toLowerCase(); + if (SUPPORTED_THEME_NAMES.includes(themeName.toLowerCase() as ThemeName)) { + return themeName as ThemeName; + } + + return DEFAULT_THEME_NAME; +}; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts index f74977af04b8b..093b4eef9a6de 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/index.ts @@ -18,6 +18,7 @@ import { getAnnouncementsSettings } from './announcements'; interface GetCoreSettingsOptions { isDist?: boolean; + isThemeSwitcherEnabled?: boolean; } export const getCoreSettings = ( diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts index 5701694f97abc..36324f951952e 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/settings/theme.ts @@ -10,15 +10,11 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import type { ThemeVersion } from '@kbn/ui-shared-deps-npm'; -import type { UiSettingsParams } from '@kbn/core-ui-settings-common'; - -function parseThemeTags() { - if (!process.env.KBN_OPTIMIZER_THEMES || process.env.KBN_OPTIMIZER_THEMES === '*') { - return ['v8light', 'v8dark']; - } - - return process.env.KBN_OPTIMIZER_THEMES.split(',').map((t) => t.trim()); -} +import { + type UiSettingsParams, + parseThemeTags, + SUPPORTED_THEME_NAMES, +} from '@kbn/core-ui-settings-common'; function getThemeInfo(options: GetThemeSettingsOptions) { if (options?.isDist ?? true) { @@ -27,7 +23,7 @@ function getThemeInfo(options: GetThemeSettingsOptions) { }; } - const themeTags = parseThemeTags(); + const themeTags = parseThemeTags(process.env.KBN_OPTIMIZER_THEMES); return { defaultDarkMode: themeTags[0].endsWith('dark'), }; @@ -35,6 +31,7 @@ function getThemeInfo(options: GetThemeSettingsOptions) { interface GetThemeSettingsOptions { isDist?: boolean; + isThemeSwitcherEnabled?: boolean; } export const getThemeSettings = ( @@ -89,5 +86,34 @@ export const getThemeSettings = ( readonly: true, schema: schema.literal('v8'), }, + /** + * Theme name is the (upcoming) replacement for theme versions. + */ + 'theme:name': { + name: i18n.translate('core.ui_settings.params.themeName', { + defaultMessage: 'Theme', + }), + type: 'select', + options: SUPPORTED_THEME_NAMES, + optionLabels: { + amsterdam: i18n.translate('core.ui_settings.params.themeName.options.amsterdam', { + defaultMessage: 'Amsterdam', + }), + borealis: i18n.translate('core.ui_settings.params.themeName.options.borealis', { + defaultMessage: 'Borealis', + }), + }, + value: 'amsterdam', + readonly: Object.hasOwn(options, 'isThemeSwitcherEnabled') + ? !options.isThemeSwitcherEnabled + : true, + requiresPageReload: true, + schema: schema.oneOf([ + schema.literal('amsterdam'), + schema.literal('borealis'), + // Allow experimental themes + schema.string(), + ]), + }, }; }; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts index 6563ffff78949..04b7ff6b0f558 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_config.ts @@ -19,6 +19,11 @@ const deprecations: ConfigDeprecationProvider = ({ unused, renameFromRoot }) => const configSchema = schema.object({ overrides: schema.object({}, { unknowns: 'allow' }), publicApiEnabled: offeringBasedSchema({ serverless: schema.boolean({ defaultValue: false }) }), + experimental: schema.maybe( + schema.object({ + themeSwitcherEnabled: schema.maybe(schema.boolean({ defaultValue: false })), + }) + ), }); export type UiSettingsConfigType = TypeOf; diff --git a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts index 958391b5fc725..70c880c85594f 100644 --- a/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts +++ b/packages/core/ui-settings/core-ui-settings-server-internal/src/ui_settings_service.ts @@ -68,10 +68,15 @@ export class UiSettingsService public async preboot(): Promise { this.log.debug('Prebooting ui settings service'); - const { overrides } = await firstValueFrom(this.config$); + const { overrides, experimental } = await firstValueFrom(this.config$); this.overrides = overrides; - this.register(getCoreSettings({ isDist: this.isDist })); + this.register( + getCoreSettings({ + isDist: this.isDist, + isThemeSwitcherEnabled: experimental?.themeSwitcherEnabled, + }) + ); return { createDefaultsClient: () => diff --git a/packages/kbn-management/settings/setting_ids/index.ts b/packages/kbn-management/settings/setting_ids/index.ts index bc0f7206a2835..9a7c95917878a 100644 --- a/packages/kbn-management/settings/setting_ids/index.ts +++ b/packages/kbn-management/settings/setting_ids/index.ts @@ -45,6 +45,7 @@ export const SHORT_DOTS_ENABLE_ID = 'shortDots:enable'; export const SORT_OPTIONS_ID = 'sort:options'; export const STATE_STORE_IN_SESSION_STORAGE_ID = 'state:storeInSessionStorage'; export const THEME_DARK_MODE_ID = 'theme:darkMode'; +export const THEME_NAME_ID = 'theme:name'; export const TIMEPICKER_QUICK_RANGES_ID = 'timepicker:quickRanges'; export const TIMEPICKER_REFRESH_INTERVAL_DEFAULTS_ID = 'timepicker:refreshIntervalDefaults'; export const TIMEPICKER_TIME_DEFAULTS_ID = 'timepicker:timeDefaults'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 8e1cd3e8fb7a1..58424700d9bf6 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -2,7 +2,7 @@ pageLoadAssetSize: actions: 20000 advancedSettings: 27596 aiAssistantManagementSelection: 19146 - aiops: 16526 + aiops: 16600 alerting: 106936 apm: 64385 banners: 17946 diff --git a/packages/kbn-optimizer/src/common/index.ts b/packages/kbn-optimizer/src/common/index.ts index 112e677c9d713..543991a92065d 100644 --- a/packages/kbn-optimizer/src/common/index.ts +++ b/packages/kbn-optimizer/src/common/index.ts @@ -18,7 +18,6 @@ export * from './rxjs_helpers'; export * from './array_helpers'; export * from './event_stream_helpers'; export * from './parse_path'; -export * from './theme_tags'; export * from './obj_helpers'; export * from './hashes'; export * from './dll_manifest'; diff --git a/packages/kbn-optimizer/src/common/theme_tags.test.ts b/packages/kbn-optimizer/src/common/theme_tags.test.ts deleted file mode 100644 index edf58797587f6..0000000000000 --- a/packages/kbn-optimizer/src/common/theme_tags.test.ts +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { parseThemeTags } from './theme_tags'; - -it('returns default tags when passed undefined', () => { - expect(parseThemeTags()).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns all tags when passed *', () => { - expect(parseThemeTags('*')).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns specific tag when passed a single value', () => { - expect(parseThemeTags('v8light')).toMatchInlineSnapshot(` - Array [ - "v8light", - ] - `); -}); - -it('returns specific tags when passed a comma separated list', () => { - expect(parseThemeTags('v8light,v8dark')).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('returns specific tags when passed an array', () => { - expect(parseThemeTags(['v8light', 'v8dark'])).toMatchInlineSnapshot(` - Array [ - "v8dark", - "v8light", - ] - `); -}); - -it('throws when an invalid tag is in the array', () => { - expect(() => parseThemeTags(['v8light', 'v7light'])).toThrowErrorMatchingInlineSnapshot( - `"Invalid theme tags [v7light], options: [v8dark, v8light]"` - ); -}); - -it('throws when an invalid tags in comma separated list', () => { - expect(() => parseThemeTags('v8light ,v7light')).toThrowErrorMatchingInlineSnapshot( - `"Invalid theme tags [v7light], options: [v8dark, v8light]"` - ); -}); - -it('returns tags in alphabetical order', () => { - const tags = parseThemeTags(['v8dark', 'v8light']); - expect(tags).toEqual(tags.slice().sort((a, b) => a.localeCompare(b))); -}); - -it('returns an immutable array', () => { - expect(() => { - const tags = parseThemeTags('v8light'); - // @ts-expect-error - tags.push('foo'); - }).toThrowErrorMatchingInlineSnapshot(`"Cannot add property 1, object is not extensible"`); -}); diff --git a/packages/kbn-optimizer/src/common/theme_tags.ts b/packages/kbn-optimizer/src/common/theme_tags.ts deleted file mode 100644 index fc126d55a4330..0000000000000 --- a/packages/kbn-optimizer/src/common/theme_tags.ts +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { ascending } from './array_helpers'; - -const tags = (...themeTags: string[]) => - Object.freeze(themeTags.sort(ascending((tag) => tag)) as ThemeTag[]); - -const validTag = (tag: any): tag is ThemeTag => ALL_THEMES.includes(tag); -const isArrayOfStrings = (input: unknown): input is string[] => - Array.isArray(input) && input.every((v) => typeof v === 'string'); - -export type ThemeTags = readonly ThemeTag[]; -export type ThemeTag = 'v8light' | 'v8dark'; -export const DEFAULT_THEMES = tags('v8light', 'v8dark'); -export const ALL_THEMES = tags('v8light', 'v8dark'); - -export function parseThemeTags(input?: any): ThemeTags { - if (!input) { - return DEFAULT_THEMES; - } - - if (input === '*') { - return ALL_THEMES; - } - - if (typeof input === 'string') { - input = input.split(',').map((tag) => tag.trim()); - } - - if (!isArrayOfStrings(input)) { - throw new Error(`Invalid theme tags, must be an array of strings`); - } - - if (!input.length) { - throw new Error( - `Invalid theme tags, you must specify at least one of [${ALL_THEMES.join(', ')}]` - ); - } - - const invalidTags = input.filter((t) => !validTag(t)); - if (invalidTags.length) { - throw new Error( - `Invalid theme tags [${invalidTags.join(', ')}], options: [${ALL_THEMES.join(', ')}]` - ); - } - - return tags(...input); -} diff --git a/packages/kbn-optimizer/src/common/worker_config.ts b/packages/kbn-optimizer/src/common/worker_config.ts index 8881d2354740b..6a61c3a99af07 100644 --- a/packages/kbn-optimizer/src/common/worker_config.ts +++ b/packages/kbn-optimizer/src/common/worker_config.ts @@ -9,8 +9,8 @@ import Path from 'path'; +import { ThemeTags, parseThemeTags } from '@kbn/core-ui-settings-common'; import { UnknownVals } from './ts_helpers'; -import { ThemeTags, parseThemeTags } from './theme_tags'; export interface WorkerConfig { readonly repoRoot: string; diff --git a/packages/kbn-optimizer/src/log_optimizer_state.ts b/packages/kbn-optimizer/src/log_optimizer_state.ts index 3173ef2a05980..2bb810f45d240 100644 --- a/packages/kbn-optimizer/src/log_optimizer_state.ts +++ b/packages/kbn-optimizer/src/log_optimizer_state.ts @@ -10,11 +10,12 @@ import { inspect } from 'util'; import { ToolingLog } from '@kbn/tooling-log'; +import { hasNonDefaultThemeTags } from '@kbn/core-ui-settings-common'; import { tap } from 'rxjs'; import { OptimizerConfig } from './optimizer'; import { OptimizerUpdate$ } from './run_optimizer'; -import { CompilerMsg, pipeClosure, ALL_THEMES } from './common'; +import { CompilerMsg, pipeClosure } from './common'; export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { return pipeClosure((update$: OptimizerUpdate$) => { @@ -80,9 +81,9 @@ export function logOptimizerState(log: ToolingLog, config: OptimizerConfig) { ); } - if (config.themeTags.length !== ALL_THEMES.length) { + if (hasNonDefaultThemeTags(config.themeTags)) { log.warning( - `only building [${config.themeTags}] themes, customize with the KBN_OPTIMIZER_THEMES environment variable` + `running with non-default [${config.themeTags}] set of themes, customize with the KBN_OPTIMIZER_THEMES environment variable` ); } } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts index 6d520836ac4b2..f08849005e971 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_cache_key.test.ts @@ -90,8 +90,8 @@ describe('getOptimizerCacheKey()', () => { "optimizerCacheKey": "♻", "repoRoot": , "themeTags": Array [ - "v8dark", "v8light", + "v8dark", ], }, } diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index a3329dcc3d57f..5fd2318953a8c 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -8,10 +8,10 @@ */ jest.mock('@kbn/repo-packages'); +jest.mock('@kbn/core-ui-settings-common'); jest.mock('./assign_bundles_to_workers'); jest.mock('./kibana_platform_plugins'); jest.mock('./get_plugin_bundles'); -jest.mock('../common/theme_tags'); jest.mock('./filter_by_id'); jest.mock('./focus_bundles'); jest.mock('../limits'); @@ -29,7 +29,7 @@ import { REPO_ROOT } from '@kbn/repo-info'; import { createAbsolutePathSerializer } from '@kbn/jest-serializers'; import { OptimizerConfig, ParsedOptions } from './optimizer_config'; -import { parseThemeTags } from '../common'; +import { parseThemeTags } from '@kbn/core-ui-settings-common'; expect.addSnapshotSerializer(createAbsolutePathSerializer()); diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts index b09650c0708da..1b04a6fbd25a3 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.ts @@ -10,16 +10,9 @@ import Path from 'path'; import Os from 'os'; import { getPackages, getPluginPackagesFilter, type PluginSelector } from '@kbn/repo-packages'; +import { ThemeTag, ThemeTags, parseThemeTags } from '@kbn/core-ui-settings-common'; -import { - Bundle, - WorkerConfig, - CacheableWorkerConfig, - ThemeTag, - ThemeTags, - parseThemeTags, - omit, -} from '../common'; +import { Bundle, WorkerConfig, CacheableWorkerConfig, omit } from '../common'; import { toKibanaPlatformPlugin, KibanaPlatformPlugin } from './kibana_platform_plugins'; import { getPluginBundles } from './get_plugin_bundles'; diff --git a/packages/kbn-optimizer/src/worker/theme_loader.ts b/packages/kbn-optimizer/src/worker/theme_loader.ts index 92a728f17f5cb..3bce12d94e974 100644 --- a/packages/kbn-optimizer/src/worker/theme_loader.ts +++ b/packages/kbn-optimizer/src/worker/theme_loader.ts @@ -9,12 +9,11 @@ import { stringifyRequest, getOptions } from 'loader-utils'; import webpack from 'webpack'; -import { parseThemeTags, ALL_THEMES, ThemeTag } from '../common'; - -const getVersion = (tag: ThemeTag) => 8; -const getIsDark = (tag: ThemeTag) => tag.includes('dark'); -const compare = (a: ThemeTag, b: ThemeTag) => - (getVersion(a) === getVersion(b) ? 1 : 0) + (getIsDark(a) === getIsDark(b) ? 1 : 0); +import { + FALLBACK_THEME_TAG, + parseThemeTags, + hasNonDefaultThemeTags, +} from '@kbn/core-ui-settings-common'; // eslint-disable-next-line import/no-default-export export default function (this: webpack.loader.LoaderContext) { @@ -23,27 +22,34 @@ export default function (this: webpack.loader.LoaderContext) { const options = getOptions(this); const bundleId = options.bundleId as string; const themeTags = parseThemeTags(options.themeTags); - - const cases = ALL_THEMES.map((tag) => { - if (themeTags.includes(tag)) { - return ` - case '${tag}': - return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});`; - } - - const fallback = themeTags - .slice() - .sort((a, b) => compare(b, tag) - compare(a, tag)) - .shift()!; - - const message = `SASS files in [${bundleId}] were not built for theme [${tag}]. Styles were compiled using the [${fallback}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [${tag}] in a comma separated list of themes you want to compile. You can also set it to "*" to build all themes.`; - return ` - case '${tag}': - console.error(new Error(${JSON.stringify(message)})); - return require(${stringifyRequest(this, `${this.resourcePath}?${fallback}`)})`; - }).join('\n'); + const isFallbackNeeded = hasNonDefaultThemeTags(themeTags); + + /** + * The following piece of code generates a `switch` statement that gets injected into the output + * bundle for all `.scss` file imports. The generated `switch` contains: + * - a `case` clause for each of the bundled theme tags, + * - an optional `default` clause for the theme fallback logic, included when some of the default + * Kibana theme tags are omitted and the fallback logic might be needed. The fallback logic + * should never have to run and is added as an extra precaution layer. + */ + + let defaultClause = ''; + if (isFallbackNeeded) { + defaultClause = ` + default: + console.error(new Error("SASS files in [${bundleId}] were not built for theme [" + window.__kbnThemeTag__ + "]. Styles were compiled using the [${FALLBACK_THEME_TAG}] theme instead to keep Kibana somewhat usable. Please adjust the advanced settings to make use of [${themeTags}] or make sure the KBN_OPTIMIZER_THEMES environment variable includes [" + window.__kbnThemeTag__ + "] in a comma-separated list of themes you want to compile. You can also set it to \'*\' to build all themes.")); + return require(${stringifyRequest(this, `${this.resourcePath}?${FALLBACK_THEME_TAG}`)});`; + } return ` -switch (window.__kbnThemeTag__) {${cases} +switch (window.__kbnThemeTag__) { +${themeTags + .map( + (tag) => ` + case '${tag}': + return require(${stringifyRequest(this, `${this.resourcePath}?${tag}`)});` + ) + .join('\n')} + ${defaultClause} }`; } diff --git a/packages/kbn-optimizer/tsconfig.json b/packages/kbn-optimizer/tsconfig.json index f8cb993537be7..d6e79f66a561a 100644 --- a/packages/kbn-optimizer/tsconfig.json +++ b/packages/kbn-optimizer/tsconfig.json @@ -18,6 +18,7 @@ "kbn_references": [ "@kbn/config-schema", "@kbn/dev-utils", + "@kbn/core-ui-settings-common", "@kbn/optimizer-webpack-helpers", "@kbn/std", "@kbn/ui-shared-deps-npm", diff --git a/packages/kbn-storybook/src/lib/decorators.tsx b/packages/kbn-storybook/src/lib/decorators.tsx index d6b3c6abf2f40..270da371172eb 100644 --- a/packages/kbn-storybook/src/lib/decorators.tsx +++ b/packages/kbn-storybook/src/lib/decorators.tsx @@ -21,7 +21,7 @@ import type { AnalyticsServiceStart } from '@kbn/core-analytics-browser'; import { KibanaRootContextProvider } from '@kbn/react-kibana-context-root'; import { i18n } from '@kbn/i18n'; -const theme$ = new BehaviorSubject({ darkMode: false }); +const theme$ = new BehaviorSubject({ darkMode: false, name: 'amsterdam' }); const i18nStart: I18nStart = { Context: ({ children }) => {children}, @@ -43,7 +43,7 @@ const KibanaContextDecorator: DecoratorFn = (storyFn, { globals }) => { const colorMode = globals.euiTheme === 'v8.dark' ? 'dark' : 'light'; useEffect(() => { - theme$.next({ darkMode: colorMode === 'dark' }); + theme$.next({ darkMode: colorMode === 'dark', name: 'amsterdam' }); }, [colorMode]); return ( diff --git a/packages/kbn-ui-shared-deps-src/BUILD.bazel b/packages/kbn-ui-shared-deps-src/BUILD.bazel index d2e67ccd14ac6..b0d7bb65843d9 100644 --- a/packages/kbn-ui-shared-deps-src/BUILD.bazel +++ b/packages/kbn-ui-shared-deps-src/BUILD.bazel @@ -39,6 +39,7 @@ webpack_cli( "//packages/shared-ux/error_boundary", "//packages/kbn-rison", "//packages/shared-ux/code_editor/impl:code_editor", + "//packages/react/kibana_context/theme", ], output_dir = True, args = [ diff --git a/packages/kbn-ui-shared-deps-src/src/definitions.js b/packages/kbn-ui-shared-deps-src/src/definitions.js index f56baf1731ac9..2c391dd18d1bc 100644 --- a/packages/kbn-ui-shared-deps-src/src/definitions.js +++ b/packages/kbn-ui-shared-deps-src/src/definitions.js @@ -105,6 +105,7 @@ const externals = { '@kbn/esql-ast': '__kbnSharedDeps__.KbnEsqlAst', '@kbn/ebt-tools': '__kbnSharedDeps__.KbnEbtTools', '@elastic/apm-rum-core': '__kbnSharedDeps__.ElasticApmRumCore', + '@kbn/react-kibana-context-theme': '__kbnSharedDeps__.KbnReactKibanaContextTheme', }; module.exports = { distDir, jsFilename, cssDistFilename, externals }; diff --git a/packages/kbn-ui-shared-deps-src/src/entry.js b/packages/kbn-ui-shared-deps-src/src/entry.js index f87c2e7d75ead..25432fcfe399e 100644 --- a/packages/kbn-ui-shared-deps-src/src/entry.js +++ b/packages/kbn-ui-shared-deps-src/src/entry.js @@ -78,3 +78,4 @@ export const KbnCodeEditor = require('@kbn/code-editor'); export const KbnEsqlAst = require('@kbn/esql-ast'); export const KbnEbtTools = require('@kbn/ebt-tools'); export const ElasticApmRumCore = require('@elastic/apm-rum-core'); +export const KbnReactKibanaContextTheme = require('@kbn/react-kibana-context-theme'); diff --git a/packages/kbn-ui-theme/src/theme.ts b/packages/kbn-ui-theme/src/theme.ts index 0d8f7fb789c2a..3718c7dfb9d64 100644 --- a/packages/kbn-ui-theme/src/theme.ts +++ b/packages/kbn-ui-theme/src/theme.ts @@ -7,6 +7,8 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +// TODO(tkajtoch): Add support for multiple themes + /* eslint-disable-next-line @kbn/eslint/module_migration */ import { default as v8Light } from '@elastic/eui/dist/eui_theme_light.json'; /* eslint-disable-next-line @kbn/eslint/module_migration */ diff --git a/packages/kbn-unified-data-table/__mocks__/services.ts b/packages/kbn-unified-data-table/__mocks__/services.ts index c4bafef861afb..8a3f9568ba5e9 100644 --- a/packages/kbn-unified-data-table/__mocks__/services.ts +++ b/packages/kbn-unified-data-table/__mocks__/services.ts @@ -43,7 +43,7 @@ export function createServicesMock() { ...uiSettingsMock, }; - const theme = themeServiceMock.createSetupContract({ darkMode: false }); + const theme = themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' }); corePluginMock.theme = theme; const dataPlugin = dataPluginMock.createStartContract(); diff --git a/packages/react/kibana_context/common/BUILD.bazel b/packages/react/kibana_context/common/BUILD.bazel new file mode 100644 index 0000000000000..43f20a833d07f --- /dev/null +++ b/packages/react/kibana_context/common/BUILD.bazel @@ -0,0 +1,36 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//@elastic/eui", +] + +js_library( + name = "common", + package_name = "@kbn/react-kibana-context-common", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/common/color_mode.test.ts b/packages/react/kibana_context/common/color_mode.test.ts index 2062f628b54ae..8fcd75f49d45a 100644 --- a/packages/react/kibana_context/common/color_mode.test.ts +++ b/packages/react/kibana_context/common/color_mode.test.ts @@ -11,10 +11,10 @@ import { getColorMode } from './color_mode'; describe('getColorMode', () => { it('returns the correct `colorMode` when `darkMode` is enabled', () => { - expect(getColorMode({ darkMode: true })).toEqual('DARK'); + expect(getColorMode({ name: 'amsterdam', darkMode: true })).toEqual('DARK'); }); it('returns the correct `colorMode` when `darkMode` is disabled', () => { - expect(getColorMode({ darkMode: false })).toEqual('LIGHT'); + expect(getColorMode({ name: 'amsterdam', darkMode: false })).toEqual('LIGHT'); }); }); diff --git a/packages/react/kibana_context/common/index.ts b/packages/react/kibana_context/common/index.ts index 77edac36e7912..541e804b788c7 100644 --- a/packages/react/kibana_context/common/index.ts +++ b/packages/react/kibana_context/common/index.ts @@ -8,6 +8,7 @@ */ export { getColorMode } from './color_mode'; +export { getThemeConfigByName, DEFAULT_THEME_CONFIG, type ThemeConfig } from './theme'; export type { KibanaTheme, ThemeServiceStart } from './types'; import type { KibanaTheme } from './types'; @@ -18,4 +19,5 @@ import type { KibanaTheme } from './types'; */ export const defaultTheme: KibanaTheme = { darkMode: false, + name: 'amsterdam', }; diff --git a/packages/react/kibana_context/common/kibana.jsonc b/packages/react/kibana_context/common/kibana.jsonc index 90fde56f05df6..b52bc6a40d0cc 100644 --- a/packages/react/kibana_context/common/kibana.jsonc +++ b/packages/react/kibana_context/common/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-common", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/common/theme.ts b/packages/react/kibana_context/common/theme.ts new file mode 100644 index 0000000000000..45c89172cf187 --- /dev/null +++ b/packages/react/kibana_context/common/theme.ts @@ -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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { EuiThemeSystem, EuiThemeAmsterdam } from '@elastic/eui'; + +export interface ThemeConfig { + euiTheme: EuiThemeSystem; +} + +const THEMES: Record = { + amsterdam: { + euiTheme: EuiThemeAmsterdam, + }, +}; + +export const getThemeConfigByName = (name: string): ThemeConfig | null => { + return THEMES[name as keyof typeof THEMES] || null; +}; + +export const DEFAULT_THEME_CONFIG = THEMES.amsterdam; diff --git a/packages/react/kibana_context/common/types.ts b/packages/react/kibana_context/common/types.ts index 2118c797d8be6..05e7ab7a0c7d5 100644 --- a/packages/react/kibana_context/common/types.ts +++ b/packages/react/kibana_context/common/types.ts @@ -20,6 +20,10 @@ import { Observable } from 'rxjs'; export interface KibanaTheme { /** is dark mode enabled or not */ readonly darkMode: boolean; + /** + * Name of the active theme + */ + readonly name: string; } // To avoid a circular dependency with the deprecation of `CoreThemeProvider`, diff --git a/packages/react/kibana_context/root/BUILD.bazel b/packages/react/kibana_context/root/BUILD.bazel new file mode 100644 index 0000000000000..1c47c25bc20a9 --- /dev/null +++ b/packages/react/kibana_context/root/BUILD.bazel @@ -0,0 +1,37 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//@elastic/eui", + "//packages/core/base/core-base-common", +] + +js_library( + name = "root", + package_name = "@kbn/react-kibana-context-root", + srcs = ["package.json"] + SRCS, + deps = DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/root/eui_provider.test.tsx b/packages/react/kibana_context/root/eui_provider.test.tsx index 069954b09fc60..d7486be2d4798 100644 --- a/packages/react/kibana_context/root/eui_provider.test.tsx +++ b/packages/react/kibana_context/root/eui_provider.test.tsx @@ -54,7 +54,7 @@ describe('KibanaEuiProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: KibanaTheme = { darkMode: true }; + const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -70,7 +70,7 @@ describe('KibanaEuiProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -83,7 +83,7 @@ describe('KibanaEuiProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_context/root/eui_provider.tsx b/packages/react/kibana_context/root/eui_provider.tsx index 74b2f6d8605ab..1e4e45c9f36f1 100644 --- a/packages/react/kibana_context/root/eui_provider.tsx +++ b/packages/react/kibana_context/root/eui_provider.tsx @@ -13,7 +13,12 @@ import createCache from '@emotion/cache'; import { EuiProvider, EuiProviderProps, euiStylisPrefixer } from '@elastic/eui'; import { EUI_STYLES_GLOBAL, EUI_STYLES_UTILS } from '@kbn/core-base-common'; -import { getColorMode, defaultTheme } from '@kbn/react-kibana-context-common'; +import { + getColorMode, + defaultTheme, + getThemeConfigByName, + DEFAULT_THEME_CONFIG, +} from '@kbn/react-kibana-context-common'; import { ThemeServiceStart } from '@kbn/react-kibana-context-common'; /** @@ -64,8 +69,13 @@ export const KibanaEuiProvider: FC> = modify, children, }) => { - const theme = useObservable(theme$, defaultTheme); - const themeColorMode = useMemo(() => getColorMode(theme), [theme]); + const kibanaTheme = useObservable(theme$, defaultTheme); + const themeColorMode = useMemo(() => getColorMode(kibanaTheme), [kibanaTheme]); + + const theme = useMemo(() => { + const config = getThemeConfigByName(kibanaTheme.name) || DEFAULT_THEME_CONFIG; + return config.euiTheme; + }, [kibanaTheme.name]); // In some cases-- like in Storybook or testing-- we want to explicitly override the // colorMode provided by the `theme`. @@ -76,7 +86,9 @@ export const KibanaEuiProvider: FC> = const globalStyles = globalStylesProp === false ? false : undefined; return ( - + {children} ); diff --git a/packages/react/kibana_context/root/kibana.jsonc b/packages/react/kibana_context/root/kibana.jsonc index 80a029c6ed487..740d92da927c9 100644 --- a/packages/react/kibana_context/root/kibana.jsonc +++ b/packages/react/kibana_context/root/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-root", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/root/root_provider.test.tsx b/packages/react/kibana_context/root/root_provider.test.tsx index df4bdf14c5c22..919adb09581d5 100644 --- a/packages/react/kibana_context/root/root_provider.test.tsx +++ b/packages/react/kibana_context/root/root_provider.test.tsx @@ -58,7 +58,7 @@ describe('KibanaRootContextProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: KibanaTheme = { darkMode: true }; + const coreTheme: KibanaTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_context/theme/BUILD.bazel b/packages/react/kibana_context/theme/BUILD.bazel new file mode 100644 index 0000000000000..504477ad7a0ed --- /dev/null +++ b/packages/react/kibana_context/theme/BUILD.bazel @@ -0,0 +1,38 @@ +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") + +SRCS = glob( + [ + "**/*.ts", + "**/*.tsx", + ], + exclude = [ + "**/test_helpers.ts", + "**/*.config.js", + "**/*.mock.*", + "**/*.test.*", + "**/*.stories.*", + "**/__snapshots__/**", + "**/integration_tests/**", + "**/mocks/**", + "**/scripts/**", + "**/storybook/**", + "**/test_fixtures/**", + "**/test_helpers/**", + ], +) + +BUNDLER_DEPS = [ + "@npm//react", + "@npm//tslib", + "@npm//react-use", + "//packages/react/kibana_context/common", + "//packages/react/kibana_context/root", +] + +js_library( + name = "theme", + package_name = "@kbn/react-kibana-context-theme", + srcs = ["package.json"] + SRCS, + deps = BUNDLER_DEPS, + visibility = ["//visibility:public"], +) diff --git a/packages/react/kibana_context/theme/kibana.jsonc b/packages/react/kibana_context/theme/kibana.jsonc index b3f8ba25cc5d3..56ae8b57a6682 100644 --- a/packages/react/kibana_context/theme/kibana.jsonc +++ b/packages/react/kibana_context/theme/kibana.jsonc @@ -1,5 +1,5 @@ { - "type": "shared-browser", + "type": "shared-common", "id": "@kbn/react-kibana-context-theme", "owner": "@elastic/appex-sharedux" } diff --git a/packages/react/kibana_context/theme/theme_provider.test.tsx b/packages/react/kibana_context/theme/theme_provider.test.tsx index a20af098d857d..9889da9a689a3 100644 --- a/packages/react/kibana_context/theme/theme_provider.test.tsx +++ b/packages/react/kibana_context/theme/theme_provider.test.tsx @@ -52,7 +52,7 @@ describe('KibanaThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -66,7 +66,10 @@ describe('KibanaThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ + darkMode: true, + name: 'amsterdam', + }); const wrapper = mountWithIntl( @@ -79,7 +82,7 @@ describe('KibanaThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/packages/react/kibana_mount/to_mount_point.test.tsx b/packages/react/kibana_mount/to_mount_point.test.tsx index 2232551877750..50a49263e2532 100644 --- a/packages/react/kibana_mount/to_mount_point.test.tsx +++ b/packages/react/kibana_mount/to_mount_point.test.tsx @@ -41,7 +41,7 @@ describe('toMountPoint', () => { }; it('exposes the euiTheme when `theme$` is provided', async () => { - const theme = { theme$: of({ darkMode: true }) }; + const theme = { theme$: of({ darkMode: true, name: 'amsterdam' }) }; const mount = toMountPoint(, { theme, i18n, analytics }); const targetEl = document.createElement('div'); @@ -53,7 +53,7 @@ describe('toMountPoint', () => { }); it('propagates changes of the theme$ observable', async () => { - const theme$ = new BehaviorSubject({ darkMode: true }); + const theme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const mount = toMountPoint(, { theme: { theme$ }, i18n, analytics }); @@ -65,7 +65,7 @@ describe('toMountPoint', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - theme$.next({ darkMode: false }); + theme$.next({ darkMode: false, name: 'amsterdam' }); }); await flushPromises(); diff --git a/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts index d91f31e243783..01c40ecc53d2d 100644 --- a/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts +++ b/src/plugins/chart_expressions/expression_partition_vis/public/__mocks__/theme.ts @@ -11,6 +11,6 @@ import { themeServiceMock } from '@kbn/core/public/mocks'; import { ThemeService } from '@kbn/charts-plugin/public/services'; const theme = new ThemeService(); -theme.init(themeServiceMock.createSetupContract({ darkMode: false })); +theme.init(themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' })); export { theme }; diff --git a/src/plugins/charts/public/services/theme/theme.test.tsx b/src/plugins/charts/public/services/theme/theme.test.tsx index ef8594e200dca..ef77405edf27e 100644 --- a/src/plugins/charts/public/services/theme/theme.test.tsx +++ b/src/plugins/charts/public/services/theme/theme.test.tsx @@ -19,7 +19,7 @@ import { ThemeService } from './theme'; import { coreMock } from '@kbn/core/public/mocks'; const createTheme$Mock = (mode: boolean) => { - return from([{ darkMode: mode }]); + return from([{ darkMode: mode, name: 'amsterdam' }]); }; const { theme: setUpMockTheme } = coreMock.createSetup(); @@ -37,6 +37,7 @@ describe('ThemeService', () => { expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toStrictEqual({ darkMode: false, + name: 'amsterdam', }); }); @@ -47,6 +48,7 @@ describe('ThemeService', () => { expect(await themeService.darkModeEnabled$.pipe(take(1)).toPromise()).toStrictEqual({ darkMode: true, + name: 'amsterdam', }); }); }); diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index f00d105444630..b00b5a95b3958 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -143,7 +143,7 @@ export function createDiscoverServicesMock(): DiscoverServices { }; const { profilesManagerMock } = createContextAwarenessMocks(); - const theme = themeServiceMock.createSetupContract({ darkMode: false }); + const theme = themeServiceMock.createSetupContract({ darkMode: false, name: 'amsterdam' }); corePluginMock.theme = theme; corePluginMock.chrome.getActiveSolutionNavId$.mockReturnValue(new BehaviorSubject(null)); diff --git a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx index 0de8d47398e51..1732ff51bf87d 100644 --- a/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx +++ b/src/plugins/guided_onboarding/public/components/guide_panel.test.tsx @@ -58,7 +58,7 @@ const setupComponentWithPluginStateMock = async ( }; const setupGuidePanelComponent = async (api: GuidedOnboardingApi) => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); let testBed: TestBed; const GuidePanelComponent = () => ( (undefined); const [guideConfig, setGuideConfig] = useState(undefined); const [isLoading, setIsLoading] = useState(false); - const { darkMode: isDarkTheme } = useObservable(theme$, { darkMode: false }); + const { darkMode: isDarkTheme } = useObservable(theme$, { darkMode: false, name: 'amsterdam' }); const styles = getGuidePanelStyles({ euiThemeContext, isDarkTheme }); diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index ef7820391273b..4d21adeecd377 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -73,7 +73,7 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => theme, } = services; const addBasePath = http.basePath.prepend; - const currentTheme = useObservable(theme.theme$, { darkMode: false }); + const currentTheme = useObservable(theme.theme$, { darkMode: false, name: 'amsterdam' }); // Home does not have a locator implemented, so hard-code it here. const addDataHref = addBasePath('/app/integrations/browse'); diff --git a/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx b/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx index 4c3e1d91b91b5..95b56ac659603 100644 --- a/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx +++ b/src/plugins/kibana_react/public/dark_mode/use_dark_mode.test.tsx @@ -37,7 +37,7 @@ describe('useDarkMode', () => { const mock = (): [KibanaServices, BehaviorSubject] => { const core = coreMock.createStart(); - const subject = new BehaviorSubject({ darkMode: false }); + const subject = new BehaviorSubject({ darkMode: false, name: 'amsterdam' }); core.theme.theme$ = subject.asObservable(); return [core, subject]; @@ -73,7 +73,7 @@ describe('useDarkMode', () => { expect(div!.textContent).toBe('false'); act(() => { - subject.next({ darkMode: true }); + subject.next({ darkMode: true, name: 'amsterdam' }); }); div = container!.querySelector('div'); diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index e3374219b6553..c4202cf6f92e2 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -272,6 +272,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'theme:name': { + type: 'keyword', + _meta: { description: 'Non-default value of setting.' }, + }, 'state:storeInSessionStorage': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b49647c3d4791..e4b10f038a75a 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -108,6 +108,7 @@ export interface UsageStats { 'timepicker:quickRanges': string; 'theme:version': string; 'theme:darkMode': boolean; + 'theme:name': string; 'state:storeInSessionStorage': boolean; 'savedObjects:perPage': number; 'search:queryLanguage': string; diff --git a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx index 27aefe1d43488..93511354b19ad 100644 --- a/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx +++ b/src/plugins/kibana_utils/public/theme/kibana_theme_provider.test.tsx @@ -52,7 +52,7 @@ describe('KibanaThemeProvider', () => { }; it('exposes the EUI theme provider', async () => { - const coreTheme: CoreTheme = { darkMode: true }; + const coreTheme: CoreTheme = { darkMode: true, name: 'amsterdam' }; const wrapper = mountWithIntl( @@ -66,7 +66,7 @@ describe('KibanaThemeProvider', () => { }); it('propagates changes of the coreTheme observable', async () => { - const coreTheme$ = new BehaviorSubject({ darkMode: true }); + const coreTheme$ = new BehaviorSubject({ darkMode: true, name: 'amsterdam' }); const wrapper = mountWithIntl( @@ -79,7 +79,7 @@ describe('KibanaThemeProvider', () => { expect(euiTheme!.colorMode).toEqual('DARK'); await act(async () => { - coreTheme$.next({ darkMode: false }); + coreTheme$.next({ darkMode: false, name: 'amsterdam' }); }); await refresh(wrapper); diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 6a43177492548..78f8b4f2f7b38 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -10272,6 +10272,12 @@ "description": "Non-default value of setting." } }, + "theme:name": { + "type": "keyword", + "_meta": { + "description": "Non-default value of setting." + } + }, "state:storeInSessionStorage": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/fleet/.storybook/context/index.tsx b/x-pack/plugins/fleet/.storybook/context/index.tsx index 67ed1c8aa6845..373dbc838835f 100644 --- a/x-pack/plugins/fleet/.storybook/context/index.tsx +++ b/x-pack/plugins/fleet/.storybook/context/index.tsx @@ -99,7 +99,7 @@ export const StorybookContext: React.FC<{ settings: getSettings(), theme: { theme$: EMPTY, - getTheme: () => ({ darkMode: false }), + getTheme: () => ({ darkMode: false, name: 'amsterdam' }), }, security: {} as unknown as SecurityServiceStart, userProfile: {} as unknown as UserProfileServiceStart, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx index 6c4a94d77a871..b32d7456bd2b5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/workspace_panel.tsx @@ -488,7 +488,10 @@ export const InnerWorkspacePanel = React.memo(function InnerWorkspacePanel({ } }, [suggestionForDraggedField, dispatchLens]); - const IS_DARK_THEME: boolean = useObservable(core.theme.theme$, { darkMode: false }).darkMode; + const IS_DARK_THEME: boolean = useObservable(core.theme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; const renderDragDropPrompt = () => { if (chartSizeSpec) { diff --git a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx index ec672d20f55da..0633c13b8097a 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/components/table_basic.tsx @@ -80,7 +80,10 @@ export const DatatableComponent = (props: DatatableRenderProps) => { const dataGridRef = useRef(null); const isInteractive = props.interactive; - const isDarkMode = useObservable(props.theme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(props.theme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; const [columnConfig, setColumnConfig] = useState({ columns: props.args.columns, diff --git a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx index 55dea2be2e370..efe5a39458eed 100644 --- a/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/datatable/visualization.tsx @@ -465,7 +465,10 @@ export const getDatatableVisualization = ({ }; }, DimensionEditorComponent(props) { - const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; return ( diff --git a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx index 661921caaa1ef..8d44fec4e96e4 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/partition/visualization.tsx @@ -494,7 +494,10 @@ export const getPieVisualization = ({ }; }, DimensionEditorComponent(props) { - const isDarkMode = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; return ; }, DimensionEditorDataExtraComponent(props) { diff --git a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx index 8d962ef076093..b457926fe373d 100644 --- a/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx +++ b/x-pack/plugins/lens/public/visualizations/tagcloud/tagcloud_visualization.tsx @@ -294,7 +294,10 @@ export const getTagcloudVisualization = ({ }, DimensionEditorComponent(props) { - const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { darkMode: false }).darkMode; + const isDarkMode: boolean = useObservable(kibanaTheme.theme$, { + darkMode: false, + name: 'amsterdam', + }).darkMode; if (props.groupId === TAG_GROUP_ID) { return ( l.layerId === props.layerId)!; const dimensionEditor = isReferenceLayer(layer) ? ( diff --git a/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts b/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts index e618e02b1b51e..93260405cce59 100644 --- a/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts +++ b/x-pack/plugins/observability_solution/infra/public/hooks/use_is_dark_mode.ts @@ -10,7 +10,7 @@ import useObservable from 'react-use/lib/useObservable'; import { of } from 'rxjs'; import { useKibanaContextForPlugin } from './use_kibana'; -const themeDefault: CoreTheme = { darkMode: false }; +const themeDefault: CoreTheme = { darkMode: false, name: 'amsterdam' }; export const useIsDarkMode = () => { const { services } = useKibanaContextForPlugin(); diff --git a/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx index dd0f97038740a..482602c87fd06 100644 --- a/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/infra/public/test_utils/use_global_storybook_theme.tsx @@ -53,8 +53,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx index dd0f97038740a..482602c87fd06 100644 --- a/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/logs_shared/public/test_utils/use_global_storybook_theme.tsx @@ -53,8 +53,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx index 7ed147138cce3..536cebb21a511 100644 --- a/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/metrics_data_access/public/test_utils/use_global_storybook_theme.tsx @@ -52,8 +52,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx index ea94e2edb6f8b..86e67681ab7af 100644 --- a/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx +++ b/x-pack/plugins/observability_solution/observability/public/test_utils/use_global_storybook_theme.tsx @@ -55,8 +55,8 @@ export const decorateWithGlobalStorybookThemeProviders: DecoratorFn = ( const euiThemeFromId = (themeId: string): CoreTheme => { switch (themeId) { case 'v8.dark': - return { darkMode: true }; + return { darkMode: true, name: 'amsterdam' }; default: - return { darkMode: false }; + return { darkMode: false, name: 'amsterdam' }; } }; diff --git a/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx index 593c2eafa920e..09740a5be71af 100644 --- a/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/observability/public/utils/kibana_react.storybook_decorator.tsx @@ -36,6 +36,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createTheme$Mock = () => { diff --git a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx index 8b6e951f9c97c..92a9f0b03a35a 100644 --- a/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx +++ b/x-pack/plugins/observability_solution/slo/public/utils/kibana_react.storybook_decorator.tsx @@ -27,6 +27,7 @@ export function KibanaReactStorybookDecorator(Story: ComponentType) { const mockTheme: CoreTheme = { darkMode: false, + name: 'amsterdam', }; const createTheme$Mock = () => { diff --git a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx index 01d6fc95afcc1..e78ed371468fc 100644 --- a/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx +++ b/x-pack/plugins/security/public/account_management/user_profile/user_profile.test.tsx @@ -318,7 +318,7 @@ describe('useUserProfileForm', () => { const data: UserProfileData = {}; const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false }); - coreStart.theme.getTheme.mockReturnValue({ darkMode: true }); + coreStart.theme.getTheme.mockReturnValue({ darkMode: true, name: 'amsterdam' }); coreStart.settings.client.isOverridden.mockReturnValue(true); const testWrapper = mount( @@ -354,7 +354,7 @@ describe('useUserProfileForm', () => { const data: UserProfileData = {}; const nonCloudUser = mockAuthenticatedUser({ elastic_cloud_user: false }); - coreStart.theme.getTheme.mockReturnValue({ darkMode: false }); + coreStart.theme.getTheme.mockReturnValue({ darkMode: false, name: 'amsterdam' }); coreStart.settings.client.isOverridden.mockReturnValue(true); const testWrapper = mount( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx index 5d9d126f1940d..12b1162082bfc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/toolbar/components/inspect/modal.test.tsx @@ -43,7 +43,7 @@ describe('Modal Inspect', () => { }; const renderModalInspectQuery = () => { - const theme = { theme$: of({ darkMode: false }) }; + const theme = { theme$: of({ darkMode: false, name: 'amsterdam' }) }; return render(, { wrapper: ({ children }) => ( {children} From c422bbd8daea29a65efcc94320d228354cb4e5e5 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 11:48:16 +0000 Subject: [PATCH 10/50] [Ownership] Assign test files to obs-ux-management-team (#200176) ## Summary Assign test files to obs-ux-management-team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2ca51c6debaef..6fef1bf948015 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1254,6 +1254,12 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/apps/infra/logs @elastic/obs-ux-logs-team # Observability UX management team +/x-pack/test/api_integration/services/data_view_api.ts @elastic/obs-ux-management-team +/x-pack/test/api_integration/services/slo.ts @elastic/obs-ux-management-team +/x-pack/test/functional/services/slo @elastic/obs-ux-management-team +/x-pack/test/functional/apps/slo @elastic/obs-ux-management-team +/x-pack/test/observability_api_integration @elastic/obs-ux-management-team # Assigned per https://github.com/elastic/kibana/pull/182243 +/x-pack/test/functional/services/observability @elastic/obs-ux-management-team /x-pack/test/api_integration/apis/slos @elastic/obs-ux-management-team /x-pack/test/accessibility/apps/group1/uptime.ts @elastic/obs-ux-management-team /x-pack/test/accessibility/apps/group3/observability.ts @elastic/obs-ux-management-team @@ -1264,7 +1270,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/**/test_suites/observability/custom_threshold_rule/ @elastic/obs-ux-management-team /x-pack/test_serverless/**/test_suites/observability/slos/ @elastic/obs-ux-management-team /x-pack/test_serverless/api_integration/test_suites/observability/es_query_rule @elastic/obs-ux-management-team -/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting @elastic/obs-ux-management-team +/x-pack/test/api_integration/deployment_agnostic/apis/observability/alerting/burn_rate_rule @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/services/alerting_api @elastic/obs-ux-management-team /x-pack/test/api_integration/deployment_agnostic/services/slo_api @elastic/obs-ux-management-team /x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team From 208877568aef623a28d560a296a45e0db0fe46fc Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 11:51:02 +0000 Subject: [PATCH 11/50] [Ownership] Assign test files to obs-ux-infra_services-team team (#200156) ## Summary Assign test files to obs-ux-infra_services-team team Contributes to: #192979 --- .github/CODEOWNERS | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6fef1bf948015..053b754d79ead 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1202,6 +1202,10 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai ## This should allow the infra team to work without dependencies on the @elastic/obs-ux-logs-team, which will maintain ownership of the Logs UI code only. ## infra/{common,docs,public,server}/{sub-folders}/ -> @elastic/obs-ux-infra_services-team +# obs-ux-infra_services-team +/x-pack/test/functional/page_objects/asset_details.ts @elastic/obs-ux-infra_services-team # Assigned per https://github.com/elastic/kibana/pull/200157/files/c83fae62151fe634342c498aca69b686b739fe45#r1842202022 +/x-pack/test/functional/page_objects/infra_* @elastic/obs-ux-infra_services-team +/x-pack/test/functional/es_archives/infra @elastic/obs-ux-infra_services-team /test/common/plugins/otel_metrics @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/common @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/docs @elastic/obs-ux-infra_services-team @@ -1223,8 +1227,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/plugins/observability_solution/infra/server/services @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/server/usage @elastic/obs-ux-infra_services-team /x-pack/plugins/observability_solution/infra/server/utils @elastic/obs-ux-infra_services-team -/x-pack/test/api_integration/deployment_agnostic/apis/observability/infra @elastic/obs-ux-logs-team -/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm @elastic/obs-ux-logs-team +/x-pack/test/api_integration/services/infraops_source_configuration.ts @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team # Assigned per https://github.com/elastic/kibana/pull/34916 ## Logs UI code exceptions -> @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team From 2af4a5343d95000ea7217398bf2dc169e7589f5c Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 11:54:13 +0000 Subject: [PATCH 12/50] [Ownership] Assign test files to obs-ux-logs team (#200157) ## Summary Assign test files to obs-ux-logs team Contributes to: #192979 --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 053b754d79ead..d8c9974700f63 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1230,6 +1230,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/api_integration/services/infraops_source_configuration.ts @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team # Assigned per https://github.com/elastic/kibana/pull/34916 ## Logs UI code exceptions -> @elastic/obs-ux-logs-team +/x-pack/test/upgrade/apps/logs @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_stream_log_file.ts @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/page_objects/svl_oblt_onboarding_page.ts @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/common/http_api/log_alerts @elastic/obs-ux-logs-team @@ -1251,6 +1252,8 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/plugins/observability_solution/infra/server/routes/log_alerts @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/server/routes/log_analysis @elastic/obs-ux-logs-team /x-pack/plugins/observability_solution/infra/server/services/rules @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +/x-pack/test/common/utils/synthtrace @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team # Assigned per https://github.com/elastic/kibana/blob/main/packages/kbn-apm-synthtrace/kibana.jsonc#L5 + # Infra Monitoring tests /x-pack/test/api_integration/apis/infra @elastic/obs-ux-infra_services-team /x-pack/test/functional/apps/infra @elastic/obs-ux-infra_services-team From 5adba07faa1a719a5e0e971a9be7d1aa4371b2b9 Mon Sep 17 00:00:00 2001 From: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Date: Mon, 18 Nov 2024 12:54:52 +0100 Subject: [PATCH 13/50] Migrate /test/apm_api_integration/tests/span_links to be deployment-agnostic API tests (#200140) closes #198990 part of https://github.com/elastic/kibana/issues/193245 ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) ### Checklist - [x] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] serverless - [x] stateful - [x] MKI --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/observability/apm/index.ts | 1 + .../apm}/span_links/data_generator.ts | 0 .../apis/observability/apm/span_links/index.ts | 14 ++++++++++++++ .../apm}/span_links/span_links.spec.ts | 15 ++++++++------- 4 files changed, 23 insertions(+), 7 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/span_links/data_generator.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/span_links/span_links.spec.ts (97%) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index a18edd23c509e..dcbf8edc4a755 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -34,5 +34,6 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./service_groups')); loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); + loadTestFile(require.resolve('./span_links')); }); } diff --git a/x-pack/test/apm_api_integration/tests/span_links/data_generator.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/data_generator.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/span_links/data_generator.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/data_generator.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/index.ts new file mode 100644 index 0000000000000..e7772daa131af --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('span_links', () => { + loadTestFile(require.resolve('./span_links.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/span_links.spec.ts similarity index 97% rename from x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/span_links.spec.ts index 871f44da4cdc1..5638d5d620d7b 100644 --- a/x-pack/test/apm_api_integration/tests/span_links/span_links.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/span_links/span_links.spec.ts @@ -7,23 +7,24 @@ import expect from '@kbn/expect'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateSpanLinksData } from './data_generator'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2022-01-01T00:00:00.000Z').getTime(); const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1; - // FLAKY: https://github.com/elastic/kibana/issues/177520 - registry.when('contains linked children', { config: 'basic', archives: [] }, () => { + describe('contains linked children', () => { let ids: ReturnType['ids']; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; before(async () => { const spanLinksData = generateSpanLinksData(); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); ids = spanLinksData.ids; From 71884eea0559866406a287f18fa8c675606c2e12 Mon Sep 17 00:00:00 2001 From: Krzysztof Kowalczyk Date: Mon, 18 Nov 2024 12:55:06 +0100 Subject: [PATCH 14/50] [Lens] Prefer pie over donut. Keep donut if donut already (#199735) ## Summary This PR makes it so `pie` is used over `donut` as default suggestion. If something is a `donut` already, adding another ring won't switch to `pie`. Closes: #101289 --- .../partition/suggestions.test.ts | 2 +- .../visualizations/partition/suggestions.ts | 17 +++++++++-------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts index 60a7aed1c9274..b848e0c44922e 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.test.ts @@ -668,7 +668,7 @@ describe('suggestions', () => { ).toContainEqual( expect.objectContaining({ state: { - shape: PieChartTypes.DONUT, + shape: PieChartTypes.PIE, palette, layers: [ { diff --git a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts index 0e66291c068da..4654ce73d8157 100644 --- a/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts +++ b/x-pack/plugins/lens/public/visualizations/partition/suggestions.ts @@ -39,20 +39,18 @@ function shouldReject({ table, keptLayerIds, state }: SuggestionRequest = { title: i18n.translate('xpack.lens.pie.suggestionLabel', { defaultMessage: '{chartName}', From 1945c86be9a16953bfc5781161d8f26d296f3fec Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 11:55:59 +0000 Subject: [PATCH 15/50] [Ownership] Assign sample data to core team (#200142) ## Summary Assign test files to platform docs team Contributes to: #192979 Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d8c9974700f63..d423560c02b5f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1095,6 +1095,7 @@ x-pack/test_serverless/api_integration/test_suites/common/platform_security @ela src/plugins/discover/public/context_awareness/profile_providers/security @elastic/kibana-data-discovery @elastic/security-threat-hunting-investigations # Platform Docs +/x-pack/test/functional/services/sample_data @elastic/platform-docs /x-pack/test_serverless/functional/test_suites/security/screenshot_creation/index.ts @elastic/platform-docs /x-pack/test_serverless/functional/test_suites/security/config.screenshots.ts @elastic/platform-docs /x-pack/test/screenshot_creation @elastic/platform-docs From fccb8b11513559075e3e1aa9b8f90e724fe5fe37 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 12:00:11 +0000 Subject: [PATCH 16/50] [Ownership] Assign test files to ops team (#200217) ## Summary Assign test files to ops team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d423560c02b5f..d06c00dfec607 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1410,6 +1410,9 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai #CC# /x-pack/plugins/file_upload @elastic/kibana-gis # Operations +/test/package @elastic/kibana-operations +/test/package/roles @elastic/kibana-operations +/test/common/fixtures/plugins/coverage/kibana.json @elastic/kibana-operations /src/dev/license_checker/config.ts @elastic/kibana-operations /src/dev/ @elastic/kibana-operations /src/setup_node_env/ @elastic/kibana-operations From 6c59e38ad14eca80ee055a5e9302d4f67864bc94 Mon Sep 17 00:00:00 2001 From: Jan Monschke Date: Mon, 18 Nov 2024 13:17:57 +0100 Subject: [PATCH 17/50] [SecuritySolution][Timeline] Remove timeline.isLoading (#198616) ## Summary Trying to answer the question of: "Do we still need `timeline.isLoading`?" `timeline.isSaving` should be the only indicator for "loading" states of timeline itself. All other pieces of state that are associated with timeline that could have a loading state, have their own loading indicators (e.g. data providers, alert list etc). Therefore, this PR removes all references to `timeline.isLoading` and parts of the UI that depended on it. Places that `timeline.isLoading` was used ([context](https://github.com/elastic/kibana/pull/38185)): - Blocking drag/drop on data providers - This is not necessary anymore. Drag/drop works while the underlying query is being executed. - Showing a loading state for the alerts table & data provider changes - Both components have their own loading state, so no extra loading state is necessary ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Elastic Machine --- .../drag_and_drop/droppable_wrapper.tsx | 12 +- .../public/common/mock/global_state.ts | 1 - .../public/common/mock/timeline_results.ts | 2 - .../components/alerts_table/actions.test.tsx | 48 +--- .../components/alerts_table/actions.tsx | 3 - .../use_add_bulk_to_timeline.tsx | 10 +- .../use_investigate_in_timeline.tsx | 15 +- .../components/alerts_table/types.ts | 3 - .../rules/use_rule_from_timeline.test.ts | 20 +- .../use_investigate_in_timeline.ts | 12 +- .../components/open_timeline/helpers.test.ts | 53 ----- .../components/open_timeline/helpers.ts | 15 -- .../timeline/data_providers/index.tsx | 5 +- .../data_providers/provider_item_actions.tsx | 12 +- .../data_providers/provider_item_badge.tsx | 15 +- .../data_providers/providers.test.tsx | 214 +----------------- .../timelines/components/timeline/index.tsx | 1 - .../components/timeline/tabs/eql/index.tsx | 16 +- .../components/timeline/tabs/query/index.tsx | 9 - .../public/timelines/store/actions.ts | 5 - .../public/timelines/store/defaults.ts | 1 - .../public/timelines/store/helpers.test.ts | 1 - .../public/timelines/store/helpers.ts | 2 - .../store/middlewares/timeline_save.test.ts | 1 - .../public/timelines/store/model.ts | 2 - .../public/timelines/store/reducer.ts | 11 - .../cypress/screens/timeline.ts | 2 - .../cypress/tasks/alerts.ts | 4 - 28 files changed, 15 insertions(+), 480 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx index 586165893408c..d14a651a027a9 100644 --- a/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx +++ b/x-pack/plugins/security_solution/public/common/components/drag_and_drop/droppable_wrapper.tsx @@ -15,7 +15,6 @@ interface Props { children?: React.ReactNode; droppableId: string; height?: string; - isDropDisabled?: boolean; type?: string; render?: ({ isDraggingOver }: { isDraggingOver: boolean }) => React.ReactNode; renderClone?: DraggableChildrenFn; @@ -90,15 +89,7 @@ const ReactDndDropTarget = styled.div<{ isDraggingOver: boolean; height: string ReactDndDropTarget.displayName = 'ReactDndDropTarget'; export const DroppableWrapper = React.memo( - ({ - children = null, - droppableId, - height = '100%', - isDropDisabled = false, - type, - render = null, - renderClone, - }) => { + ({ children = null, droppableId, height = '100%', type, render = null, renderClone }) => { const DroppableContent = useCallback( (provided, snapshot) => ( ( return ( { const anchor = '2020-03-01T17:59:46.349Z'; const unix = moment(anchor).valueOf(); let createTimeline: CreateTimeline; - let updateTimelineIsLoading: UpdateTimelineLoading; let searchStrategyClient: jest.Mocked; let clock: sinon.SinonFakeTimers; let mockKibanaServices: jest.Mock; @@ -270,7 +269,6 @@ describe('alert actions', () => { mockGetExceptionFilter = jest.fn().mockResolvedValue(undefined); createTimeline = jest.fn() as jest.Mocked; - updateTimelineIsLoading = jest.fn() as jest.Mocked; mockKibanaServices = KibanaServices.get as jest.Mock; fetchMock = jest.fn(); @@ -296,28 +294,10 @@ describe('alert actions', () => { describe('sendAlertToTimelineAction', () => { describe('timeline id is NOT empty string and apollo client exists', () => { - test('it invokes updateTimelineIsLoading to set to true', async () => { - await sendAlertToTimelineAction({ - createTimeline, - ecsData: mockEcsDataWithAlert, - updateTimelineIsLoading, - searchStrategyClient, - getExceptionFilter: mockGetExceptionFilter, - }); - - expect(mockGetExceptionFilter).not.toHaveBeenCalled(); - expect(updateTimelineIsLoading).toHaveBeenCalledTimes(1); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - }); - test('it invokes createTimeline with designated timeline template if "timelineTemplate" exists', async () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -407,7 +387,6 @@ describe('alert actions', () => { indexNames: [], isFavorite: false, isLive: false, - isLoading: false, isSaving: false, isSelectAllChecked: false, itemsPerPage: 25, @@ -477,7 +456,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -496,7 +474,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: mockEcsDataWithAlert, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -505,14 +482,6 @@ describe('alert actions', () => { delete defaultTimelinePropsWithoutNote.ruleNote; delete defaultTimelinePropsWithoutNote.ruleAuthor; - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - expect(updateTimelineIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: false, - }); expect(mockGetExceptionFilter).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ @@ -544,7 +513,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -552,7 +520,6 @@ describe('alert actions', () => { const expectedTimelineProps = structuredClone(defaultTimelineProps); expectedTimelineProps.timeline.excludedRowRendererIds = []; - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); expect(mockGetExceptionFilter).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps); @@ -574,7 +541,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -582,7 +548,6 @@ describe('alert actions', () => { const expectedTimelineProps = structuredClone(defaultTimelineProps); expectedTimelineProps.timeline.excludedRowRendererIds = []; - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); expect(mockGetExceptionFilter).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps); @@ -608,12 +573,10 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); expect(mockGetExceptionFilter).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ @@ -655,12 +618,10 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMock, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); expect(mockGetExceptionFilter).not.toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith(expectedTimelineProps); @@ -732,7 +693,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMockWithNoTemplateTimeline, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -740,7 +700,6 @@ describe('alert actions', () => { const expectedFrom = '2021-01-10T21:11:45.839Z'; const expectedTo = '2021-01-10T21:12:45.839Z'; - expect(updateTimelineIsLoading).not.toHaveBeenCalled(); expect(mockGetExceptionFilter).toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ @@ -861,7 +820,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMockWithNoTemplateTimelineAndNoFilters, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -886,7 +844,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMockWithTemplateTimeline, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -894,7 +851,6 @@ describe('alert actions', () => { const expectedFrom = '2021-01-10T21:11:45.839Z'; const expectedTo = '2021-01-10T21:12:45.839Z'; - expect(updateTimelineIsLoading).toHaveBeenCalled(); expect(mockGetExceptionFilter).toHaveBeenCalled(); expect(createTimeline).toHaveBeenCalledTimes(1); expect(createTimeline).toHaveBeenCalledWith({ @@ -1046,7 +1002,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMockWithNoTemplateTimeline, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); @@ -1141,7 +1096,6 @@ describe('alert actions', () => { await sendAlertToTimelineAction({ createTimeline, ecsData: ecsDataMockWithNoTemplateTimeline, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter: mockGetExceptionFilter, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx index a2dfef2c43e9f..3a3e0d0255531 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/actions.tsx @@ -936,7 +936,6 @@ export const sendBulkEventsToTimelineAction = async ( export const sendAlertToTimelineAction = async ({ createTimeline, ecsData: ecs, - updateTimelineIsLoading, searchStrategyClient, getExceptionFilter, }: SendAlertToTimelineActionProps) => { @@ -962,7 +961,6 @@ export const sendAlertToTimelineAction = async ({ // For now we do not want to populate the template timeline if we have alertIds if (!isEmpty(timelineId)) { try { - updateTimelineIsLoading({ id: TimelineId.active, isLoading: true }); const [responseTimeline, eventDataResp] = await Promise.all([ getTimelineTemplate(timelineId), lastValueFrom( @@ -1092,7 +1090,6 @@ export const sendAlertToTimelineAction = async ({ } catch (error) { /* eslint-disable-next-line no-console */ console.error(error); - updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); return createTimeline({ from, notes: null, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx index 0086f40ffa44b..e402dfe2488fa 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_add_bulk_to_timeline.tsx @@ -23,7 +23,6 @@ import { useTimelineEventsHandler } from '../../../../timelines/containers'; import { eventsViewerSelector } from '../../../../common/components/events_viewer/selectors'; import type { State } from '../../../../common/store/types'; import { useUpdateTimeline } from '../../../../timelines/components/open_timeline/use_update_timeline'; -import { timelineActions } from '../../../../timelines/store'; import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline'; import { INVESTIGATE_BULK_IN_TIMELINE } from '../translations'; import { TimelineId } from '../../../../../common/types/timeline'; @@ -141,18 +140,11 @@ export const useAddBulkToTimelineAction = ({ timelineType: TimelineTypeEnum.default, }); - const updateTimelineIsLoading = useCallback( - (payload: Parameters[0]) => - dispatch(timelineActions.updateIsLoading(payload)), - [dispatch] - ); - const updateTimeline = useUpdateTimeline(); const createTimeline = useCallback( async ({ timeline, ruleNote, timeline: { filters: eventIdFilters } }: CreateTimelineProps) => { await clearActiveTimeline(); - updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); updateTimeline({ duplicate: true, from, @@ -168,7 +160,7 @@ export const useAddBulkToTimelineAction = ({ ruleNote, }); }, - [updateTimeline, updateTimelineIsLoading, clearActiveTimeline, from, to] + [updateTimeline, clearActiveTimeline, from, to] ); const sendBulkEventsToTimelineHandler = useCallback( diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx index d7df06616f221..3b36452f9315d 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_investigate_in_timeline.tsx @@ -5,7 +5,6 @@ * 2.0. */ import { useCallback, useMemo } from 'react'; -import { useDispatch } from 'react-redux'; import { i18n } from '@kbn/i18n'; import { ALERT_RULE_EXCEPTIONS_LIST, ALERT_RULE_PARAMETERS } from '@kbn/rule-data-utils'; @@ -23,7 +22,6 @@ import { createHistoryEntry } from '../../../../common/utils/global_query_string import { useKibana } from '../../../../common/lib/kibana'; import { TimelineId } from '../../../../../common/types/timeline'; import { TimelineTypeEnum } from '../../../../../common/api/timeline'; -import { timelineActions } from '../../../../timelines/store'; import { sendAlertToTimelineAction } from '../actions'; import { useUpdateTimeline } from '../../../../timelines/components/open_timeline/use_update_timeline'; import { useCreateTimeline } from '../../../../timelines/hooks/use_create_timeline'; @@ -98,7 +96,6 @@ export const useInvestigateInTimeline = ({ const { data: { search: searchStrategyClient }, } = useKibana().services; - const dispatch = useDispatch(); const { startTransaction } = useStartTransaction(); const { services } = useKibana(); @@ -133,12 +130,6 @@ export const useInvestigateInTimeline = ({ [addError, getExceptionFilterFromIds] ); - const updateTimelineIsLoading = useCallback( - (payload: Parameters[0]) => - dispatch(timelineActions.updateIsLoading(payload)), - [dispatch] - ); - const clearActiveTimeline = useCreateTimeline({ timelineId: TimelineId.active, timelineType: TimelineTypeEnum.default, @@ -153,7 +144,6 @@ export const useInvestigateInTimeline = ({ !newColumns || isEmpty(newColumns) ? defaultUdtHeaders : newColumns; await clearActiveTimeline(); - updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); updateTimeline({ duplicate: true, from: fromTimeline, @@ -173,12 +163,11 @@ export const useInvestigateInTimeline = ({ ruleNote, }); }, - [updateTimeline, updateTimelineIsLoading, clearActiveTimeline] + [updateTimeline, clearActiveTimeline] ); const investigateInTimelineAlertClick = useCallback(async () => { createHistoryEntry(); - startTransaction({ name: ALERTS_ACTIONS.INVESTIGATE_IN_TIMELINE }); if (onInvestigateInTimelineAlertClick) { onInvestigateInTimelineAlertClick(); @@ -188,7 +177,6 @@ export const useInvestigateInTimeline = ({ createTimeline, ecsData: ecsRowData, searchStrategyClient, - updateTimelineIsLoading, getExceptionFilter, }); } @@ -198,7 +186,6 @@ export const useInvestigateInTimeline = ({ ecsRowData, onInvestigateInTimelineAlertClick, searchStrategyClient, - updateTimelineIsLoading, getExceptionFilter, ]); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts index 167601871ae2a..53deaf4145310 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/types.ts @@ -55,13 +55,10 @@ export interface UpdateAlertStatusActionProps { export interface SendAlertToTimelineActionProps { createTimeline: CreateTimeline; ecsData: Ecs | Ecs[]; - updateTimelineIsLoading: UpdateTimelineLoading; searchStrategyClient: ISearchStart; getExceptionFilter: GetExceptionFilter; } -export type UpdateTimelineLoading = ({ id, isLoading }: { id: string; isLoading: boolean }) => void; - export interface CreateTimelineProps { from: string; timeline: TimelineModel; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.test.ts index ce653c82d7831..57a0aa43cdde6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_from_timeline.test.ts @@ -121,7 +121,6 @@ describe('useRuleFromTimeline', () => { expect(result.current.loading).toEqual(true); await waitForNextUpdate(); expect(setRuleQuery).toHaveBeenCalled(); - expect(mockDispatch).toHaveBeenCalledTimes(2); }); }); @@ -153,16 +152,8 @@ describe('useRuleFromTimeline', () => { await waitForNextUpdate(); expect(setRuleQuery).toHaveBeenCalled(); - expect(mockDispatch).toHaveBeenCalledTimes(4); + expect(mockDispatch).toHaveBeenCalledTimes(2); expect(mockDispatch).toHaveBeenNthCalledWith(1, { - type: 'x-pack/security_solution/local/timeline/UPDATE_LOADING', - payload: { - id: 'timeline-1', - isLoading: true, - }, - }); - - expect(mockDispatch).toHaveBeenNthCalledWith(2, { type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', payload: { id: 'timeline', @@ -170,13 +161,6 @@ describe('useRuleFromTimeline', () => { selectedPatterns: selectedTimeline.data.timeline.indexNames, }, }); - expect(mockDispatch).toHaveBeenNthCalledWith(3, { - type: 'x-pack/security_solution/local/timeline/UPDATE_LOADING', - payload: { - id: 'timeline-1', - isLoading: false, - }, - }); }); it('when from timeline data view id === selected data view id and browser fields is not empty, set rule data to match from timeline query', async () => { @@ -347,7 +331,7 @@ describe('useRuleFromTimeline', () => { const { waitForNextUpdate } = renderHook(() => useRuleFromTimeline(setRuleQuery)); await waitForNextUpdate(); expect(setRuleQuery).toHaveBeenCalled(); - expect(mockDispatch).toHaveBeenNthCalledWith(4, { + expect(mockDispatch).toHaveBeenNthCalledWith(2, { type: 'x-pack/security_solution/local/sourcerer/SET_SELECTED_DATA_VIEW', payload: { id: 'timeline', diff --git a/x-pack/plugins/security_solution/public/threat_intelligence/use_investigate_in_timeline.ts b/x-pack/plugins/security_solution/public/threat_intelligence/use_investigate_in_timeline.ts index 85ced6d6e153d..f840e65497cc4 100644 --- a/x-pack/plugins/security_solution/public/threat_intelligence/use_investigate_in_timeline.ts +++ b/x-pack/plugins/security_solution/public/threat_intelligence/use_investigate_in_timeline.ts @@ -6,14 +6,12 @@ */ import { useCallback } from 'react'; -import { useDispatch } from 'react-redux'; import { timelineDefaults } from '../timelines/store/defaults'; import { APP_UI_ID } from '../../common/constants'; import type { DataProvider } from '../../common/types'; import { TimelineId } from '../../common/types/timeline'; import { TimelineTypeEnum } from '../../common/api/timeline'; import { useStartTransaction } from '../common/lib/apm/use_start_transaction'; -import { timelineActions } from '../timelines/store'; import { useCreateTimeline } from '../timelines/hooks/use_create_timeline'; import type { CreateTimelineProps } from '../detections/components/alerts_table/types'; import { useUpdateTimeline } from '../timelines/components/open_timeline/use_update_timeline'; @@ -46,15 +44,8 @@ export const useInvestigateInTimeline = ({ from, to, }: UseInvestigateInTimelineActionProps) => { - const dispatch = useDispatch(); const { startTransaction } = useStartTransaction(); - const updateTimelineIsLoading = useCallback( - (payload: Parameters[0]) => - dispatch(timelineActions.updateIsLoading(payload)), - [dispatch] - ); - const clearActiveTimeline = useCreateTimeline({ timelineId: TimelineId.active, timelineType: TimelineTypeEnum.default, @@ -65,7 +56,6 @@ export const useInvestigateInTimeline = ({ const createTimeline = useCallback( async ({ from: fromTimeline, timeline, to: toTimeline, ruleNote }: CreateTimelineProps) => { await clearActiveTimeline(); - updateTimelineIsLoading({ id: TimelineId.active, isLoading: false }); updateTimeline({ duplicate: true, from: fromTimeline, @@ -80,7 +70,7 @@ export const useInvestigateInTimeline = ({ ruleNote, }); }, - [updateTimeline, updateTimelineIsLoading, clearActiveTimeline] + [updateTimeline, clearActiveTimeline] ); const investigateInTimelineClick = useCallback(async () => { diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts index 917f1d1bc29db..525d8bba3d909 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.test.ts @@ -11,7 +11,6 @@ import { waitFor } from '@testing-library/react'; import { mockTimelineResults, mockGetOneTimelineResult } from '../../../common/mock'; import { timelineDefaults } from '../../store/defaults'; -import { updateIsLoading as dispatchUpdateIsLoading } from '../../store/actions'; import type { QueryTimelineById } from './helpers'; import { defaultTimelineToTimelineModel, @@ -646,13 +645,6 @@ describe('helpers', () => { jest.clearAllMocks(); }); - test('dispatch updateIsLoading to true', () => { - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - }); - test('get timeline by Id', () => { expect(resolveTimeline).toHaveBeenCalled(); }); @@ -671,13 +663,6 @@ describe('helpers', () => { ...timeline, }); }); - - test('dispatch updateIsLoading to false', () => { - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: false, - }); - }); }); describe('update a timeline', () => { @@ -706,11 +691,6 @@ describe('helpers', () => { await queryTimelineById(args); }); - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - // expect(resolveTimeline).toHaveBeenCalled(); const { timeline } = formatTimelineResponseToModel( omitTypenameInTimeline(getOr({}, 'data.timeline', selectedTimeline)), @@ -741,11 +721,6 @@ describe('helpers', () => { }, }); }); - - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: false, - }); }); test('should update timeline correctly when timeline is untitled', async () => { @@ -764,11 +739,6 @@ describe('helpers', () => { queryTimelineById(newArgs); }); - expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - expect(mockUpdateTimeline).toHaveBeenNthCalledWith( 1, expect.objectContaining({ @@ -778,10 +748,6 @@ describe('helpers', () => { }), }) ); - expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: false, - }); }); test('should update timeline correctly when timeline is already saved and onOpenTimeline is not provided', async () => { @@ -791,11 +757,6 @@ describe('helpers', () => { queryTimelineById(args); }); - expect(dispatchUpdateIsLoading).toHaveBeenCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - await waitFor(() => { expect(mockUpdateTimeline).toHaveBeenNthCalledWith( 1, @@ -860,13 +821,6 @@ describe('helpers', () => { jest.clearAllMocks(); }); - test('dispatch updateIsLoading to true', () => { - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: true, - }); - }); - test('get timeline by Id', () => { expect(resolveTimeline).toHaveBeenCalled(); }); @@ -885,13 +839,6 @@ describe('helpers', () => { }, }); }); - - test('dispatch updateIsLoading to false', () => { - expect(dispatchUpdateIsLoading).toBeCalledWith({ - id: TimelineId.active, - isLoading: false, - }); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts index e9c1d85b9049e..d3ca6c4654ff3 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/components/open_timeline/helpers.ts @@ -9,8 +9,6 @@ import { set } from '@kbn/safer-lodash-set/fp'; import { getOr } from 'lodash/fp'; import { v4 as uuidv4 } from 'uuid'; import deepMerge from 'deepmerge'; -import { useDispatch } from 'react-redux'; -import { useCallback } from 'react'; import { useDiscoverInTimelineContext } from '../../../common/components/discover_in_timeline/use_discover_in_timeline_context'; import type { ColumnHeaderOptions } from '../../../../common/types/timeline'; import type { @@ -49,7 +47,6 @@ import { DEFAULT_TO_MOMENT, } from '../../../common/utils/default_date_settings'; import { resolveTimeline } from '../../containers/api'; -import { timelineActions } from '../../store'; export const OPEN_TIMELINE_CLASS_NAME = 'open-timeline'; @@ -314,13 +311,6 @@ export interface QueryTimelineById { export const useQueryTimelineById = () => { const { resetDiscoverAppState } = useDiscoverInTimelineContext(); const updateTimeline = useUpdateTimeline(); - const dispatch = useDispatch(); - - const updateIsLoading = useCallback( - (status: { id: string; isLoading: boolean }) => - dispatch(timelineActions.updateIsLoading(status)), - [dispatch] - ); return ({ activeTimelineTab = TimelineTabs.query, @@ -333,7 +323,6 @@ export const useQueryTimelineById = () => { openTimeline = true, savedSearchId, }: QueryTimelineById) => { - updateIsLoading({ id: TimelineId.active, isLoading: true }); if (timelineId == null) { updateTimeline({ id: TimelineId.active, @@ -356,7 +345,6 @@ export const useQueryTimelineById = () => { }, }); resetDiscoverAppState(); - updateIsLoading({ id: TimelineId.active, isLoading: false }); } else { return Promise.resolve(resolveTimeline(timelineId)) .then((result) => { @@ -409,9 +397,6 @@ export const useQueryTimelineById = () => { if (onError != null) { onError(error, timelineId); } - }) - .finally(() => { - updateIsLoading({ id: TimelineId.active, isLoading: false }); }); } }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx index bcdf750d114f0..7a90b5254e445 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/index.tsx @@ -109,9 +109,6 @@ export const DataProviders = React.memo(({ timelineId }) => { const { browserFields } = useSourcererDataView(SourcererScopeName.timeline); const getTimeline = useMemo(() => timelineSelectors.getTimelineByIdSelector(), []); - const isLoading = useDeepEqualSelector( - (state) => (getTimeline(state, timelineId) ?? timelineDefaults).isLoading - ); const dataProviders = useDeepEqualSelector( (state) => (getTimeline(state, timelineId) ?? timelineDefaults).dataProviders ); @@ -167,7 +164,7 @@ export const DataProviders = React.memo(({ timelineId }) => { dataProviders={dataProviders} /> ) : ( - + )} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx index a5bef9b83551b..39d765fdd1f61 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_actions.tsx @@ -44,7 +44,6 @@ interface OwnProps { kqlQuery: string; // eslint-disable-line react/no-unused-prop-types isEnabled: boolean; isExcluded: boolean; - isLoading: boolean; isOpen: boolean; onDataProviderEdited?: OnDataProviderEdited; operator: QueryOperator; @@ -77,7 +76,6 @@ interface GetProviderActionsProps { field: string; isEnabled: boolean; isExcluded: boolean; - isLoading: boolean; onDataProviderEdited?: OnDataProviderEdited; onFilterForFieldPresent: () => void; operator: QueryOperator; @@ -98,7 +96,6 @@ export const getProviderActions = ({ field, isEnabled, isExcluded, - isLoading, operator, onDataProviderEdited, onFilterForFieldPresent, @@ -116,28 +113,24 @@ export const getProviderActions = ({ items: [ { className: EDIT_CLASS_NAME, - disabled: isLoading, icon: 'pencil', name: i18n.EDIT_MENU_ITEM, panel: 1, }, { className: EXCLUDE_CLASS_NAME, - disabled: isLoading, icon: `${isExcluded ? 'plusInCircle' : 'minusInCircle'}`, name: isExcluded ? i18n.INCLUDE_DATA_PROVIDER : i18n.EXCLUDE_DATA_PROVIDER, onClick: toggleExcluded, }, { className: ENABLE_CLASS_NAME, - disabled: isLoading, icon: `${isEnabled ? 'eyeClosed' : 'eye'}`, name: isEnabled ? i18n.TEMPORARILY_DISABLE_DATA_PROVIDER : i18n.RE_ENABLE_DATA_PROVIDER, onClick: toggleEnabled, }, { className: FILTER_FOR_FIELD_PRESENT_CLASS_NAME, - disabled: isLoading, icon: 'logstashFilter', name: i18n.FILTER_FOR_FIELD_PRESENT, onClick: onFilterForFieldPresent, @@ -145,7 +138,7 @@ export const getProviderActions = ({ timelineType === TimelineTypeEnum.template ? { className: CONVERT_TO_FIELD_CLASS_NAME, - disabled: isLoading || operator === IS_ONE_OF_OPERATOR, + disabled: operator === IS_ONE_OF_OPERATOR, icon: 'visText', name: type === DataProviderTypeEnum.template @@ -156,7 +149,6 @@ export const getProviderActions = ({ : { name: null }, { className: DELETE_CLASS_NAME, - disabled: isLoading, icon: 'trash', name: i18n.DELETE_DATA_PROVIDER, onClick: deleteItem, @@ -196,7 +188,6 @@ export class ProviderItemActions extends React.PureComponent { field, isEnabled, isExcluded, - isLoading, isOpen, operator, providerId, @@ -216,7 +207,6 @@ export class ProviderItemActions extends React.PureComponent { field, isEnabled, isExcluded, - isLoading, onDataProviderEdited: this.onDataProviderEdited, onFilterForFieldPresent: this.onFilterForFieldPresent, operator, diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx index 5f6f567bf32c7..53d43689a2706 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/provider_item_badge.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { useDispatch } from 'react-redux'; @@ -15,10 +14,7 @@ import { TimelineTypeEnum, } from '../../../../../common/api/timeline'; import type { BrowserFields } from '../../../../common/containers/source'; -import { - useDeepEqualSelector, - useShallowEqualSelector, -} from '../../../../common/hooks/use_selector'; +import { useShallowEqualSelector } from '../../../../common/hooks/use_selector'; import { timelineSelectors } from '../../../store'; import type { PrimitiveOrArrayOfPrimitives } from '../../../../common/lib/kuery'; @@ -27,7 +23,6 @@ import { ProviderBadge } from './provider_badge'; import { ProviderItemActions } from './provider_item_actions'; import type { DataProvidersAnd, QueryOperator } from './data_provider'; import { dragAndDropActions } from '../../../../common/store/drag_and_drop'; -import { timelineDefaults } from '../../../store/defaults'; interface ProviderItemBadgeProps { andProviderId?: string; @@ -86,10 +81,6 @@ export const ProviderItemBadge = React.memo( return getTimeline(state, timelineId)?.timelineType ?? TimelineTypeEnum.default; }); - const { isLoading } = useDeepEqualSelector( - (state) => getTimeline(state, timelineId ?? '') ?? timelineDefaults - ); - const togglePopover = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); }, [isPopoverOpen, setIsPopoverOpen]); @@ -142,7 +133,7 @@ export const ProviderItemBadge = React.memo( const button = useMemo( () => ( ( field, isEnabled, isExcluded, - isLoading, kqlQuery, onToggleTypeProvider, operator, @@ -186,7 +176,6 @@ export const ProviderItemBadge = React.memo( kqlQuery={kqlQuery} isEnabled={isEnabled} isExcluded={isExcluded} - isLoading={isLoading} isOpen={isPopoverOpen} onDataProviderEdited={onDataProviderEdited} operator={operator} diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx index d6492c0d864b0..dbb073ddecc80 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/data_providers/providers.test.tsx @@ -32,7 +32,7 @@ describe('Providers', () => { beforeEach(() => { jest.clearAllMocks(); - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: false }); + (useDeepEqualSelector as jest.Mock).mockReturnValue({}); }); describe('rendering', () => { @@ -88,28 +88,6 @@ describe('Providers', () => { expect(mockOnDataProviderRemoved.mock.calls[0][0].providerId).toEqual('id-Provider 1'); }); - test('while loading data, it does NOT invoke the onDataProviderRemoved callback when the close button is clicked', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const wrapper = mount( - - - - - - ); - - wrapper - .find('[data-test-subj="providerBadge"] [data-euiicon-type]') - .first() - .simulate('click'); - - expect(mockOnDataProviderRemoved).not.toBeCalled(); - }); - test('it invokes the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { const wrapper = mount( @@ -132,31 +110,6 @@ describe('Providers', () => { .simulate('click'); expect(mockOnDataProviderRemoved.mock.calls[0][0].providerId).toEqual('id-Provider 1'); }); - - test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the option "Delete" in the provider menu', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const wrapper = mount( - - - - - - ); - wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click'); - - wrapper.update(); - - wrapper - .find(`[data-test-subj="providerActions"] .${DELETE_CLASS_NAME}`) - .first() - .simulate('click'); - - expect(mockOnDataProviderRemoved).not.toBeCalled(); - }); }); describe('#onToggleDataProviderEnabled', () => { @@ -191,35 +144,6 @@ describe('Providers', () => { providerId: 'id-Provider 1', }); }); - - test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const mockOnToggleDataProviderEnabled = jest.spyOn( - timelineActions, - 'updateDataProviderEnabled' - ); - const wrapper = mount( - - - - - - ); - - wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click'); - wrapper.update(); - - wrapper - .find(`[data-test-subj="providerActions"] .${ENABLE_CLASS_NAME}`) - .first() - .simulate('click'); - - expect(mockOnToggleDataProviderEnabled).not.toBeCalled(); - }); }); describe('#onToggleDataProviderExcluded', () => { @@ -257,37 +181,6 @@ describe('Providers', () => { providerId: 'id-Provider 1', }); }); - - test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const mockOnToggleDataProviderExcluded = jest.spyOn( - timelineActions, - 'updateDataProviderExcluded' - ); - - const wrapper = mount( - - - - - - ); - - wrapper.find('button[data-test-subj="providerBadge"]').first().simulate('click'); - - wrapper.update(); - - wrapper - .find(`[data-test-subj="providerActions"] .${EXCLUDE_CLASS_NAME}`) - .first() - .simulate('click'); - - expect(mockOnToggleDataProviderExcluded).not.toBeCalled(); - }); }); describe('#ProviderWithAndProvider', () => { @@ -349,35 +242,6 @@ describe('Providers', () => { }); }); - test('while loading data, it does NOT invoke the onDataProviderRemoved callback when you click on the close button is clicked', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - - const wrapper = mount( - - - - - - ); - - wrapper - .find('[data-test-subj="providerBadge"]') - .at(4) - .find('[data-euiicon-type]') - .first() - .simulate('click'); - - wrapper.update(); - - expect(mockOnDataProviderRemoved).not.toBeCalled(); - }); - test('it invokes the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { const dataProviders = mockDataProviders.slice(0, 1); dataProviders[0].and = mockDataProviders.slice(1, 3); @@ -420,44 +284,6 @@ describe('Providers', () => { }); }); - test('while loading data, it does NOT invoke the onToggleDataProviderEnabled callback when you click on the option "Temporary disable" in the provider menu', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderEnabled = jest.spyOn( - timelineActions, - 'updateDataProviderEnabled' - ); - - const wrapper = mount( - - - - - - ); - - wrapper - .find('[data-test-subj="providerBadge"]') - .at(4) - .find('button') - .first() - .simulate('click'); - - wrapper.update(); - - wrapper - .find(`[data-test-subj="providerActions"] .${ENABLE_CLASS_NAME}`) - .first() - .simulate('click'); - - expect(mockOnToggleDataProviderEnabled).not.toBeCalled(); - }); - test('it invokes the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { const dataProviders = mockDataProviders.slice(0, 1); dataProviders[0].and = mockDataProviders.slice(1, 3); @@ -499,43 +325,5 @@ describe('Providers', () => { providerId: 'id-Provider 1', }); }); - - test('while loading data, it does NOT invoke the onToggleDataProviderExcluded callback when you click on the option "Exclude results" in the provider menu', () => { - (useDeepEqualSelector as jest.Mock).mockReturnValue({ isLoading: true }); - const dataProviders = mockDataProviders.slice(0, 1); - dataProviders[0].and = mockDataProviders.slice(1, 3); - const mockOnToggleDataProviderExcluded = jest.spyOn( - timelineActions, - 'updateDataProviderExcluded' - ); - - const wrapper = mount( - - - - - - ); - - wrapper - .find('[data-test-subj="providerBadge"]') - .at(4) - .find('button') - .first() - .simulate('click'); - - wrapper.update(); - - wrapper - .find(`[data-test-subj="providerActions"] .${EXCLUDE_CLASS_NAME}`) - .first() - .simulate('click'); - - expect(mockOnToggleDataProviderExcluded).not.toBeCalled(); - }); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx index 05d15f076f569..e54bfc82399f9 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/index.tsx @@ -100,7 +100,6 @@ const StatefulTimelineComponent: React.FC = ({ 'sessionViewConfig', 'initialized', 'show', - 'isLoading', 'activeTab', ], getTimeline(state, timelineId) ?? timelineDefaults diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx index 5c4a592d99a7d..22289d090ab39 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/eql/index.tsx @@ -7,9 +7,9 @@ import { EuiFlexGroup } from '@elastic/eui'; import { isEmpty } from 'lodash/fp'; -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import type { ConnectedProps } from 'react-redux'; -import { connect, useDispatch } from 'react-redux'; +import { connect } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { InPortal } from 'react-reverse-portal'; import type { EuiDataGridControlColumn } from '@elastic/eui'; @@ -25,7 +25,7 @@ import { } from '../../../../../flyout/document_details/shared/constants/panel_keys'; import { useDeepEqualSelector } from '../../../../../common/hooks/use_selector'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -import { timelineActions, timelineSelectors } from '../../../../store'; +import { timelineSelectors } from '../../../../store'; import { useTimelineEvents } from '../../../../containers'; import { TimelineId, TimelineTabs } from '../../../../../../common/types/timeline'; import type { inputsModel, State } from '../../../../../common/store'; @@ -66,7 +66,6 @@ export const EqlTabContentComponent: React.FC = ({ eventIdToNoteIds, }) => { const { telemetry } = useKibana().services; - const dispatch = useDispatch(); const { query: eqlQuery = '', ...restEqlOption } = eqlOptions; const { portalNode: eqlEventsCountPortalNode } = useEqlEventsCountPortal(); const { setTimelineFullScreen, timelineFullScreen } = useTimelineFullScreen(); @@ -206,15 +205,6 @@ export const EqlTabContentComponent: React.FC = ({ [dataLoadingState] ); - useEffect(() => { - dispatch( - timelineActions.updateIsLoading({ - id: timelineId, - isLoading: isQueryLoading || loadingSourcerer, - }) - ); - }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); - const unifiedHeader = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx index ec61c67a3954a..967253a34a71a 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/tabs/query/index.tsx @@ -280,15 +280,6 @@ export const QueryTabContentComponent: React.FC = ({ [dataLoadingState] ); - useEffect(() => { - dispatch( - timelineActions.updateIsLoading({ - id: timelineId, - isLoading: isQueryLoading || loadingSourcerer, - }) - ); - }, [loadingSourcerer, timelineId, isQueryLoading, dispatch]); - // NOTE: The timeline is blank after browser FORWARD navigation (after using back button to navigate to // the previous page from the timeline), yet we still see total count. This is because the timeline // is not getting refreshed when using browser navigation. diff --git a/x-pack/plugins/security_solution/public/timelines/store/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/actions.ts index e65b7273b5de7..976d35c030651 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/actions.ts @@ -191,11 +191,6 @@ export const updateEqlOptions = actionCreator<{ value: string | undefined; }>('UPDATE_EQL_OPTIONS_TIMELINE'); -export const updateIsLoading = actionCreator<{ - id: string; - isLoading: boolean; -}>('UPDATE_LOADING'); - export const setEventsLoading = actionCreator<{ id: string; eventIds: string[]; diff --git a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts index e4fd97cd50534..5dbe2d1b9c1e8 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/defaults.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/defaults.ts @@ -64,7 +64,6 @@ export const timelineDefaults: SubsetTimelineModel & indexNames: [], isFavorite: false, isLive: false, - isLoading: false, isSaving: false, itemsPerPage: 25, itemsPerPageOptions: [10, 25, 50, 100], diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts index b0067d6d0d9f4..328c62fb08e20 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.test.ts @@ -104,7 +104,6 @@ const basicTimeline: TimelineModel = { indexNames: [], isFavorite: false, isLive: false, - isLoading: false, isSaving: false, isSelectAllChecked: false, itemsPerPage: 25, diff --git a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts index a9566c22a814a..6aa4262b26310 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/helpers.ts @@ -131,7 +131,6 @@ export const addTimelineToStore = ({ ...timelineById, [id]: { ...timeline, - isLoading: timelineById[id].isLoading, initialized: timeline.initialized ?? timelineById[id].initialized, resolveTimelineConfig, dateRange: @@ -180,7 +179,6 @@ export const addNewTimeline = ({ savedObjectId: null, version: null, isSaving: false, - isLoading: false, timelineType, ...templateTimelineInfo, }, diff --git a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.test.ts b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.test.ts index 3c8bcf4b55f58..c3d7a26d7b027 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.test.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/middlewares/timeline_save.test.ts @@ -295,7 +295,6 @@ describe('Timeline save middleware', () => { isFavorite: false, isLive: false, isSelectAllChecked: false, - isLoading: false, isSaving: false, itemsPerPage: 25, itemsPerPageOptions: [10, 25, 50, 100], diff --git a/x-pack/plugins/security_solution/public/timelines/store/model.ts b/x-pack/plugins/security_solution/public/timelines/store/model.ts index 4061276204668..92c435f93cb43 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/model.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/model.ts @@ -129,7 +129,6 @@ export interface TimelineModel { selectedEventIds: Record; /** If selectAll checkbox in header is checked **/ isSelectAllChecked: boolean; - isLoading: boolean; selectAll: boolean; /* discover saved search Id */ savedSearchId: string | null; @@ -190,7 +189,6 @@ export type SubsetTimelineModel = Readonly< | 'show' | 'sort' | 'isSaving' - | 'isLoading' | 'savedObjectId' | 'version' | 'status' diff --git a/x-pack/plugins/security_solution/public/timelines/store/reducer.ts b/x-pack/plugins/security_solution/public/timelines/store/reducer.ts index ba93b512136c8..546e60e47d10b 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/reducer.ts @@ -45,7 +45,6 @@ import { removeColumn, upsertColumn, updateColumns, - updateIsLoading, updateSort, clearSelected, setSelected, @@ -409,16 +408,6 @@ export const timelineReducer = reducerWithInitialState(initialTimelineState) timelineById: state.timelineById, }), })) - .case(updateIsLoading, (state, { id, isLoading }) => ({ - ...state, - timelineById: { - ...state.timelineById, - [id]: { - ...state.timelineById[id], - isLoading, - }, - }, - })) .case(updateSort, (state, { id, sort }) => ({ ...state, timelineById: updateTableSort({ id, sort, timelineById: state.timelineById }), diff --git a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts index df81ad54ecd48..c25bd1b9b115f 100644 --- a/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts +++ b/x-pack/test/security_solution_cypress/cypress/screens/timeline.ts @@ -117,8 +117,6 @@ export const ALERTS_TABLE_COUNT = `[data-test-subj="toolbar-alerts-count"]`; export const STAR_ICON = '[data-test-subj="timeline-favorite-empty-star"]'; -export const TIMELINE_COLUMN_SPINNER = '[data-test-subj="timeline-loading-spinner"]'; - export const TIMELINE_COLLAPSED_ITEMS_BTN = '[data-test-subj="euiCollapsedItemActionsButton"]'; export const TIMELINE_CREATE_TEMPLATE_FROM_TIMELINE_BTN = diff --git a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts index e8541a529add0..b70adbefa1850 100644 --- a/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts +++ b/x-pack/test/security_solution_cypress/cypress/tasks/alerts.ts @@ -57,7 +57,6 @@ import { TOOLTIP, } from '../screens/alerts'; import { LOADING_INDICATOR, REFRESH_BUTTON } from '../screens/security_header'; -import { TIMELINE_COLUMN_SPINNER } from '../screens/timeline'; import { UPDATE_ENRICHMENT_RANGE_BUTTON, ENRICHMENT_QUERY_END_INPUT, @@ -216,7 +215,6 @@ export const goToClosedAlertsOnRuleDetailsPage = () => { cy.get(CLOSED_ALERTS_FILTER_BTN).click(); cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); - cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; export const goToClosedAlerts = () => { @@ -233,7 +231,6 @@ export const goToClosedAlerts = () => { selectPageFilterValue(0, 'closed'); cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); - cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; export const goToOpenedAlertsOnRuleDetailsPage = () => { @@ -297,7 +294,6 @@ export const goToAcknowledgedAlerts = () => { selectPageFilterValue(0, 'acknowledged'); cy.get(REFRESH_BUTTON).should('not.have.attr', 'aria-label', 'Needs updating'); cy.get(REFRESH_BUTTON).should('have.attr', 'aria-label', 'Refresh query'); - cy.get(TIMELINE_COLUMN_SPINNER).should('not.exist'); }; export const markAlertsAcknowledged = () => { From 77384f31b540da1aa1bbf820c670bde5f52d9b80 Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Mon, 18 Nov 2024 13:30:32 +0100 Subject: [PATCH 18/50] [Infra] Change order of the errors shown on Infra pages (#200531) Closes #200190 ## Summary This PR fixes the issue with the errors shown on the metrics explorer page - when the metrics indices can't be fetched we should show the error returned and if there is no fetch error and a remote cluster configured but no connection is possible we should show the remote cluster error: - Example with 504 error ![image](https://github.com/user-attachments/assets/65cd8226-8c81-4c64-b043-c9db5a93d3e0) - Example with remote cluster error ![image](https://github.com/user-attachments/assets/e024a3f8-76e0-4ad7-8aa6-e35ad5c1112a) ## Testing Couldn't find a way to reproduce this so I "faked" the API response to be an error and checked several cases - API returns an error, we should show the error: https://github.com/user-attachments/assets/c1086b22-1ff5-4333-97a5-b3d1dca16afe - API doesn't return an error but the remote cluster connection wasn't possible, we should show the remote cluster error: https://github.com/user-attachments/assets/151b3ae4-5ca1-4d54-bd58-2729db202cdb - If no remote cluster is used/or a remote cluster is connected and the API response is not returning an error the page should load correctly: https://github.com/user-attachments/assets/f9ef1066-3dfd-4957-8b46-878bf58d2f1c --- .../components/shared/templates/infra_page_template.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/observability_solution/infra/public/components/shared/templates/infra_page_template.tsx b/x-pack/plugins/observability_solution/infra/public/components/shared/templates/infra_page_template.tsx index b7318c736843a..61130564a2753 100644 --- a/x-pack/plugins/observability_solution/infra/public/components/shared/templates/infra_page_template.tsx +++ b/x-pack/plugins/observability_solution/infra/public/components/shared/templates/infra_page_template.tsx @@ -98,12 +98,12 @@ export const InfraPageTemplate = ({ }); }, [hasData, setScreenContext, source]); - if (!isSourceLoading && !remoteClustersExist) { - return ; + if (sourceError) { + return ; } - if (sourceError) { - ; + if (!isSourceLoading && !remoteClustersExist) { + return ; } if (dataViewLoadError) { From 0f11dcd65a1f812f523d2ca858d94e970fedca46 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 12:30:47 +0000 Subject: [PATCH 19/50] [Ownership] Assign test files to apm teams (#200182) ## Summary Assign test files to apm teams Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index d06c00dfec607..2c5a51ddb81ec 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1302,7 +1302,10 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/**/test_suites/**/fleet/ @elastic/fleet # APM -/x-pack/test/functional/apps/apm/ @elastic/obs-ux-infra_services-team +/x-pack/test/stack_functional_integration/apps/apm @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +/x-pack/test/common/services/apm_synthtrace_kibana_client.ts @elastic/obs-ux-infra_services-team @elastic/obs-ux-logs-team +/test/api_integration/apis/ui_metric/*.ts @elastic/obs-ux-infra_services-team +/x-pack/test/functional/apps/apm/ @elastic/obs-ux-infra_services-team /x-pack/test/apm_api_integration/ @elastic/obs-ux-infra_services-team /src/apm.js @elastic/kibana-core @vigneshshanmugam /packages/kbn-utility-types/src/dot.ts @dgieselaar @@ -1312,6 +1315,7 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai #CC# /x-pack/plugins/observability_solution/observability/ @elastic/apm-ui # Uptime +/x-pack/test/functional/page_objects/uptime_page.ts @elastic/obs-ux-management-team /x-pack/test/functional_with_es_ssl/apps/discover_ml_uptime/uptime/ @elastic/obs-ux-management-team /x-pack/test/functional/apps/uptime @elastic/obs-ux-management-team /x-pack/test/functional/es_archives/uptime @elastic/obs-ux-management-team @@ -1323,6 +1327,16 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/api_integration/test_suites/observability/synthetics @elastic/obs-ux-management-team # obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/config.* @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/landing_page.ts @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/navigation.ts @elastic/obs-ux-logs-team +/x-pack/test/functional/page_objects/dataset_quality.ts @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/index* @elastic/obs-ux-logs-team +/x-pack/test_serverless/functional/test_suites/observability/cypress @elastic/obs-ux-logs-team +/x-pack/test/functional/services/infra_source_configuration_form.ts @elastic/obs-ux-logs-team +/x-pack/test/functional/services/logs_ui @elastic/obs-ux-logs-team +/x-pack/test/functional/page_objects/observability_logs_explorer.ts @elastic/obs-ux-logs-team +/test/functional/services/selectable.ts @elastic/obs-ux-logs-team /x-pack/test/observability_onboarding_api_integration @elastic/obs-ux-logs-team /x-pack/test_serverless/api_integration/test_suites/observability/index.feature_flags.ts @elastic/obs-ux-logs-team /x-pack/test/api_integration/apis/logs_ui @elastic/obs-ux-logs-team @@ -1332,7 +1346,6 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/functional/test_suites/observability/observability_logs_explorer @elastic/obs-ux-logs-team /x-pack/test/functional/apps/dataset_quality @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/dataset_quality @elastic/obs-ux-logs-team -/x-pack/test_serverless/functional/test_suites/observability/ @elastic/obs-ux-logs-team /x-pack/test_serverless/functional/test_suites/observability/discover @elastic/obs-ux-logs-team @elastic/kibana-data-discovery /src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team /x-pack/test/api_integration/apis/logs_shared @elastic/obs-ux-logs-team From af91db16dd2eaf9f26ef337210b84d32e0455f1d Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 12:41:13 +0000 Subject: [PATCH 20/50] [Ownership] Assign test files to stack-monitoring team (#200178) ## Summary Assign test files to stack-monitoring team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2c5a51ddb81ec..6499b1761174a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1283,6 +1283,9 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test_serverless/**/test_suites/observability/infra/ @elastic/obs-ux-infra_services-team # Elastic Stack Monitoring +/x-pack/test/monitoring_api_integration @elastic/stack-monitoring +/x-pack/test/functional/page_objects/monitoring_page.ts @elastic/stack-monitoring +/x-pack/test/functional/es_archives/monitoring @elastic/stack-monitoring /x-pack/test/functional/services/monitoring @elastic/stack-monitoring /x-pack/test/functional/apps/monitoring @elastic/stack-monitoring /x-pack/test/api_integration/apis/monitoring @elastic/stack-monitoring From 7ee23af27b4b9c182132120980eae7af644fca29 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 12:46:07 +0000 Subject: [PATCH 21/50] [Ownership] Assign test files to observability-ui team (#200185) ## Summary Assign test files to observability-ui team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 6499b1761174a..ae55ea6b37108 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1353,6 +1353,12 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /src/plugins/unified_doc_viewer/public/components/doc_viewer_logs_overview @elastic/obs-ux-logs-team /x-pack/test/api_integration/apis/logs_shared @elastic/obs-ux-logs-team +# Observability-ui +/x-pack/test_serverless/api_integration/test_suites/observability/index.ts @elastic/observability-ui +/x-pack/test/functional_solution_sidenav/tests/observability_sidenav.ts @elastic/observability-ui +/x-pack/test/functional/page_objects/observability_page.ts @elastic/observability-ui +/x-pack/test_serverless/**/test_suites/observability/config.ts @elastic/observability-ui + # Observability onboarding tour /x-pack/plugins/observability_solution/observability_shared/public/components/tour @elastic/appex-sharedux /x-pack/test/functional/apps/infra/tour.ts @elastic/appex-sharedux From 2235f95ef060f9ec49d120b735669d3e13bfb594 Mon Sep 17 00:00:00 2001 From: Ignacio Rivas Date: Mon, 18 Nov 2024 13:56:11 +0100 Subject: [PATCH 22/50] [Console] Avoid console url redirection (#199966) --- test/functional/apps/console/_text_input.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/functional/apps/console/_text_input.ts b/test/functional/apps/console/_text_input.ts index c2580d2dbb067..0e286434fa8d1 100644 --- a/test/functional/apps/console/_text_input.ts +++ b/test/functional/apps/console/_text_input.ts @@ -32,7 +32,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('with a data URI in the load_from query', () => { it('loads the data from the URI', async () => { await PageObjects.common.navigateToApp('console', { - hash: '#/console?load_from=data:text/plain,BYUwNmD2Q', + hash: '#/console/shell?load_from=data:text/plain,BYUwNmD2Q', }); await retry.try(async () => { @@ -44,7 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('with invalid data', () => { it('shows a toast error', async () => { await PageObjects.common.navigateToApp('console', { - hash: '#/console?load_from=data:text/plain,BYUwNmD2', + hash: '#/console/shell?load_from=data:text/plain,BYUwNmD2', }); await retry.try(async () => { From ab7de2082b956f7726b85545abbc1c7ce915a56e Mon Sep 17 00:00:00 2001 From: Konrad Szwarc Date: Mon, 18 Nov 2024 14:20:54 +0100 Subject: [PATCH 23/50] [EDR Workflows] Add Signer option to Mac trusted apps (#197821) This PR adds a Signer condition for trusted apps on macOS. Previously, users could only build conditions using hash, path, and signer options on Windows. With these changes, macOS also supports the Signer option, leaving only Linux limited to Path and Hash options. https://github.com/user-attachments/assets/ea8fb734-7884-451d-8873-e3a29861876b --- .../src/path_validations/index.ts | 7 +- .../exceptions_list_item_generator.ts | 39 ++- .../endpoint/schema/trusted_apps.test.ts | 36 +-- .../common/endpoint/schema/trusted_apps.ts | 9 + .../common/endpoint/types/trusted_apps.ts | 21 +- .../cypress/e2e/artifacts/trusted_apps.cy.ts | 231 ++++++++++++++++++ .../management/cypress/tasks/artifacts.ts | 124 +++++++++- .../pages/trusted_apps/state/type_guards.ts | 18 +- .../condition_entry_input/index.test.tsx | 4 +- .../condition_entry_input/index.tsx | 14 +- .../trusted_apps/view/components/form.tsx | 57 +++-- .../pages/trusted_apps/view/translations.ts | 16 +- .../validators/trusted_app_validator.ts | 42 +++- .../trusted_apps.ts | 73 ++++-- 14 files changed, 586 insertions(+), 105 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts diff --git a/packages/kbn-securitysolution-utils/src/path_validations/index.ts b/packages/kbn-securitysolution-utils/src/path_validations/index.ts index 0609129349b60..1f1eaf0b01423 100644 --- a/packages/kbn-securitysolution-utils/src/path_validations/index.ts +++ b/packages/kbn-securitysolution-utils/src/path_validations/index.ts @@ -21,19 +21,22 @@ export enum ConditionEntryField { HASH = 'process.hash.*', PATH = 'process.executable.caseless', SIGNER = 'process.Ext.code_signature', + SIGNER_MAC = 'process.code_signature', } export enum EntryFieldType { HASH = '.hash.', EXECUTABLE = '.executable.caseless', PATH = '.path', - SIGNER = '.Ext.code_signature', + SIGNER = '.code_signature', } export type TrustedAppConditionEntryField = | 'process.hash.*' | 'process.executable.caseless' - | 'process.Ext.code_signature'; + | 'process.Ext.code_signature' + | 'process.code_signature'; + export type BlocklistConditionEntryField = | 'file.hash.*' | 'file.path' diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts index cb332f8dea55a..b7293a3cef16c 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/exceptions_list_item_generator.ts @@ -9,13 +9,9 @@ import type { ExceptionListItemSchema, CreateExceptionListItemSchema, UpdateExceptionListItemSchema, + EntriesArray, } from '@kbn/securitysolution-io-ts-list-types'; -import { - ENDPOINT_EVENT_FILTERS_LIST_ID, - ENDPOINT_TRUSTED_APPS_LIST_ID, - ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, - ENDPOINT_BLOCKLISTS_LIST_ID, -} from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import { BaseDataGenerator } from './base_data_generator'; import { BY_POLICY_ARTIFACT_TAG_PREFIX, GLOBAL_ARTIFACT_TAG } from '../service/artifacts/constants'; @@ -150,7 +146,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Trusted app (${this.randomString(5)})`, - list_id: ENDPOINT_TRUSTED_APPS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, ...overrides, }); } @@ -173,10 +169,33 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator = {}): ExceptionListItemSchema { return this.generate({ name: `Event filter (${this.randomString(5)})`, - list_id: ENDPOINT_EVENT_FILTERS_LIST_ID, + list_id: ENDPOINT_ARTIFACT_LISTS.eventFilters.id, entries: [ { field: 'process.pe.company', @@ -224,7 +243,7 @@ export class ExceptionsListItemGenerator extends BaseDataGenerator { @@ -105,14 +105,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const body = PostTrustedAppCreateRequestSchema.body; it('should not error on a valid message', () => { @@ -389,14 +390,15 @@ describe('When invoking Trusted Apps Schema', () => { value: 'c:/programs files/Anti-Virus', ...(data || {}), }); - const createNewTrustedApp = (data?: T): NewTrustedApp => ({ - name: 'Some Anti-Virus App', - description: 'this one is ok', - os: OperatingSystem.WINDOWS, - effectScope: { type: 'global' }, - entries: [createConditionEntry()], - ...(data || {}), - }); + const createNewTrustedApp = (data?: T): NewTrustedApp => + ({ + name: 'Some Anti-Virus App', + description: 'this one is ok', + os: OperatingSystem.WINDOWS, + effectScope: { type: 'global' }, + entries: [createConditionEntry()], + ...(data || {}), + } as NewTrustedApp); const updateParams = (data?: T): PutTrustedAppsRequestParams => ({ id: 'validId', diff --git a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts index 4b0a1ee2157de..e31e65195ec95 100644 --- a/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/schema/trusted_apps.ts @@ -80,6 +80,15 @@ const LinuxEntrySchema = schema.object({ const MacEntrySchema = schema.object({ ...CommonEntrySchema, + field: schema.oneOf([ + schema.literal(ConditionEntryField.HASH), + schema.literal(ConditionEntryField.PATH), + schema.literal(ConditionEntryField.SIGNER_MAC), + ]), + value: schema.string({ + validate: (field: string) => + field.length > 0 ? undefined : `invalidField.${ConditionEntryField.SIGNER_MAC}`, + }), }); const entriesSchemaOptions = { diff --git a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts index dd97ad02dcb96..4fa99c015a7c7 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/trusted_apps.ts @@ -36,23 +36,32 @@ export interface TrustedAppConditionEntry; export type WindowsConditionEntry = TrustedAppConditionEntry< ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER >; -export interface MacosLinuxConditionEntries { - os: OperatingSystem.LINUX | OperatingSystem.MAC; - entries: MacosLinuxConditionEntry[]; +export type MacosConditionEntry = TrustedAppConditionEntry< + ConditionEntryField.HASH | ConditionEntryField.PATH | ConditionEntryField.SIGNER_MAC +>; + +interface LinuxConditionEntries { + os: OperatingSystem.LINUX; + entries: LinuxConditionEntry[]; } -export interface WindowsConditionEntries { +interface WindowsConditionEntries { os: OperatingSystem.WINDOWS; entries: WindowsConditionEntry[]; } +interface MacosConditionEntries { + os: OperatingSystem.MAC; + entries: MacosConditionEntry[]; +} + export interface GlobalEffectScope { type: 'global'; } @@ -70,7 +79,7 @@ export type NewTrustedApp = { name: string; description?: string; effectScope: EffectScope; -} & (MacosLinuxConditionEntries | WindowsConditionEntries); +} & (LinuxConditionEntries | WindowsConditionEntries | MacosConditionEntries); /** A trusted app entry */ export type TrustedApp = NewTrustedApp & { diff --git a/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts new file mode 100644 index 0000000000000..aef24ed4ce045 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/cypress/e2e/artifacts/trusted_apps.cy.ts @@ -0,0 +1,231 @@ +/* + * 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 { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; +import { + createArtifactList, + createPerPolicyArtifact, + removeExceptionsList, + trustedAppsFormSelectors, +} from '../../tasks/artifacts'; +import type { IndexedFleetEndpointPolicyResponse } from '../../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { createAgentPolicyTask, getEndpointIntegrationVersion } from '../../tasks/fleet'; +import { login } from '../../tasks/login'; + +const { + openTrustedApps, + selectOs, + openFieldSelector, + expectedFieldOptions, + selectField, + fillOutValueField, + fillOutTrustedAppsFlyout, + submitForm, + validateSuccessPopup, + validateRenderedCondition, + clickAndConditionButton, + validateRenderedConditions, + deleteTrustedAppItem, + removeSingleCondition, + expectAllFieldOptionsRendered, + expectFieldOptionsNotRendered, +} = trustedAppsFormSelectors; + +describe( + 'Trusted Apps', + { + tags: ['@ess', '@serverless', '@skipInServerlessMKI'], // @skipInServerlessMKI until kibana is rebuilt after merge + }, + () => { + let indexedPolicy: IndexedFleetEndpointPolicyResponse; + + before(() => { + getEndpointIntegrationVersion().then((version) => { + createAgentPolicyTask(version).then((data) => { + indexedPolicy = data; + }); + }); + }); + + beforeEach(() => { + login(); + }); + + after(() => { + if (indexedPolicy) { + cy.task('deleteIndexedFleetEndpointPolicies', indexedPolicy); + } + }); + + const createArtifactBodyRequest = (multiCondition = false) => ({ + list_id: ENDPOINT_ARTIFACT_LISTS.trustedApps.id, + entries: [ + { + entries: [ + { + field: 'trusted', + operator: 'included', + type: 'match', + value: 'true', + }, + { + field: 'subject_name', + value: 'TestSignature', + type: 'match', + operator: 'included', + }, + ], + field: 'process.code_signature', + type: 'nested', + }, + ...(multiCondition + ? [ + { + field: 'process.hash.sha1', + value: '323769d194406183912bb903e7fe738221543348', + type: 'match', + operator: 'included', + }, + { + field: 'process.executable.caseless', + value: '/dev/null', + type: 'match', + operator: 'included', + }, + ] + : []), + ], + os_types: ['macos'], + }); + + describe('Renders Trusted Apps form fields', () => { + it('Correctly renders all blocklist fields for different OSs', () => { + openTrustedApps({ create: true }); + selectOs('windows'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('macos'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectAllFieldOptionsRendered(); + + selectOs('linux'); + expectFieldOptionsNotRendered(); + openFieldSelector(); + expectedFieldOptions(['Path', 'Hash']); + }); + }); + + describe('Handles CRUD with signature field', () => { + afterEach(() => { + removeExceptionsList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + }); + + it('Correctly creates a trusted app with a single signature field on Mac', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignature/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + submitForm(); + validateSuccessPopup('create'); + validateRenderedCondition(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with single signature field', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest()).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os single signature field trusted app item', () => { + const expectedCondition = /AND\s*process\.code_signature\s*IS\s*TestSignatureNext/; + openTrustedApps({ itemId }); + fillOutValueField('Next'); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + + it('Correctly creates a trusted app with a multiple conditions on Mac', () => { + const expectedCondition = + /\s*OSIS\s*Mac\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.hash\.\*\s*IS\s*323769d194406183912bb903e7fe738221543348\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + + openTrustedApps({ create: true }); + fillOutTrustedAppsFlyout(); + selectOs('macos'); + // Set signature field + openFieldSelector(); + selectField(); + fillOutValueField('TestSignature'); + // Add another condition + clickAndConditionButton(); + // Set hash field + openFieldSelector(1, 1); + selectField('Hash', 1, 1); + fillOutValueField('323769d194406183912bb903e7fe738221543348', 1, 1); + // Add another condition + clickAndConditionButton(); + // Set path field + openFieldSelector(1, 2); + selectField('Path', 1, 2); + fillOutValueField('/dev/null', 1, 2); + + submitForm(); + validateSuccessPopup('create'); + validateRenderedConditions(expectedCondition); + }); + + describe('Correctly updates and deletes Mac os trusted app with multiple conditions', () => { + let itemId: string; + + beforeEach(() => { + createArtifactList(ENDPOINT_ARTIFACT_LISTS.trustedApps.id); + createPerPolicyArtifact('Test TrustedApp', createArtifactBodyRequest(true)).then( + (response) => { + itemId = response.body.item_id; + } + ); + }); + + it('Updates Mac os multiple condition trusted app item', () => { + const expectedCondition = + /\s*AND\s*process\.code_signature\s*IS\s*TestSignature\s*AND\s*process\.executable\.caselessIS\s*\/dev\/null\s*/; + openTrustedApps({ itemId }); + removeSingleCondition(1, 1); + submitForm(); + validateSuccessPopup('update'); + validateRenderedCondition(expectedCondition); + }); + + it('Deletes a blocklist item', () => { + openTrustedApps(); + deleteTrustedAppItem(); + validateSuccessPopup('delete'); + }); + }); + }); + } +); diff --git a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts index fdffa0bd03381..034ea11d87a83 100644 --- a/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts +++ b/x-pack/plugins/security_solution/public/management/cypress/tasks/artifacts.ts @@ -18,7 +18,7 @@ import { EXCEPTION_LIST_ITEM_URL, EXCEPTION_LIST_URL, } from '@kbn/securitysolution-list-constants'; -import { APP_BLOCKLIST_PATH } from '../../../../common/constants'; +import { APP_BLOCKLIST_PATH, APP_TRUSTED_APPS_PATH } from '../../../../common/constants'; import { loadPage, request } from './common'; export const removeAllArtifacts = () => { @@ -108,6 +108,128 @@ export const yieldFirstPolicyID = (): Cypress.Chainable => return body.items[0].id; }); +export const trustedAppsFormSelectors = { + selectOs: (os: 'windows' | 'macos' | 'linux') => { + cy.getByTestSubj('trustedApps-form-osSelectField').click(); + cy.get(`button[role="option"][id="${os}"]`).click(); + }, + + openFieldSelector: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field` + ).click(); + }, + + selectField: (field: 'Signature' | 'Hash' | 'Path' = 'Signature', group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-field-type-${field}` + ).click(); + }, + + fillOutValueField: (value: string, group = 1, entry = 0) => { + cy.getByTestSubj(`trustedApps-form-conditionsBuilder-group${group}-entry${entry}-value`).type( + value + ); + }, + + clickAndConditionButton: () => { + cy.getByTestSubj('trustedApps-form-conditionsBuilder-group1-AndButton').click(); + }, + + submitForm: () => { + cy.getByTestSubj('trustedAppsListPage-flyout-submitButton').click(); + }, + + fillOutTrustedAppsFlyout: () => { + cy.getByTestSubj('trustedApps-form-nameTextField').type('Test TrustedApp'); + cy.getByTestSubj('trustedApps-form-descriptionField').type('Test Description'); + }, + + expectedFieldOptions: (fields = ['Path', 'Hash', 'Signature']) => { + if (fields.length) { + fields.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).contains(field); + }); + } else { + const fields2 = ['Path', 'Hash', 'Signature']; + fields2.forEach((field) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group1-entry0-field-type-${field}` + ).should('not.exist'); + }); + } + }, + + expectAllFieldOptionsRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions(); + }, + + expectFieldOptionsNotRendered: () => { + trustedAppsFormSelectors.expectedFieldOptions([]); + }, + + openTrustedApps: ({ create, itemId }: { create?: boolean; itemId?: string } = {}) => { + if (!create && !itemId) { + loadPage(APP_TRUSTED_APPS_PATH); + } else if (create) { + loadPage(`${APP_TRUSTED_APPS_PATH}?show=create`); + } else if (itemId) { + loadPage(`${APP_TRUSTED_APPS_PATH}?itemId=${itemId}&show=edit`); + } + }, + + validateSuccessPopup: (type: 'create' | 'update' | 'delete') => { + let expectedTitle = ''; + switch (type) { + case 'create': + expectedTitle = '"Test TrustedApp" has been added to your trusted applications.'; + break; + case 'update': + expectedTitle = '"Test TrustedApp" has been updated'; + break; + case 'delete': + expectedTitle = '"Test TrustedApp" has been removed from trusted applications.'; + break; + } + cy.getByTestSubj('euiToastHeader__title').contains(expectedTitle); + }, + + validateRenderedCondition: (expectedCondition: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-os') + .invoke('text') + .should('match', /OS\s*IS\s*Mac/); + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions-condition') + .invoke('text') + .should('match', expectedCondition); + }); + }, + validateRenderedConditions: (expectedConditions: RegExp) => { + cy.getByTestSubj('trustedAppsListPage-card-criteriaConditions') + .invoke('text') + .should('match', expectedConditions); + }, + removeSingleCondition: (group = 1, entry = 0) => { + cy.getByTestSubj( + `trustedApps-form-conditionsBuilder-group${group}-entry${entry}-remove` + ).click(); + }, + deleteTrustedAppItem: () => { + cy.getByTestSubj('trustedAppsListPage-card') + .first() + .within(() => { + cy.getByTestSubj('trustedAppsListPage-card-header-actions-button').click(); + }); + + cy.getByTestSubj('trustedAppsListPage-card-cardDeleteAction').click(); + cy.getByTestSubj('trustedAppsListPage-deleteModal-submitButton').click(); + }, +}; + export const blocklistFormSelectors = { expectSingleOperator: (field: 'Path' | 'Signature' | 'Hash') => { cy.getByTestSubj('blocklist-form-field-select').contains(field); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts index 082435817d43d..f6dff90b48227 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/type_guards.ts @@ -8,18 +8,14 @@ import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { TrustedAppConditionEntry, - MacosLinuxConditionEntry, - WindowsConditionEntry, + LinuxConditionEntry, } from '../../../../../common/endpoint/types'; -export const isWindowsTrustedAppCondition = ( +export const isSignerFieldExcluded = ( condition: TrustedAppConditionEntry -): condition is WindowsConditionEntry => { - return condition.field === ConditionEntryField.SIGNER || true; -}; - -export const isMacosLinuxTrustedAppCondition = ( - condition: TrustedAppConditionEntry -): condition is MacosLinuxConditionEntry => { - return condition.field !== ConditionEntryField.SIGNER; +): condition is LinuxConditionEntry => { + return ( + condition.field !== ConditionEntryField.SIGNER && + condition.field !== ConditionEntryField.SIGNER_MAC + ); }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx index 6ba6a0b91d4eb..100e10e16cb99 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.test.tsx @@ -152,13 +152,13 @@ describe('Condition entry input', () => { expect(superSelectProps.options.length).toBe(2); }); - it('should be able to select two options when MAC OS', () => { + it('should be able to select three options when MAC OS', () => { const element = mount(getElement('testCheckSignatureOption', { os: OperatingSystem.MAC })); const superSelectProps = element .find('[data-test-subj="testCheckSignatureOption-field"]') .first() .props() as EuiSuperSelectProps; - expect(superSelectProps.options.length).toBe(2); + expect(superSelectProps.options.length).toBe(3); }); it('should have operator value selected when field is HASH', () => { diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx index 3ce26c70d3186..b55c86b939395 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/condition_entry_input/index.tsx @@ -143,6 +143,18 @@ export const ConditionEntryInput = memo( }, ] : []), + ...(os === OperatingSystem.MAC + ? [ + { + dropdownDisplay: getDropdownDisplay(ConditionEntryField.SIGNER_MAC), + inputDisplay: CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC], + value: ConditionEntryField.SIGNER_MAC, + 'data-test-subj': getTestId( + `field-type-${CONDITION_FIELD_TITLE[ConditionEntryField.SIGNER_MAC]}` + ), + }, + ] + : []), ]; }, [getTestId, os]); @@ -224,7 +236,7 @@ export const ConditionEntryInput = memo( - {/* Unicode `nbsp` is used below so that Remove button is property displayed */} + {/* Unicode `nbsp` is used below so that Remove button is properly displayed */} ( entries: [] as ArtifactFormComponentProps['item']['entries'], }; - if (os !== OperatingSystem.WINDOWS) { - const macOsLinuxConditionEntry = item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - nextItem.entries.push(...macOsLinuxConditionEntry); - if (item.entries.length === 0) { - nextItem.entries.push(defaultConditionEntry()); - } - } else { - nextItem.entries.push(...item.entries); + switch (os) { + case OperatingSystem.LINUX: + nextItem.entries = item.entries.filter((entry) => + isSignerFieldExcluded(entry as TrustedAppConditionEntry) + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.MAC: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER + ? { ...entry, field: ConditionEntryField.SIGNER_MAC } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + case OperatingSystem.WINDOWS: + nextItem.entries = item.entries.map((entry) => + entry.field === ConditionEntryField.SIGNER_MAC + ? { ...entry, field: ConditionEntryField.SIGNER } + : entry + ); + if (item.entries.length === 0) { + nextItem.entries.push(defaultConditionEntry()); + } + break; + default: + nextItem.entries.push(...item.entries); + break; } processChanged(nextItem); @@ -429,17 +448,15 @@ export const TrustedAppsForm = memo( entries: [], }; const os = ((item.os_types ?? [])[0] as OperatingSystem) ?? OperatingSystem.WINDOWS; - if (os === OperatingSystem.WINDOWS) { - nextItem.entries = [...item.entries, defaultConditionEntry()].filter((entry) => - isWindowsTrustedAppCondition(entry as TrustedAppConditionEntry) - ); - } else { + if (os === OperatingSystem.LINUX) { nextItem.entries = [ ...item.entries.filter((entry) => - isMacosLinuxTrustedAppCondition(entry as TrustedAppConditionEntry) + isSignerFieldExcluded(entry as TrustedAppConditionEntry) ), defaultConditionEntry(), ]; + } else { + nextItem.entries = [...item.entries, defaultConditionEntry()]; } processChanged(nextItem); setHasFormChanged(true); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts index c1e544be537ef..0208898617c49 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/translations.ts @@ -8,9 +8,10 @@ import { i18n } from '@kbn/i18n'; import { ConditionEntryField } from '@kbn/securitysolution-utils'; import type { - MacosLinuxConditionEntry, + LinuxConditionEntry, WindowsConditionEntry, OperatorFieldIds, + MacosConditionEntry, } from '../../../../../common/endpoint/types'; export const NAME_LABEL = i18n.translate('xpack.securitySolution.trustedApps.name.label', { @@ -68,6 +69,10 @@ export const CONDITION_FIELD_TITLE: { [K in ConditionEntryField]: string } = { 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signature', { defaultMessage: 'Signature' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.signatureMac', + { defaultMessage: 'Signature' } + ), }; export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } = { @@ -83,6 +88,10 @@ export const CONDITION_FIELD_DESCRIPTION: { [K in ConditionEntryField]: string } 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signature', { defaultMessage: 'The signer of the application' } ), + [ConditionEntryField.SIGNER_MAC]: i18n.translate( + 'xpack.securitySolution.trustedapps.logicalConditionBuilder.entry.field.description.signatureMac', + { defaultMessage: 'The signer of the application' } + ), }; export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { @@ -95,7 +104,10 @@ export const OPERATOR_TITLES: { [K in OperatorFieldIds]: string } = { }; export const ENTRY_PROPERTY_TITLES: Readonly<{ - [K in keyof Omit]: string; + [K in keyof Omit< + LinuxConditionEntry | WindowsConditionEntry | MacosConditionEntry, + 'type' + >]: string; }> = { field: i18n.translate('xpack.securitySolution.trustedapps.trustedapp.entry.field', { defaultMessage: 'Field', diff --git a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts index 38dd3442f3b4f..3b8a77a964006 100644 --- a/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts +++ b/x-pack/plugins/security_solution/server/lists_integration/endpoint/validators/trusted_app_validator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { ENDPOINT_ARTIFACT_LISTS } from '@kbn/securitysolution-list-constants'; import type { TypeOf } from '@kbn/config-schema'; import { schema } from '@kbn/config-schema'; import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; @@ -30,7 +30,8 @@ const ProcessHashField = schema.oneOf([ schema.literal('process.hash.sha256'), ]); const ProcessExecutablePath = schema.literal('process.executable.caseless'); -const ProcessCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessWindowsCodeSigner = schema.literal('process.Ext.code_signature'); +const ProcessMacCodeSigner = schema.literal('process.code_signature'); const ConditionEntryTypeSchema = schema.conditional( schema.siblingRef('field'), @@ -43,7 +44,8 @@ const ConditionEntryOperatorSchema = schema.literal('included'); type ConditionEntryFieldAllowedType = | TypeOf | TypeOf - | TypeOf; + | TypeOf + | TypeOf; type TrustedAppConditionEntry< T extends ConditionEntryFieldAllowedType = ConditionEntryFieldAllowedType @@ -54,7 +56,8 @@ type TrustedAppConditionEntry< operator: 'included'; value: string; } - | TypeOf; + | TypeOf + | TypeOf; /* * A generic Entry schema to be used for a specific entry schema depending on the OS @@ -85,11 +88,10 @@ const CommonEntrySchema = { ), }; -// Windows Signer entries use a Nested field that checks to ensure +// Windows/MacOS Signer entries use a Nested field that checks to ensure // that the certificate is trusted -const WindowsSignerEntrySchema = schema.object({ +const SignerEntrySchema = { type: schema.literal('nested'), - field: ProcessCodeSigner, entries: schema.arrayOf( schema.oneOf([ schema.object({ @@ -107,21 +109,35 @@ const WindowsSignerEntrySchema = schema.object({ ]), { minSize: 2, maxSize: 2 } ), +}; + +const SignerWindowsEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessWindowsCodeSigner, +}); + +const SignerMacEntrySchema = schema.object({ + ...SignerEntrySchema, + field: ProcessMacCodeSigner, }); const WindowsEntrySchema = schema.oneOf([ - WindowsSignerEntrySchema, + SignerWindowsEntrySchema, schema.object({ ...CommonEntrySchema, field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), }), ]); -const LinuxEntrySchema = schema.object({ - ...CommonEntrySchema, -}); +const MacEntrySchema = schema.oneOf([ + SignerMacEntrySchema, + schema.object({ + ...CommonEntrySchema, + field: schema.oneOf([ProcessHashField, ProcessExecutablePath]), + }), +]); -const MacEntrySchema = schema.object({ +const LinuxEntrySchema = schema.object({ ...CommonEntrySchema, }); @@ -172,7 +188,7 @@ const TrustedAppDataSchema = schema.object( export class TrustedAppValidator extends BaseValidator { static isTrustedApp(item: { listId: string }): boolean { - return item.listId === ENDPOINT_TRUSTED_APPS_LIST_ID; + return item.listId === ENDPOINT_ARTIFACT_LISTS.trustedApps.id; } protected async validateHasWritePrivilege(): Promise { diff --git a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts index cb310acf47aa4..e7d693111aa05 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/edr_workflows/artifacts/trial_license_complete_tier/trusted_apps.ts @@ -206,26 +206,7 @@ export default function ({ getService }: FtrProviderContext) { const body = trustedAppApiCall.getBody(); body.os_types = ['linux']; - body.entries = [ - { - field: 'process.Ext.code_signature', - entries: [ - { - field: 'trusted', - value: 'true', - type: 'match', - operator: 'included', - }, - { - field: 'subject_name', - value: 'foo', - type: 'match', - operator: 'included', - }, - ], - type: 'nested', - }, - ]; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) .set('kbn-xsrf', 'true') @@ -235,6 +216,58 @@ export default function ({ getService }: FtrProviderContext) { .expect(anErrorMessageWith(/^.*(?!process\.Ext\.code_signature)/)); }); + it(`should error on [${trustedAppApiCall.method} if Mac signer field is used for Windows entry`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it(`should error on [${trustedAppApiCall.method} if Windows signer field is used for Mac entry`, async () => { + const body = trustedAppApiCall.getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCall.method](trustedAppApiCall.path) + .set('kbn-xsrf', 'true') + .send(body) + .expect(400); + }); + + it('should not error if signer is set for a windows os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['windows']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry(); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + + it('should not error if signer is set for a mac os entry item', async () => { + const body = trustedAppApiCalls[0].getBody(); + + body.os_types = ['macos']; + body.entries = exceptionsGenerator.generateTrustedAppSignerEntry('mac'); + + await endpointPolicyManagerSupertest[trustedAppApiCalls[0].method]( + trustedAppApiCalls[0].path + ) + .set('kbn-xsrf', 'true') + .send(body) + .expect(200); + }); + it(`should error on [${trustedAppApiCall.method}] if more than one OS is set`, async () => { const body = trustedAppApiCall.getBody(); From b5e0d05dd142527dbea5ddf4f4ad9ac77e734d33 Mon Sep 17 00:00:00 2001 From: Gerard Soldevila Date: Mon, 18 Nov 2024 14:34:54 +0100 Subject: [PATCH 24/50] Kibana Sustainable Architecture: Expose `StatusResponse` in core-status-common (#200524) ## Summary - Exposes the interfaces that define the format of the response of the `/api/status` endpoint. - Moves them from `@kbn/core-status-common-internal` to `@kbn/core-status-common`. - Removes the former package, as it no longer contains anything. - Fixes some of the illegal dependencies uncovered by https://github.com/elastic/kibana/pull/199630. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 - package.json | 1 - .../status/components/status_table.test.tsx | 2 +- .../status/components/version_header.test.tsx | 2 +- .../src/status/components/version_header.tsx | 2 +- .../src/status/lib/load_status.test.ts | 2 +- .../src/status/lib/load_status.ts | 4 ++-- .../src/status/lib/status_level.test.ts | 2 +- .../core-apps-browser-internal/tsconfig.json | 1 - .../core-status-common-internal/README.md | 3 --- .../core-status-common-internal/index.ts | 17 -------------- .../jest.config.js | 14 ------------ .../core-status-common-internal/kibana.jsonc | 5 ----- .../core-status-common-internal/package.json | 7 ------ .../core-status-common-internal/src/index.ts | 17 -------------- .../core-status-common-internal/tsconfig.json | 22 ------------------- .../core/status/core-status-common/index.ts | 13 +++++++++-- .../status/core-status-common/jest.config.js | 2 +- .../status/core-status-common/src/index.ts | 12 ---------- .../src/status.ts | 3 ++- .../status/core-status-common/tsconfig.json | 4 +++- .../src/routes/status.ts | 2 +- .../src/routes/status_response_schemas.ts | 6 ++--- .../core-status-server-internal/tsconfig.json | 1 - packages/kbn-manifest/index.ts | 2 +- .../public/progress_indicator.tsx | 2 +- src/plugins/interactive_setup/tsconfig.json | 2 +- tsconfig.base.json | 2 -- .../common/endpoint/utils/kibana_status.ts | 6 ++--- .../plugins/security_solution/tsconfig.json | 2 +- yarn.lock | 4 ---- 31 files changed, 35 insertions(+), 130 deletions(-) delete mode 100644 packages/core/status/core-status-common-internal/README.md delete mode 100644 packages/core/status/core-status-common-internal/index.ts delete mode 100644 packages/core/status/core-status-common-internal/jest.config.js delete mode 100644 packages/core/status/core-status-common-internal/kibana.jsonc delete mode 100644 packages/core/status/core-status-common-internal/package.json delete mode 100644 packages/core/status/core-status-common-internal/src/index.ts delete mode 100644 packages/core/status/core-status-common-internal/tsconfig.json delete mode 100644 packages/core/status/core-status-common/src/index.ts rename packages/core/status/{core-status-common-internal => core-status-common}/src/status.ts (92%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index ae55ea6b37108..c2450338f3e45 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -225,7 +225,6 @@ packages/core/security/core-security-server @elastic/kibana-core packages/core/security/core-security-server-internal @elastic/kibana-core packages/core/security/core-security-server-mocks @elastic/kibana-core packages/core/status/core-status-common @elastic/kibana-core -packages/core/status/core-status-common-internal @elastic/kibana-core packages/core/status/core-status-server @elastic/kibana-core packages/core/status/core-status-server-internal @elastic/kibana-core packages/core/status/core-status-server-mocks @elastic/kibana-core diff --git a/package.json b/package.json index acb1a118284ed..1114f3a94ca6e 100644 --- a/package.json +++ b/package.json @@ -384,7 +384,6 @@ "@kbn/core-security-server-internal": "link:packages/core/security/core-security-server-internal", "@kbn/core-security-server-mocks": "link:packages/core/security/core-security-server-mocks", "@kbn/core-status-common": "link:packages/core/status/core-status-common", - "@kbn/core-status-common-internal": "link:packages/core/status/core-status-common-internal", "@kbn/core-status-server": "link:packages/core/status/core-status-server", "@kbn/core-status-server-internal": "link:packages/core/status/core-status-server-internal", "@kbn/core-test-helpers-deprecations-getters": "link:packages/core/test-helpers/core-test-helpers-deprecations-getters", diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx index 38d69311d741e..b9949a6decf44 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/status_table.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { StatusTable } from './status_table'; const state = { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx index 62e48467ae51f..6180860df780d 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { mountWithIntl, findTestSubject } from '@kbn/test-jest-helpers'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; import { VersionHeader } from './version_header'; const buildServerVersion = (parts: Partial = {}): ServerVersion => ({ diff --git a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx index 0dc64a3cb7db0..15c1f9d07a273 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx +++ b/packages/core/apps/core-apps-browser-internal/src/status/components/version_header.tsx @@ -10,7 +10,7 @@ import React, { FC } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import type { ServerVersion } from '@kbn/core-status-common-internal'; +import type { ServerVersion } from '@kbn/core-status-common'; interface VersionHeaderProps { version: ServerVersion; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts index a63c5011dcaf8..c37db930de789 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.test.ts @@ -8,7 +8,7 @@ */ import { httpServiceMock } from '@kbn/core-http-browser-mocks'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { notificationServiceMock } from '@kbn/core-notifications-browser-mocks'; import { mocked } from '@kbn/core-metrics-collectors-server-mocks'; import { loadStatus } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts index f89e2196d2122..e8519030c3fdf 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/load_status.ts @@ -11,11 +11,11 @@ import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; import type { HttpSetup } from '@kbn/core-http-browser'; import type { NotificationsSetup } from '@kbn/core-notifications-browser'; -import type { ServiceStatusLevelId } from '@kbn/core-status-common'; import type { + ServiceStatusLevelId, StatusResponse, StatusInfoServiceStatus as ServiceStatus, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; import type { DataType } from './format_number'; interface MetricMeta { diff --git a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts index 3d393bd8e4719..290845c4bdd08 100644 --- a/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts +++ b/packages/core/apps/core-apps-browser-internal/src/status/lib/status_level.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common-internal'; +import type { StatusInfoServiceStatus as ServiceStatus } from '@kbn/core-status-common'; import { getLevelSortValue, groupByLevel, getHighestStatus } from './status_level'; import { FormattedStatus, StatusState } from './load_status'; diff --git a/packages/core/apps/core-apps-browser-internal/tsconfig.json b/packages/core/apps/core-apps-browser-internal/tsconfig.json index a18bb3421a1f4..9902b12732760 100644 --- a/packages/core/apps/core-apps-browser-internal/tsconfig.json +++ b/packages/core/apps/core-apps-browser-internal/tsconfig.json @@ -24,7 +24,6 @@ "@kbn/core-application-browser", "@kbn/core-application-browser-internal", "@kbn/core-mount-utils-browser-internal", - "@kbn/core-status-common-internal", "@kbn/core-http-browser-internal", "@kbn/core-application-browser-mocks", "@kbn/core-notifications-browser-mocks", diff --git a/packages/core/status/core-status-common-internal/README.md b/packages/core/status/core-status-common-internal/README.md deleted file mode 100644 index f4e4af7fd3b3a..0000000000000 --- a/packages/core/status/core-status-common-internal/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @kbn/core-status-common-internal - -This package contains the common internal types for Core's `status` domain. diff --git a/packages/core/status/core-status-common-internal/index.ts b/packages/core/status/core-status-common-internal/index.ts deleted file mode 100644 index f6a7a29056145..0000000000000 --- a/packages/core/status/core-status-common-internal/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusInfo, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './src'; diff --git a/packages/core/status/core-status-common-internal/jest.config.js b/packages/core/status/core-status-common-internal/jest.config.js deleted file mode 100644 index bc848cd656199..0000000000000 --- a/packages/core/status/core-status-common-internal/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -module.exports = { - preset: '@kbn/test', - rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], -}; diff --git a/packages/core/status/core-status-common-internal/kibana.jsonc b/packages/core/status/core-status-common-internal/kibana.jsonc deleted file mode 100644 index 20ce17ae3cefa..0000000000000 --- a/packages/core/status/core-status-common-internal/kibana.jsonc +++ /dev/null @@ -1,5 +0,0 @@ -{ - "type": "shared-common", - "id": "@kbn/core-status-common-internal", - "owner": "@elastic/kibana-core" -} diff --git a/packages/core/status/core-status-common-internal/package.json b/packages/core/status/core-status-common-internal/package.json deleted file mode 100644 index d2c456b6dc96a..0000000000000 --- a/packages/core/status/core-status-common-internal/package.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "name": "@kbn/core-status-common-internal", - "private": true, - "version": "1.0.0", - "author": "Kibana Core", - "license": "Elastic License 2.0 OR AGPL-3.0-only OR SSPL-1.0" -} \ No newline at end of file diff --git a/packages/core/status/core-status-common-internal/src/index.ts b/packages/core/status/core-status-common-internal/src/index.ts deleted file mode 100644 index 60c51dcf47632..0000000000000 --- a/packages/core/status/core-status-common-internal/src/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export type { - StatusInfo, - StatusInfoCoreStatus, - StatusInfoServiceStatus, - StatusResponse, - ServerVersion, - ServerMetrics, -} from './status'; diff --git a/packages/core/status/core-status-common-internal/tsconfig.json b/packages/core/status/core-status-common-internal/tsconfig.json deleted file mode 100644 index 7d31fa090eb0f..0000000000000 --- a/packages/core/status/core-status-common-internal/tsconfig.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "extends": "../../../../tsconfig.base.json", - "compilerOptions": { - "outDir": "target/types", - "types": [ - "jest", - "node" - ] - }, - "include": [ - "**/*.ts", - "**/*.tsx", - ], - "kbn_references": [ - "@kbn/core-status-common", - "@kbn/core-metrics-server", - "@kbn/config" - ], - "exclude": [ - "target/**/*", - ] -} diff --git a/packages/core/status/core-status-common/index.ts b/packages/core/status/core-status-common/index.ts index 50eb85608522e..1aae83558016a 100644 --- a/packages/core/status/core-status-common/index.ts +++ b/packages/core/status/core-status-common/index.ts @@ -7,5 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ServiceStatusLevels } from './src'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId, CoreStatus } from './src'; +export { ServiceStatusLevels } from './src/service_status'; +export type { CoreStatus } from './src/core_status'; +export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './src/service_status'; +export type { + StatusInfo, + StatusInfoCoreStatus, + StatusInfoServiceStatus, + StatusResponse, + ServerVersion, + ServerMetrics, +} from './src/status'; diff --git a/packages/core/status/core-status-common/jest.config.js b/packages/core/status/core-status-common/jest.config.js index bc848cd656199..48ce844bb7d3f 100644 --- a/packages/core/status/core-status-common/jest.config.js +++ b/packages/core/status/core-status-common/jest.config.js @@ -10,5 +10,5 @@ module.exports = { preset: '@kbn/test', rootDir: '../../../..', - roots: ['/packages/core/status/core-status-common-internal'], + roots: ['/packages/core/status/core-status-common'], }; diff --git a/packages/core/status/core-status-common/src/index.ts b/packages/core/status/core-status-common/src/index.ts deleted file mode 100644 index 7cfcc7dbf79a8..0000000000000 --- a/packages/core/status/core-status-common/src/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the "Elastic License - * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side - * Public License v 1"; you may not use this file except in compliance with, at - * your election, the "Elastic License 2.0", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -export { ServiceStatusLevels } from './service_status'; -export type { ServiceStatus, ServiceStatusLevel, ServiceStatusLevelId } from './service_status'; -export type { CoreStatus } from './core_status'; diff --git a/packages/core/status/core-status-common-internal/src/status.ts b/packages/core/status/core-status-common/src/status.ts similarity index 92% rename from packages/core/status/core-status-common-internal/src/status.ts rename to packages/core/status/core-status-common/src/status.ts index 370d2c9ac6e5d..7c981c56ceeb3 100644 --- a/packages/core/status/core-status-common-internal/src/status.ts +++ b/packages/core/status/core-status-common/src/status.ts @@ -8,8 +8,9 @@ */ import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus, CoreStatus } from '@kbn/core-status-common'; import type { OpsMetrics } from '@kbn/core-metrics-server'; +import type { ServiceStatusLevelId, ServiceStatus } from './service_status'; +import type { CoreStatus } from './core_status'; export interface StatusInfoServiceStatus extends Omit { level: ServiceStatusLevelId; diff --git a/packages/core/status/core-status-common/tsconfig.json b/packages/core/status/core-status-common/tsconfig.json index a63f70f93043d..3b61a574a06bb 100644 --- a/packages/core/status/core-status-common/tsconfig.json +++ b/packages/core/status/core-status-common/tsconfig.json @@ -12,7 +12,9 @@ "**/*.tsx", ], "kbn_references": [ - "@kbn/std" + "@kbn/std", + "@kbn/config", + "@kbn/core-metrics-server" ], "exclude": [ "target/**/*", diff --git a/packages/core/status/core-status-server-internal/src/routes/status.ts b/packages/core/status/core-status-server-internal/src/routes/status.ts index 87e0e6e745a92..bafda87c2b08d 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status.ts @@ -15,7 +15,7 @@ import type { IRouter } from '@kbn/core-http-server'; import type { MetricsServiceSetup } from '@kbn/core-metrics-server'; import type { CoreIncrementUsageCounter } from '@kbn/core-usage-data-server'; import { type ServiceStatus, type CoreStatus, ServiceStatusLevels } from '@kbn/core-status-common'; -import { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { calculateLegacyStatus, type LegacyStatusInfo } from '../legacy_status'; import { statusResponse, type RedactedStatusHttpBody } from './status_response_schemas'; diff --git a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts index a2dcbcf7d21b6..68cebab4392e0 100644 --- a/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts +++ b/packages/core/status/core-status-server-internal/src/routes/status_response_schemas.ts @@ -9,15 +9,15 @@ import { schema, type Type, type TypeOf } from '@kbn/config-schema'; import type { BuildFlavor } from '@kbn/config'; -import type { ServiceStatusLevelId, ServiceStatus } from '@kbn/core-status-common'; - import type { + ServiceStatusLevelId, + ServiceStatus, StatusResponse, StatusInfoCoreStatus, ServerMetrics, StatusInfo, ServerVersion, -} from '@kbn/core-status-common-internal'; +} from '@kbn/core-status-common'; const serviceStatusLevelId: () => Type = () => schema.oneOf( diff --git a/packages/core/status/core-status-server-internal/tsconfig.json b/packages/core/status/core-status-server-internal/tsconfig.json index bda646809e414..5ca46556cac33 100644 --- a/packages/core/status/core-status-server-internal/tsconfig.json +++ b/packages/core/status/core-status-server-internal/tsconfig.json @@ -29,7 +29,6 @@ "@kbn/core-saved-objects-server-internal", "@kbn/core-status-server", "@kbn/core-status-common", - "@kbn/core-status-common-internal", "@kbn/core-usage-data-base-server-internal", "@kbn/core-base-server-mocks", "@kbn/core-environment-server-mocks", diff --git a/packages/kbn-manifest/index.ts b/packages/kbn-manifest/index.ts index 5fc4727a1a72d..ce890742ea61f 100644 --- a/packages/kbn-manifest/index.ts +++ b/packages/kbn-manifest/index.ts @@ -37,7 +37,7 @@ export const runKbnManifestCli = () => { --list all List all the manifests --package [packageId] Select a package to update. --plugin [pluginId] Select a plugin to update. - --set [property] [value] Set the desired "[property]": "[value]" + --set [property]=[value] Set the desired "[property]": "[value]" --unset [property] Removes the desired "[property]: value" from the manifest `, }, diff --git a/src/plugins/interactive_setup/public/progress_indicator.tsx b/src/plugins/interactive_setup/public/progress_indicator.tsx index 6bb87a792e809..be094f9ef7e8d 100644 --- a/src/plugins/interactive_setup/public/progress_indicator.tsx +++ b/src/plugins/interactive_setup/public/progress_indicator.tsx @@ -15,7 +15,7 @@ import useAsyncFn from 'react-use/lib/useAsyncFn'; import useTimeoutFn from 'react-use/lib/useTimeoutFn'; import type { IHttpFetchError } from '@kbn/core-http-browser'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { i18n } from '@kbn/i18n'; import { useKibana } from './use_kibana'; diff --git a/src/plugins/interactive_setup/tsconfig.json b/src/plugins/interactive_setup/tsconfig.json index 51fff541980cc..048143fd464e0 100644 --- a/src/plugins/interactive_setup/tsconfig.json +++ b/src/plugins/interactive_setup/tsconfig.json @@ -14,7 +14,7 @@ "@kbn/i18n", "@kbn/ui-theme", "@kbn/core-http-browser", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/safer-lodash-set", "@kbn/test-jest-helpers", "@kbn/config-schema", diff --git a/tsconfig.base.json b/tsconfig.base.json index 223b2d5a58ca2..f6aaa2ee0ac7f 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -606,8 +606,6 @@ "@kbn/core-security-server-mocks/*": ["packages/core/security/core-security-server-mocks/*"], "@kbn/core-status-common": ["packages/core/status/core-status-common"], "@kbn/core-status-common/*": ["packages/core/status/core-status-common/*"], - "@kbn/core-status-common-internal": ["packages/core/status/core-status-common-internal"], - "@kbn/core-status-common-internal/*": ["packages/core/status/core-status-common-internal/*"], "@kbn/core-status-server": ["packages/core/status/core-status-server"], "@kbn/core-status-server/*": ["packages/core/status/core-status-server/*"], "@kbn/core-status-server-internal": ["packages/core/status/core-status-server-internal"], diff --git a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts index 78147439eedd9..f1fcac6e758c0 100644 --- a/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts +++ b/x-pack/plugins/security_solution/common/endpoint/utils/kibana_status.ts @@ -7,7 +7,7 @@ import { KbnClient } from '@kbn/test'; import type { Client } from '@elastic/elasticsearch'; -import type { StatusResponse } from '@kbn/core-status-common-internal'; +import type { StatusResponse } from '@kbn/core-status-common'; import { catchAxiosErrorFormatAndThrow } from '../format_axios_error'; export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise => { @@ -15,11 +15,11 @@ export const fetchKibanaStatus = async (kbnClient: KbnClient): Promise({ method: 'GET', path: '/api/status', }) - .then((response) => response.data as StatusResponse) + .then(({ data }) => data) .catch(catchAxiosErrorFormatAndThrow); }; /** diff --git a/x-pack/plugins/security_solution/tsconfig.json b/x-pack/plugins/security_solution/tsconfig.json index c0b84c2b70a84..cbd992ea78d8b 100644 --- a/x-pack/plugins/security_solution/tsconfig.json +++ b/x-pack/plugins/security_solution/tsconfig.json @@ -126,7 +126,7 @@ "@kbn/dev-cli-errors", "@kbn/dev-utils", "@kbn/tooling-log", - "@kbn/core-status-common-internal", + "@kbn/core-status-common", "@kbn/repo-info", "@kbn/storybook", "@kbn/controls-plugin", diff --git a/yarn.lock b/yarn.lock index b69c7755172e4..c2add93693fe7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4458,10 +4458,6 @@ version "0.0.0" uid "" -"@kbn/core-status-common-internal@link:packages/core/status/core-status-common-internal": - version "0.0.0" - uid "" - "@kbn/core-status-common@link:packages/core/status/core-status-common": version "0.0.0" uid "" From b878274b432c283a41eadfab4fa962c9b65d11bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Loix?= Date: Mon, 18 Nov 2024 13:35:26 +0000 Subject: [PATCH 25/50] [EuiInMemoryTable] Persist table rows per page and sort (#198297) --- .../public/examples/finder/finder_app.tsx | 1 + .../table_list_view_table/src/mocks.tsx | 20 ++++ .../src/table_list_view.test.tsx | 56 ++++++++- .../src/table_list_view_table.tsx | 19 ++- .../table_list_view_table/tsconfig.json | 4 +- .../src/alert_fields_table/index.tsx | 39 ++---- packages/kbn-alerts-ui-shared/tsconfig.json | 3 +- .../src/editor_footer/query_history.tsx | 27 ++--- packages/kbn-esql-editor/tsconfig.json | 1 + ...t_annotation_group_saved_object_finder.tsx | 1 + packages/shared-ux/table_persist/index.ts | 3 +- packages/shared-ux/table_persist/src/index.ts | 6 + .../src/table_persist_hoc.test.tsx | 113 ++++++++++++++++++ .../table_persist/src/table_persist_hoc.tsx | 74 ++++++++++++ .../src/use_table_persist.test.ts | 10 +- .../table_persist/src/use_table_persist.ts | 36 +++++- .../sessions_mgmt/components/table/table.tsx | 20 +++- .../components/data_table.tsx | 29 ++++- src/plugins/data/tsconfig.json | 3 +- .../table/__snapshots__/table.test.tsx.snap | 5 +- .../components/table/table.test.tsx | 16 ++- .../components/table/table.tsx | 34 +++++- .../relationships_table.tsx | 11 +- .../table/__snapshots__/table.test.tsx.snap | 12 +- .../components/table/table.test.tsx | 17 ++- .../components/table/table.tsx | 37 +++++- .../table/__snapshots__/table.test.tsx.snap | 12 +- .../components/table/table.test.tsx | 28 +++-- .../components/table/table.tsx | 35 +++++- .../index_pattern_table.tsx | 19 ++- .../data_view_management/tsconfig.json | 1 + .../open_search_panel.test.tsx.snap | 1 + .../components/top_nav/open_search_panel.tsx | 1 + .../add_panel_flyout/add_panel_flyout.tsx | 1 + ...t_annotation_group_saved_object_finder.tsx | 1 + .../public/finder/index.tsx | 7 +- .../finder/saved_object_finder.test.tsx | 62 +++++++++- .../public/finder/saved_object_finder.tsx | 51 +++++--- .../saved_objects_finder/tsconfig.json | 1 + .../__snapshots__/flyout.test.tsx.snap | 5 - .../__snapshots__/relationships.test.tsx.snap | 35 +++++- .../objects_table/components/flyout.test.tsx | 18 ++- .../objects_table/components/flyout.tsx | 43 +++---- .../components/relationships.test.tsx | 22 ++-- .../components/relationships.tsx | 28 ++++- .../saved_objects_management/tsconfig.json | 1 + .../search_selection/search_selection.tsx | 1 + .../_area_chart.ts | 3 + test/functional/services/inspector.ts | 3 +- .../embeddable_flyout/flyout.component.tsx | 1 + .../markdown_editor/plugins/lens/plugin.tsx | 1 + .../graph/public/components/source_picker.tsx | 1 + .../source_selection/source_selection.tsx | 1 + .../data_drift/index_patterns_picker.tsx | 1 + .../components/data_view/change_data_view.tsx | 1 + .../new_job/pages/index_or_search/page.tsx | 1 + .../search_selection/search_selection.tsx | 1 + 57 files changed, 798 insertions(+), 186 deletions(-) create mode 100644 packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx create mode 100644 packages/shared-ux/table_persist/src/table_persist_hoc.tsx diff --git a/examples/content_management_examples/public/examples/finder/finder_app.tsx b/examples/content_management_examples/public/examples/finder/finder_app.tsx index 99ec949fac7d1..b8aaa6fe5f34b 100644 --- a/examples/content_management_examples/public/examples/finder/finder_app.tsx +++ b/examples/content_management_examples/public/examples/finder/finder_app.tsx @@ -23,6 +23,7 @@ export const FinderApp = (props: { ({ defaultValue: false, }, }); + +export const localStorageMock = (): IStorage => { + let store: Record = {}; + + return { + getItem: (key: string) => { + return store[key] || null; + }, + setItem: (key: string, value: unknown) => { + store[key] = value; + }, + clear() { + store = {}; + }, + removeItem(key: string) { + delete store[key]; + }, + }; +}; diff --git a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx index 38229399f2ec8..aebaca335db5f 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view.test.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view.test.tsx @@ -18,7 +18,7 @@ import type { LocationDescriptor, History } from 'history'; import type { UserContentCommonSchema } from '@kbn/content-management-table-list-view-common'; import { WithServices } from './__jest__'; -import { getTagList } from './mocks'; +import { getTagList, localStorageMock } from './mocks'; import { TableListViewTable, type TableListViewTableProps } from './table_list_view_table'; import { getActions } from './table_list_view.test.helpers'; import type { Services } from './services'; @@ -335,6 +335,12 @@ describe('TableListView', () => { const totalItems = 30; const updatedAt = new Date().toISOString(); + beforeEach(() => { + Object.defineProperty(window, 'localStorage', { + value: localStorageMock(), + }); + }); + const hits: UserContentCommonSchema[] = [...Array(totalItems)].map((_, i) => ({ id: `item${i}`, type: 'dashboard', @@ -429,6 +435,54 @@ describe('TableListView', () => { expect(firstRowTitle).toBe('Item 20'); expect(lastRowTitle).toBe('Item 29'); }); + + test('should persist the number of rows in the table', async () => { + let testBed: TestBed; + + const tableId = 'myTable'; + + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + { + const { component, table, find } = testBed!; + component.update(); + + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(20); // 20 by default + + let storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).toBe(null); + + find('tablePaginationPopoverButton').simulate('click'); + find('tablePagination-10-rows').simulate('click'); + + storageValue = localStorage.getItem(`tablePersist:${tableId}`); + expect(storageValue).not.toBe(null); + expect(JSON.parse(storageValue!).pageSize).toBe(10); + } + + // Mount a second table and verify that is shows only 10 rows + { + await act(async () => { + testBed = await setup({ + initialPageSize, + findItems: jest.fn().mockResolvedValue({ total: hits.length, hits: [...hits] }), + id: tableId, + }); + }); + + const { component, table } = testBed!; + component.update(); + const { tableCellsValues } = table.getMetaData('itemsInMemTable'); + expect(tableCellsValues.length).toBe(10); // 10 items this time + } + }); }); describe('column sorting', () => { diff --git a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx index 1fe5123d54151..c7653c668f0df 100644 --- a/packages/content-management/table_list_view_table/src/table_list_view_table.tsx +++ b/packages/content-management/table_list_view_table/src/table_list_view_table.tsx @@ -43,6 +43,7 @@ import { ContentInsightsProvider, useContentInsightsServices, } from '@kbn/content-management-content-insights-public'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { Table, @@ -443,7 +444,7 @@ function TableListViewTableComp({ hasUpdatedAtMetadata, hasCreatedByMetadata, hasRecentlyAccessedMetadata, - pagination, + pagination: _pagination, tableSort, tableFilter, } = state; @@ -903,7 +904,7 @@ function TableListViewTableComp({ [updateTableSortFilterAndPagination] ); - const onTableChange = useCallback( + const customOnTableChange = useCallback( (criteria: CriteriaWithPagination) => { const data: { sort?: State['tableSort']; @@ -1038,6 +1039,20 @@ function TableListViewTableComp({ ); }, [entityName, fetchError]); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: listingId, + initialPageSize, + customOnTableChange, + pageSizeOptions: uniq([10, 20, 50, initialPageSize]).sort(), + }); + + const pagination = useMemo(() => { + return { + ..._pagination, + pageSize, + }; + }, [_pagination, pageSize]); + // ------------ // Effects // ------------ diff --git a/packages/content-management/table_list_view_table/tsconfig.json b/packages/content-management/table_list_view_table/tsconfig.json index a5530ee717e49..90a96953570fb 100644 --- a/packages/content-management/table_list_view_table/tsconfig.json +++ b/packages/content-management/table_list_view_table/tsconfig.json @@ -37,7 +37,9 @@ "@kbn/content-management-user-profiles", "@kbn/recently-accessed", "@kbn/content-management-content-insights-public", - "@kbn/content-management-favorites-public" + "@kbn/content-management-favorites-public", + "@kbn/kibana-utils-plugin", + "@kbn/shared-ux-table-persist" ], "exclude": [ "target/**/*" diff --git a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx index 3f3940e98bf4a..3da86b5f848f7 100644 --- a/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx +++ b/packages/kbn-alerts-ui-shared/src/alert_fields_table/index.tsx @@ -13,12 +13,13 @@ import { EuiTabbedContent, EuiTabbedContentProps, useEuiOverflowScroll, + EuiBasicTableColumn, } from '@elastic/eui'; import { css } from '@emotion/react'; -import React, { memo, useCallback, useMemo, useState } from 'react'; +import React, { memo, useMemo } from 'react'; import { Alert } from '@kbn/alerting-types'; import { euiThemeVars } from '@kbn/ui-theme'; -import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; export const search = { box: { @@ -66,28 +67,6 @@ export const ScrollableFlyoutTabbedContent = (props: EuiTabbedContentProps) => ( const COUNT_PER_PAGE_OPTIONS = [25, 50, 100]; -const useFieldBrowserPagination = () => { - const [pagination, setPagination] = useState<{ pageIndex: number }>({ - pageIndex: 0, - }); - - const onTableChange = useCallback(({ page: { index } }: { page: { index: number } }) => { - setPagination({ pageIndex: index }); - }, []); - const paginationTableProp = useMemo( - () => ({ - ...pagination, - pageSizeOptions: COUNT_PER_PAGE_OPTIONS, - }), - [pagination] - ); - - return { - onTableChange, - paginationTableProp, - }; -}; - type AlertField = Exclude< { [K in keyof Alert]: { key: K; value: Alert[K] }; @@ -111,7 +90,11 @@ export interface AlertFieldsTableProps { * A paginated, filterable table to show alert object fields */ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) => { - const { onTableChange, paginationTableProp } = useFieldBrowserPagination(); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'obltAlertFields', + initialPageSize: 25, + }); + const items = useMemo(() => { let _items = Object.entries(alert).map( ([key, value]) => @@ -131,7 +114,11 @@ export const AlertFieldsTable = memo(({ alert, fields }: AlertFieldsTableProps) itemId="key" columns={columns} onTableChange={onTableChange} - pagination={paginationTableProp} + pagination={{ + pageSize, + pageSizeOptions: COUNT_PER_PAGE_OPTIONS, + }} + sorting={sorting} search={search} css={css` & .euiTableRow { diff --git a/packages/kbn-alerts-ui-shared/tsconfig.json b/packages/kbn-alerts-ui-shared/tsconfig.json index 0da17dfe3d1ac..317f80dd209f3 100644 --- a/packages/kbn-alerts-ui-shared/tsconfig.json +++ b/packages/kbn-alerts-ui-shared/tsconfig.json @@ -49,6 +49,7 @@ "@kbn/core-ui-settings-browser", "@kbn/core-http-browser-mocks", "@kbn/core-notifications-browser-mocks", - "@kbn/kibana-react-plugin" + "@kbn/kibana-react-plugin", + "@kbn/shared-ux-table-persist" ] } diff --git a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx index 864306737e9ca..7316a5b49ddea 100644 --- a/packages/kbn-esql-editor/src/editor_footer/query_history.tsx +++ b/packages/kbn-esql-editor/src/editor_footer/query_history.tsx @@ -17,7 +17,6 @@ import { EuiInMemoryTable, EuiBasicTableColumn, EuiButtonEmpty, - Criteria, EuiButtonIcon, CustomItemAction, EuiCopy, @@ -25,6 +24,7 @@ import { euiScrollBarStyles, } from '@elastic/eui'; import { css, Interpolation, Theme } from '@emotion/react'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { type QueryHistoryItem, getHistoryItems } from '../history_local_storage'; import { getReducedSpaceStyling, swapArrayElements } from './query_history_helpers'; @@ -212,8 +212,16 @@ export function QueryHistory({ }) { const theme = useEuiTheme(); const scrollBarStyles = euiScrollBarStyles(theme); - const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); - const historyItems: QueryHistoryItem[] = getHistoryItems(sortDirection); + + const { sorting, onTableChange } = useEuiTablePersist({ + tableId: 'esqlQueryHistory', + initialSort: { + field: 'timeRan', + direction: 'desc', + }, + }); + + const historyItems: QueryHistoryItem[] = getHistoryItems(sorting.sort.direction); const actions: Array> = useMemo(() => { return [ @@ -276,19 +284,6 @@ export function QueryHistory({ return getTableColumns(containerWidth, isOnReducedSpaceLayout, actions); }, [actions, containerWidth, isOnReducedSpaceLayout]); - const onTableChange = ({ page, sort }: Criteria) => { - if (sort) { - const { direction } = sort; - setSortDirection(direction); - } - }; - - const sorting = { - sort: { - field: 'timeRan', - direction: sortDirection, - }, - }; const { euiTheme } = theme; const extraStyling = isOnReducedSpaceLayout ? getReducedSpaceStyling() : ''; diff --git a/packages/kbn-esql-editor/tsconfig.json b/packages/kbn-esql-editor/tsconfig.json index c26b971e5231c..075c5ff9ab457 100644 --- a/packages/kbn-esql-editor/tsconfig.json +++ b/packages/kbn-esql-editor/tsconfig.json @@ -28,6 +28,7 @@ "@kbn/fields-metadata-plugin", "@kbn/esql-validation-autocomplete", "@kbn/esql-utils", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx index 2f9c7afcd2190..38a701abdd81c 100644 --- a/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx +++ b/packages/kbn-event-annotation-components/components/event_annotation_group_saved_object_finder.tsx @@ -100,6 +100,7 @@ export const EventAnnotationGroupSavedObjectFinder = ({ ) : ( { onChoose({ id, type, fullName, savedObject }); diff --git a/packages/shared-ux/table_persist/index.ts b/packages/shared-ux/table_persist/index.ts index da2596dac14ab..c46f6a8e8a00a 100644 --- a/packages/shared-ux/table_persist/index.ts +++ b/packages/shared-ux/table_persist/index.ts @@ -7,4 +7,5 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS } from './src'; +export { useEuiTablePersist, DEFAULT_PAGE_SIZE_OPTIONS, withEuiTablePersist } from './src'; +export type { EuiTablePersistInjectedProps, EuiTablePersistPropsGetter, HOCProps } from './src'; diff --git a/packages/shared-ux/table_persist/src/index.ts b/packages/shared-ux/table_persist/src/index.ts index 20416b5cec902..33ac8b18d1d34 100644 --- a/packages/shared-ux/table_persist/src/index.ts +++ b/packages/shared-ux/table_persist/src/index.ts @@ -9,3 +9,9 @@ export { useEuiTablePersist } from './use_table_persist'; export { DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; +export { withEuiTablePersist } from './table_persist_hoc'; +export type { + EuiTablePersistInjectedProps, + EuiTablePersistPropsGetter, + HOCProps, +} from './table_persist_hoc'; diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx new file mode 100644 index 0000000000000..bed84111379bb --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.test.tsx @@ -0,0 +1,113 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import React, { PureComponent } from 'react'; +import { render, screen } from '@testing-library/react'; + +import { withEuiTablePersist, type EuiTablePersistInjectedProps } from './table_persist_hoc'; + +const mockUseEuiTablePersist = jest.fn().mockReturnValue({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', +}); + +jest.mock('./use_table_persist', () => { + const original = jest.requireActual('./use_table_persist'); + + return { + ...original, + useEuiTablePersist: (...args: unknown[]) => mockUseEuiTablePersist(...args), + }; +}); + +class TestComponent extends PureComponent> { + constructor(props: EuiTablePersistInjectedProps) { + super(props); + } + + render() { + return
{JSON.stringify(this.props.euiTablePersist)}
; + } +} + +describe('withEuiTablePersist', () => { + it('should call useEuiTablePersist and return its values', () => { + const customOnTableChange = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange, + pageSizeOptions, + }); + + render(); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableId', + customOnTableChange, + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + pageSizeOptions, + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); + + it('should allow override through props', () => { + const customOnTableChangeDefault = jest.fn(); + const customOnTableChangeProp = jest.fn(); + const pageSizeOptions = [5, 10, 25, 50]; + + const WrappedComponent = withEuiTablePersist(TestComponent, { + tableId: 'testTableId', + initialPageSize: 10, + initialSort: { field: 'testField', direction: 'asc' }, + customOnTableChange: customOnTableChangeDefault, + pageSizeOptions, + }); + + render( + + ); + + expect(mockUseEuiTablePersist).toHaveBeenCalledWith({ + tableId: 'testTableIdChanged', + customOnTableChange: customOnTableChangeProp, + initialPageSize: 20, + initialSort: { field: 'testFieldChanged', direction: 'desc' }, + pageSizeOptions: [5], + }); + + expect(screen.getByTestId('value').textContent).toBe( + JSON.stringify({ + pageSize: 'mockPageSize', + sorting: 'mockSorting', + onTableChange: 'mockOnTableChange', + }) + ); + }); +}); diff --git a/packages/shared-ux/table_persist/src/table_persist_hoc.tsx b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx new file mode 100644 index 0000000000000..313cf1a1a21a2 --- /dev/null +++ b/packages/shared-ux/table_persist/src/table_persist_hoc.tsx @@ -0,0 +1,74 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ +import React from 'react'; +import { type CriteriaWithPagination } from '@elastic/eui'; +import { EuiTablePersistProps, useEuiTablePersist } from './use_table_persist'; +import { PropertySort } from './types'; + +export interface EuiTablePersistInjectedProps { + euiTablePersist: { + /** The EuiInMemoryTable onTableChange prop */ + onTableChange: (change: CriteriaWithPagination) => void; + /** The EuiInMemoryTable sorting prop */ + sorting: { sort: PropertySort } | true; + /** The EuiInMemoryTable pagination.pageSize value */ + pageSize: number; + }; +} + +export type EuiTablePersistPropsGetter = ( + props: Omit> +) => EuiTablePersistProps; + +export type HOCProps = P & { + /** Custom value for the EuiTablePersist HOC */ + euiTablePersistProps?: Partial>; +}; + +export function withEuiTablePersist( + WrappedComponent: React.ComponentClass>, + euiTablePersistDefault: + | (EuiTablePersistProps & { get?: undefined }) + | { + get: EuiTablePersistPropsGetter; + } +) { + const HOC: React.FC>>> = ( + props + ) => { + const getterOverride = euiTablePersistDefault.get ? euiTablePersistDefault.get(props) : {}; + + const mergedProps = { + ...euiTablePersistDefault, + ...props.euiTablePersistProps, + ...getterOverride, // Getter override other props + }; + + const { tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions } = + mergedProps; + + if (!tableId) { + throw new Error('tableId is required'); + } + + const euiTablePersist = useEuiTablePersist({ + tableId, + customOnTableChange, + initialSort, + initialPageSize, + pageSizeOptions, + }); + + const { euiTablePersistProps, ...rest } = props; + + return ; + }; + + return HOC; +} diff --git a/packages/shared-ux/table_persist/src/use_table_persist.test.ts b/packages/shared-ux/table_persist/src/use_table_persist.test.ts index 235777aa5d294..51fbd93f7a214 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.test.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.test.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { Criteria } from '@elastic/eui'; +import { CriteriaWithPagination } from '@elastic/eui'; import { renderHook, act } from '@testing-library/react-hooks'; import { useEuiTablePersist } from './use_table_persist'; import { createStorage } from './storage'; // Mock this if it's external @@ -58,7 +58,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); @@ -85,7 +85,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(customOnTableChange).toHaveBeenCalledWith(nextCriteria); @@ -98,7 +98,7 @@ describe('useEuiTablePersist', () => { const { result } = renderHook(() => useEuiTablePersist({ tableId: 'testTable' })); act(() => { - result.current.onTableChange({}); // Empty change + result.current.onTableChange({} as CriteriaWithPagination); // Empty change }); expect(result.current.pageSize).toBe(25); @@ -118,7 +118,7 @@ describe('useEuiTablePersist', () => { }; act(() => { - result.current.onTableChange(nextCriteria as Criteria); + result.current.onTableChange(nextCriteria as CriteriaWithPagination); }); expect(result.current.pageSize).toBe(100); diff --git a/packages/shared-ux/table_persist/src/use_table_persist.ts b/packages/shared-ux/table_persist/src/use_table_persist.ts index 9c3b7a75788b0..bf91f66beb292 100644 --- a/packages/shared-ux/table_persist/src/use_table_persist.ts +++ b/packages/shared-ux/table_persist/src/use_table_persist.ts @@ -8,7 +8,7 @@ */ import { useState, useCallback } from 'react'; -import { Criteria } from '@elastic/eui'; +import type { CriteriaWithPagination } from '@elastic/eui'; import { DEFAULT_INITIAL_PAGE_SIZE, DEFAULT_PAGE_SIZE_OPTIONS } from './constants'; import { createStorage } from './storage'; import { validatePersistData } from './validate_persist_data'; @@ -18,7 +18,7 @@ export interface EuiTablePersistProps { /** A unique id that will be included in the local storage variable for this table. */ tableId: string; /** (Optional) Specifies a custom onTableChange handler. */ - customOnTableChange?: (change: Criteria) => void; + customOnTableChange?: (change: CriteriaWithPagination) => void; /** (Optional) Specifies a custom initial table sorting. */ initialSort?: PropertySort; /** (Optional) Specifies a custom initial page size for the table. Defaults to 50. */ @@ -33,13 +33,37 @@ export interface EuiTablePersistProps { * Returns the persisting page size and sort and the onTableChange handler that should be passed * as props to an Eui table component. */ -export const useEuiTablePersist = ({ +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort: PropertySort } +): { + sorting: { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps & { initialSort?: undefined } +): { + sorting: true; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist( + props: EuiTablePersistProps +): { + sorting: true | { sort: PropertySort }; + pageSize: number; + onTableChange: (nextValues: CriteriaWithPagination) => void; +}; + +export function useEuiTablePersist({ tableId, customOnTableChange, initialSort, initialPageSize, pageSizeOptions, -}: EuiTablePersistProps) => { +}: EuiTablePersistProps) { const storage = createStorage(); const storedPersistData = storage.get(tableId, undefined); @@ -55,7 +79,7 @@ export const useEuiTablePersist = ({ const sorting = sort ? { sort } : true; // If sort is undefined, return true to allow sorting const onTableChange = useCallback( - (nextValues: Criteria) => { + (nextValues: CriteriaWithPagination) => { if (customOnTableChange) { customOnTableChange(nextValues); } @@ -92,4 +116,4 @@ export const useEuiTablePersist = ({ ); return { pageSize, sorting, onTableChange }; -}; +} diff --git a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx index 211fcdcd50235..3e41263c8750f 100644 --- a/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx +++ b/src/plugins/data/public/search/session/sessions_mgmt/components/table/table.tsx @@ -13,6 +13,7 @@ import { CoreStart } from '@kbn/core/public'; import moment from 'moment'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { TableText } from '..'; import { SEARCH_SESSIONS_TABLE_ID } from '../../../../../../common'; import { SearchSessionsMgmtAPI } from '../../lib/api'; @@ -45,7 +46,6 @@ export function SearchSessionsMgmtTable({ const [tableData, setTableData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); - const [pagination, setPagination] = useState({ pageIndex: 0 }); const showLatestResultsHandler = useRef(); const refreshTimeoutRef = useRef(null); const refreshInterval = useMemo( @@ -53,6 +53,14 @@ export function SearchSessionsMgmtTable({ [config.management.refreshInterval] ); + const { pageSize, sorting, onTableChange } = useEuiTablePersist({ + tableId: 'searchSessionsMgmt', + initialSort: { + field: 'created', + direction: 'desc', + }, + }); + // Debounce rendering the state of the Refresh button useDebounce( () => { @@ -148,12 +156,12 @@ export function SearchSessionsMgmtTable({ searchUsageCollector )} items={tableData} - pagination={pagination} - search={search} - sorting={{ sort: { field: 'created', direction: 'desc' } }} - onTableChange={({ page: { index } }) => { - setPagination({ pageIndex: index }); + pagination={{ + pageSize, }} + search={search} + sorting={sorting} + onTableChange={onTableChange} tableLayout="auto" /> ); diff --git a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx index ebb97fd210f85..10d95bcc46906 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx +++ b/src/plugins/data/public/utils/table_inspector_view/components/data_table.tsx @@ -22,11 +22,17 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { i18n } from '@kbn/i18n'; import { IUiSettingsClient } from '@kbn/core/public'; -import { Datatable, DatatableColumn } from '@kbn/expressions-plugin/public'; +import { Datatable, DatatableColumn, DatatableRow } from '@kbn/expressions-plugin/public'; import { FieldFormatsStart } from '@kbn/field-formats-plugin/public'; import { UiActionsStart } from '@kbn/ui-actions-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; import { DataViewRow, DataViewColumn } from '../types'; +const PAGE_SIZE_OPTIONS = [10, 20, 50]; + interface DataTableFormatState { columns: DataViewColumn[]; rows: DataViewRow[]; @@ -49,7 +55,10 @@ interface RenderCellArguments { isFilterable: boolean; } -export class DataTableFormat extends Component { +class DataTableFormatClass extends Component< + DataTableFormatProps & EuiTablePersistInjectedProps, + DataTableFormatState +> { static propTypes = { data: PropTypes.object.isRequired, uiSettings: PropTypes.object.isRequired, @@ -169,7 +178,7 @@ export class DataTableFormat extends Component row[dataColumn.id] === value) || 0; - return DataTableFormat.renderCell({ + return DataTableFormatClass.renderCell({ table: data, columnIndex: index, rowIndex, @@ -186,9 +195,10 @@ export class DataTableFormat extends Component {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + const renderTable = ( { editField } = { editField: () => {}, @@ -87,6 +100,7 @@ const renderTable = ( ) => shallow( { +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + +class TableClass extends PureComponent< + IndexedFieldProps & EuiTablePersistInjectedProps +> { renderBooleanTemplate(value: string, arialLabel: string) { return value ? : ; } @@ -403,11 +411,17 @@ export class Table extends PureComponent { } render() { - const { items, editField, deleteField, indexPattern } = this.props; + const { + items, + editField, + deleteField, + indexPattern, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; const columns: Array> = [ @@ -508,8 +522,18 @@ export class Table extends PureComponent { items={items} columns={columns} pagination={pagination} - sorting={{ sort: { field: 'displayName', direction: 'asc' } }} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsIndexedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialSort: { field: 'displayName', direction: 'asc' }, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx index 06991b2081639..5fb2adf8697ab 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/relationships_table/relationships_table.tsx @@ -18,6 +18,7 @@ import { import { CoreStart } from '@kbn/core/public'; import { get } from 'lodash'; import { RedirectAppLinks } from '@kbn/shared-ux-link-redirect-app'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; import { SavedObjectRelation, @@ -139,12 +140,20 @@ export const RelationshipsTable = ({ ] as SearchFilterConfig[], }; + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewMgmtRelationships', + initialPageSize: 10, + }); + return ( items={relationships} columns={columns} - pagination={true} + pagination={{ + pageSize, + }} + onTableChange={onTableChange} search={search} rowProps={() => ({ 'data-test-subj': `relationshipsTableRow`, diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap index 5f8e34d0776ec..f3fee53256c67 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/__snapshots__/table.test.tsx.snap @@ -75,9 +75,10 @@ exports[`Table should render normally 1`] = ` }, ] } + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -87,7 +88,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "name", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx index 9ef16a1cb1531..29b51160e4730 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { Table } from '.'; +import { TableWithoutPersist as Table } from './table'; import { ScriptedFieldItem } from '../../types'; import { DataView } from '@kbn/data-views-plugin/public'; @@ -21,6 +21,14 @@ const items: ScriptedFieldItem[] = [ { name: '2', lang: 'painless', script: '', isUserEditable: false }, ]; +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'name' as const } }, + }, +}; + describe('Table', () => { let indexPattern: DataView; @@ -37,8 +45,9 @@ describe('Table', () => { }); test('should render normally', () => { - const component = shallow
( + const component = shallow(
{}} @@ -52,6 +61,7 @@ describe('Table', () => { test('should render the format', () => { const component = shallow(
{}} @@ -68,6 +78,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -100,6 +112,7 @@ describe('Table', () => { test('should not allow edit or deletion for user with only read access', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx index f1561ed99f8fd..834e6792768c6 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/scripted_fields_table/components/table/table.tsx @@ -13,8 +13,14 @@ import { i18n } from '@kbn/i18n'; import { EuiInMemoryTable, EuiBasicTableColumn } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import { ScriptedFieldItem } from '../../types'; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + interface TableProps { indexPattern: DataView; items: ScriptedFieldItem[]; @@ -22,7 +28,9 @@ interface TableProps { deleteField: (field: ScriptedFieldItem) => void; } -export class Table extends PureComponent { +class TableClass extends PureComponent< + TableProps & EuiTablePersistInjectedProps +> { renderFormatCell = (value: string) => { const { indexPattern } = this.props; const title = get(indexPattern, ['fieldFormatMap', value, 'type', 'title'], ''); @@ -31,7 +39,12 @@ export class Table extends PureComponent { }; render() { - const { items, editField, deleteField } = this.props; + const { + items, + editField, + deleteField, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns: Array> = [ { @@ -132,12 +145,26 @@ export class Table extends PureComponent { ]; const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( - + ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsScriptedFields', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap index 7ddd6d34fb089..9469bacfc8a7d 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/__snapshots__/table.test.tsx.snap @@ -79,9 +79,10 @@ exports[`Table should render normally 1`] = ` ] } loading={true} + onTableChange={[Function]} pagination={ Object { - "initialPageSize": 10, + "pageSize": 10, "pageSizeOptions": Array [ 5, 10, @@ -91,7 +92,14 @@ exports[`Table should render normally 1`] = ` } } searchFormat="eql" - sorting={true} + sorting={ + Object { + "sort": Object { + "direction": "asc", + "field": "clientId", + }, + } + } tableLayout="fixed" /> `; diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx index db766ca9b3e54..695002440754f 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.test.tsx @@ -10,7 +10,7 @@ import React, { ReactElement } from 'react'; import { shallow, ShallowWrapper } from 'enzyme'; -import { Table, TableProps, TableState } from './table'; +import { TableWithoutPersist as Table } from './table'; import { EuiTableFieldDataColumnType, keys } from '@elastic/eui'; import { DataView } from '@kbn/data-views-plugin/public'; import { SourceFiltersTableFilter } from '../../types'; @@ -20,10 +20,15 @@ const items: SourceFiltersTableFilter[] = [{ value: 'tim*', clientId: '' }]; const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as DataView); -const getTableColumnRender = ( - component: ShallowWrapper, - index: number = 0 -) => { +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'clientId' as const } }, + }, +}; + +const getTableColumnRender = (component: ShallowWrapper, index: number = 0) => { const columns = component.prop>>('columns'); return { @@ -35,6 +40,7 @@ describe('Table', () => { test('should render normally', () => { const component = shallow(
{}} @@ -48,8 +54,9 @@ describe('Table', () => { }); test('should render filter matches', () => { - const component = shallow
( + const component = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -70,11 +77,12 @@ describe('Table', () => { describe('editing', () => { const saveFilter = jest.fn(); const clientId = '1'; - let component: ShallowWrapper; + let component: ShallowWrapper; beforeEach(() => { - component = shallow
( + component = shallow(
{}} @@ -125,6 +133,7 @@ describe('Table', () => { test('should update the matches dynamically as input value is changed', () => { const localComponent = shallow(
[{ name: 'time' }, { name: 'value' }], })} @@ -191,6 +200,7 @@ describe('Table', () => { const component = shallow(
{ const component = shallow(
{}} @@ -251,6 +262,7 @@ describe('Table', () => { const component = shallow(
{}} diff --git a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx index 21de13871d03d..d43c72991c136 100644 --- a/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx +++ b/src/plugins/data_view_management/public/components/edit_index_pattern/source_filters_table/components/table/table.tsx @@ -21,6 +21,11 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; + import { SourceFiltersTableFilter } from '../../types'; const filterHeader = i18n.translate( @@ -69,6 +74,8 @@ const cancelAria = i18n.translate( } ); +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; + export interface TableProps { indexPattern: DataView; items: SourceFiltersTableFilter[]; @@ -83,8 +90,11 @@ export interface TableState { editingFilterValue: string; } -export class Table extends Component { - constructor(props: TableProps) { +class TableClass extends Component< + TableProps & EuiTablePersistInjectedProps, + TableState +> { + constructor(props: TableProps & EuiTablePersistInjectedProps) { super(props); this.state = { editingFilterId: '', @@ -227,11 +237,15 @@ export class Table extends Component { } render() { - const { items, isSaving } = this.props; + const { + items, + isSaving, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const columns = this.getColumns(); const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -240,8 +254,17 @@ export class Table extends Component { items={items} columns={columns} pagination={pagination} - sorting={true} + sorting={sorting} + onTableChange={onTableChange} /> ); } } + +export const TableWithoutPersist = TableClass; // For testing purposes + +export const Table = withEuiTablePersist(TableClass, { + tableId: 'dataViewsSourceFilters', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 10, +}); diff --git a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx index 4512cb520c574..daabfe3fe6a9a 100644 --- a/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/data_view_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -30,6 +30,8 @@ import { NoDataViewsPromptComponent, useOnTryESQL } from '@kbn/shared-ux-prompt- import type { SpacesContextProps } from '@kbn/spaces-plugin/public'; import { DataViewType } from '@kbn/data-views-plugin/public'; import { RollupDeprecationTooltip } from '@kbn/rollup'; +import { useEuiTablePersist } from '@kbn/shared-ux-table-persist'; + import type { IndexPatternManagmentContext } from '../../types'; import { getListBreadcrumbs } from '../breadcrumbs'; import { type RemoveDataViewProps, removeDataView } from '../edit_index_pattern'; @@ -42,10 +44,7 @@ import { deleteModalMsg } from './delete_modal_msg'; import { NoData } from './no_data'; import { SpacesList } from './spaces_list'; -const pagination = { - initialPageSize: 10, - pageSizeOptions: [5, 10, 25, 50], -}; +const PAGE_SIZE_OPTIONS = [5, 10, 25, 50]; const sorting = { sort: { @@ -123,6 +122,12 @@ export const IndexPatternTable = ({ }; const onTryESQL = useOnTryESQL(useOnTryESQLParams); + const { pageSize, onTableChange } = useEuiTablePersist({ + tableId: 'dataViewsIndexPattern', + initialPageSize: 10, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }); + const handleOnChange = ({ queryText, error }: { queryText: string; error: unknown }) => { if (!error) { setQuery(queryText); @@ -361,8 +366,12 @@ export const IndexPatternTable = ({ itemId="id" items={indexPatterns} columns={columns} - pagination={pagination} + pagination={{ + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }} sorting={sorting} + onTableChange={onTableChange} search={search} selection={dataViews.getCanSaveSync() ? selection : undefined} /> diff --git a/src/plugins/data_view_management/tsconfig.json b/src/plugins/data_view_management/tsconfig.json index 9857dd44829fa..879d2dab84da9 100644 --- a/src/plugins/data_view_management/tsconfig.json +++ b/src/plugins/data_view_management/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/react-kibana-mount", "@kbn/rollup", "@kbn/share-plugin", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap index 1851856a8739e..b1b399d1bd736 100644 --- a/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap +++ b/src/plugins/discover/public/application/main/components/top_nav/__snapshots__/open_search_panel.test.tsx.snap @@ -22,6 +22,7 @@ exports[`OpenSearchPanel render 1`] = ` ) : ( { diff --git a/src/plugins/saved_objects_finder/public/finder/index.tsx b/src/plugins/saved_objects_finder/public/finder/index.tsx index 28a79391dd0a6..cd985f5235920 100644 --- a/src/plugins/saved_objects_finder/public/finder/index.tsx +++ b/src/plugins/saved_objects_finder/public/finder/index.tsx @@ -12,10 +12,11 @@ import React from 'react'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ContentClient } from '@kbn/content-management-plugin/public'; import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { SavedObjectFinderProps } from './saved_object_finder'; +import type { HOCProps } from '@kbn/shared-ux-table-persist'; +import type { SavedObjectFinderItem, SavedObjectFinderProps } from './saved_object_finder'; const LazySavedObjectFinder = React.lazy(() => import('./saved_object_finder')); -const SavedObjectFinder = (props: SavedObjectFinderProps) => ( +const SavedObjectFinder = (props: HOCProps) => ( @@ -32,7 +33,7 @@ export const getSavedObjectFinder = ( uiSettings: IUiSettingsClient, savedObjectsTagging?: SavedObjectsTaggingApi ) => { - return (props: SavedObjectFinderProps) => ( + return (props: HOCProps) => ( ); }; diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx index d6cce936200d4..ace6f6a9d3661 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.test.tsx @@ -28,7 +28,10 @@ import { IconType } from '@elastic/eui'; import { mount, shallow } from 'enzyme'; import React from 'react'; import * as sinon from 'sinon'; -import { SavedObjectFinderUi as SavedObjectFinder } from './saved_object_finder'; +import { + SavedObjectFinderWithoutPersist as SavedObjectFinder, + SavedObjectFinderUi, +} from './saved_object_finder'; import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import { findTestSubject } from '@kbn/test-jest-helpers'; import { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; @@ -72,6 +75,15 @@ describe('SavedObjectsFinder', () => { }, ]; + const baseProps = { + id: 'foo', + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'title' as const } }, + }, + }; + const contentManagement = contentManagementMock.createStartContract(); const contentClient = contentManagement.client; beforeEach(() => { @@ -109,6 +121,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = shallow( @@ -157,6 +171,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = shallow( { const button = Hello; const wrapper = shallow( { const wrapper = mount( @@ -251,6 +269,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -279,6 +298,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -299,6 +319,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -322,6 +343,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -346,6 +368,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -375,6 +398,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( @@ -430,6 +455,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -453,6 +479,7 @@ describe('SavedObjectsFinder', () => { const wrapper = shallow( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { const noItemsMessage = ; const wrapper = mount( { const wrapper = mount( { const wrapper = mount( { ); const wrapper = mount( - ); - wrapper.instance().componentDidMount!(); await nextTick(); wrapper.update(); expect(wrapper.find(EuiInMemoryTable).find('tbody tr')).toHaveLength(15); @@ -774,6 +818,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( { const wrapper = mount( @@ -840,6 +887,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -884,6 +933,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( { const wrapper = mount( @@ -933,6 +984,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -954,6 +1006,7 @@ describe('SavedObjectsFinder', () => { const wrapper = mount( @@ -978,6 +1031,7 @@ describe('SavedObjectsFinder', () => { render( (item.id === doc3.id ? tooltipText : undefined)} @@ -990,7 +1044,7 @@ describe('SavedObjectsFinder', () => { const tooltip = screen.queryByText(tooltipText); if (show) { - expect(tooltip).toBeInTheDocument(); + expect(tooltip)?.toBeInTheDocument(); } else { expect(tooltip).toBeNull(); } diff --git a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx index e9f51a808b335..9ea3472e59d3c 100644 --- a/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx +++ b/src/plugins/saved_objects_finder/public/finder/saved_object_finder.tsx @@ -25,15 +25,21 @@ import { EuiToolTip, EuiIconTip, IconType, - PropertySort, Query, SearchFilterConfig, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import type { SavedObjectsTaggingApi } from '@kbn/saved-objects-tagging-oss-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist/src'; + import { FinderAttributes, SavedObjectCommon, LISTING_LIMIT_SETTING } from '../../common'; +const PAGE_SIZE_OPTIONS = [5, 10, 15, 25]; + export interface SavedObjectMetaData { type: string; name: string; @@ -45,7 +51,7 @@ export interface SavedObjectMetaData; @@ -55,7 +61,6 @@ interface SavedObjectFinderState { items: SavedObjectFinderItem[]; query: Query; isFetchingItems: boolean; - sort?: PropertySort; } interface SavedObjectFinderServices { @@ -65,6 +70,7 @@ interface SavedObjectFinderServices { } interface BaseSavedObjectFinder { + id: string; services: SavedObjectFinderServices; onChoose?: ( id: SavedObjectCommon['id'], @@ -93,8 +99,8 @@ interface SavedObjectFinderInitialPageSize extends BaseSavedObjectFinder { export type SavedObjectFinderProps = SavedObjectFinderFixedPage | SavedObjectFinderInitialPageSize; -export class SavedObjectFinderUi extends React.Component< - SavedObjectFinderProps, +class SavedObjectFinderUiClass extends React.Component< + SavedObjectFinderProps & EuiTablePersistInjectedProps, SavedObjectFinderState > { public static propTypes = { @@ -174,7 +180,7 @@ export class SavedObjectFinderUi extends React.Component< } }, 300); - constructor(props: SavedObjectFinderProps) { + constructor(props: SavedObjectFinderProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -211,7 +217,11 @@ export class SavedObjectFinderUi extends React.Component< }; public render() { - const { onChoose, savedObjectMetaData } = this.props; + const { + onChoose, + savedObjectMetaData, + euiTablePersist: { pageSize, sorting, onTableChange }, + } = this.props; const taggingApi = this.props.services.savedObjectsTagging; const originalTagColumn = taggingApi?.ui.getTableColumnDefinition(); const tagColumn: EuiTableFieldDataColumnType | undefined = originalTagColumn @@ -320,16 +330,11 @@ export class SavedObjectFinderUi extends React.Component< ...(tagColumn ? [tagColumn] : []), ]; const pagination = { - initialPageSize: this.props.initialPageSize || this.props.fixedPageSize || 10, - pageSizeOptions: [5, 10, 15, 25], + initialPageSize: !!this.props.fixedPageSize ? this.props.fixedPageSize : pageSize ?? 10, + pageSize: !!this.props.fixedPageSize ? undefined : pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, showPerPageOptions: !this.props.fixedPageSize, }; - const sorting = { - sort: this.state.sort ?? { - field: this.state.query?.text ? '' : 'title', - direction: 'asc', - }, - }; const typeFilter: SearchFilterConfig = { type: 'field_value_selection', field: 'type', @@ -382,10 +387,8 @@ export class SavedObjectFinderUi extends React.Component< message={this.props.noItemsMessage} search={search} pagination={pagination} - sorting={sorting} - onTableChange={({ sort }) => { - this.setState({ sort }); - }} + sorting={!!this.state.query?.text ? undefined : sorting} + onTableChange={onTableChange} /> @@ -393,6 +396,16 @@ export class SavedObjectFinderUi extends React.Component< } } +export const SavedObjectFinderUi = withEuiTablePersist(SavedObjectFinderUiClass, { + get: (props) => ({ + tableId: `soFinder-${props.id}`, + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: props.initialPageSize ?? props.fixedPageSize ?? 10, + }), +}); + +export const SavedObjectFinderWithoutPersist = SavedObjectFinderUiClass; // For testing + // Needed for React.lazy // eslint-disable-next-line import/no-default-export export default SavedObjectFinderUi; diff --git a/src/plugins/saved_objects_finder/tsconfig.json b/src/plugins/saved_objects_finder/tsconfig.json index cecc9fbdadb61..e32d4f34e68bc 100644 --- a/src/plugins/saved_objects_finder/tsconfig.json +++ b/src/plugins/saved_objects_finder/tsconfig.json @@ -15,6 +15,7 @@ "@kbn/content-management-plugin", "@kbn/content-management-utils", "@kbn/core-ui-settings-browser", + "@kbn/shared-ux-table-persist", ], "exclude": [ "target/**/*", diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index af21429a9f7bb..b82a989d32851 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -102,7 +102,6 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` onTableChange={[Function]} pagination={ Object { - "pageIndex": 0, "pageSize": 5, "pageSizeOptions": Array [ 5, @@ -251,10 +250,6 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` "newIndexPatternId": "2", }, ], - "unmatchedReferencesTablePagination": Object { - "pageIndex": 0, - "pageSize": 5, - }, }, }, ], diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap index f4a552f0a2fa2..124f4b4f2e285 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/relationships.test.tsx.snap @@ -110,7 +110,12 @@ exports[`Relationships should render dashboards normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -310,7 +315,12 @@ exports[`Relationships should render index patterns normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -501,7 +511,12 @@ exports[`Relationships should render invalid relations 1`] = ` ] } items={Array []} - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -652,7 +667,12 @@ exports[`Relationships should render searches normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { @@ -813,7 +833,12 @@ exports[`Relationships should render visualizations normally 1`] = ` }, ] } - pagination={true} + onTableChange={[Function]} + pagination={ + Object { + "pageSize": 10, + } + } rowProps={[Function]} search={ Object { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 17a25fe3d98b7..4c94812d7de69 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -12,7 +12,7 @@ import { importFileMock, resolveImportErrorsMock } from './flyout.test.mocks'; import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { coreMock, httpServiceMock } from '@kbn/core/public/mocks'; -import { Flyout, FlyoutProps, FlyoutState } from './flyout'; +import { FlyoutClass as Flyout, FlyoutProps, FlyoutState } from './flyout'; import { ShallowWrapper } from 'enzyme'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; @@ -21,15 +21,21 @@ const mockFile = { path: '/home/foo.ndjson', } as unknown as File; +const baseProps = { + euiTablePersist: { + pageSize: 5, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'foo' as const } }, + }, +}; + describe('Flyout', () => { let defaultProps: FlyoutProps; const shallowRender = (props: FlyoutProps) => { - return shallowWithI18nProvider() as unknown as ShallowWrapper< - FlyoutProps, - FlyoutState, - Flyout - >; + return shallowWithI18nProvider( + + ) as unknown as ShallowWrapper; }; beforeEach(() => { diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 7b50cec41fa27..4e29f34dedb73 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -36,6 +36,10 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { HttpStart, IBasePath } from '@kbn/core/public'; import { ISearchStart } from '@kbn/data-plugin/public'; import type { DataViewsContract, DataView } from '@kbn/data-views-plugin/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { importFile, @@ -50,6 +54,7 @@ import { ImportSummary } from './import_summary'; const CREATE_NEW_COPIES_DEFAULT = false; const OVERWRITE_ALL_DEFAULT = true; +const PAGE_SIZE_OPTIONS = [5, 10, 25]; export interface FlyoutProps { close: () => void; @@ -65,7 +70,6 @@ export interface FlyoutProps { export interface FlyoutState { unmatchedReferences?: ProcessedImportResponse['unmatchedReferences']; - unmatchedReferencesTablePagination: { pageIndex: number; pageSize: number }; failedImports?: ProcessedImportResponse['failedImports']; successfulImports?: ProcessedImportResponse['successfulImports']; conflictingRecord?: ConflictingRecord; @@ -95,16 +99,15 @@ const getErrorMessage = (e: any) => { }); }; -export class Flyout extends Component { - constructor(props: FlyoutProps) { +export class FlyoutClass extends Component< + FlyoutProps & EuiTablePersistInjectedProps, + FlyoutState +> { + constructor(props: FlyoutProps & EuiTablePersistInjectedProps) { super(props); this.state = { unmatchedReferences: undefined, - unmatchedReferencesTablePagination: { - pageIndex: 0, - pageSize: 5, - }, conflictingRecord: undefined, error: undefined, file: undefined, @@ -275,7 +278,10 @@ export class Flyout extends Component { }; renderUnmatchedReferences() { - const { unmatchedReferences, unmatchedReferencesTablePagination: tablePagination } = this.state; + const { unmatchedReferences } = this.state; + const { + euiTablePersist: { pageSize, onTableChange }, + } = this.props; if (!unmatchedReferences) { return null; @@ -367,8 +373,8 @@ export class Flyout extends Component { ]; const pagination = { - ...tablePagination, - pageSizeOptions: [5, 10, 25], + pageSize, + pageSizeOptions: PAGE_SIZE_OPTIONS, }; return ( @@ -376,16 +382,7 @@ export class Flyout extends Component { items={unmatchedReferences as any[]} columns={columns} pagination={pagination} - onTableChange={({ page }) => { - if (page) { - this.setState({ - unmatchedReferencesTablePagination: { - pageSize: page.size, - pageIndex: page.index, - }, - }); - } - }} + onTableChange={onTableChange} /> ); } @@ -657,3 +654,9 @@ export class Flyout extends Component { ); } } + +export const Flyout = withEuiTablePersist(FlyoutClass, { + tableId: 'savedObjectsMgmtUnmatchedReferences', + pageSizeOptions: PAGE_SIZE_OPTIONS, + initialPageSize: 5, +}); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx index e98c9e7b54223..e963b626552ca 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.test.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { shallowWithI18nProvider } from '@kbn/test-jest-helpers'; import { httpServiceMock } from '@kbn/core/public/mocks'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; -import { Relationships, RelationshipsProps } from './relationships'; +import { RelationshipsClass as Relationships, RelationshipsProps } from './relationships'; jest.mock('../../../lib/fetch_export_by_type_and_search', () => ({ fetchExportByTypeAndSearch: jest.fn(), @@ -21,6 +21,14 @@ jest.mock('../../../lib/fetch_export_objects', () => ({ fetchExportObjects: jest.fn(), })); +const baseProps = { + euiTablePersist: { + pageSize: 10, + onTableChange: () => {}, + sorting: { sort: { direction: 'asc' as const, field: 'id' as const } }, + }, +}; + const allowedTypes: SavedObjectManagementTypeInfo[] = [ { name: 'index-pattern', @@ -86,7 +94,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -155,7 +163,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -223,7 +231,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingElastic').length).toBe(1); @@ -292,7 +300,7 @@ describe('Relationships', () => { showPlainSpinner: true, }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Make sure we are showing loading expect(component.find('EuiLoadingSpinner').length).toBe(1); @@ -332,7 +340,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); @@ -378,7 +386,7 @@ describe('Relationships', () => { close: jest.fn(), }; - const component = shallowWithI18nProvider(); + const component = shallowWithI18nProvider(); // Ensure all promises resolve await new Promise((resolve) => process.nextTick(resolve)); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx index 36cb9da9ad436..0d9c71ceae2ff 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/relationships.tsx @@ -27,6 +27,10 @@ import { SearchFilterConfig } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; import { IBasePath } from '@kbn/core/public'; +import { + withEuiTablePersist, + type EuiTablePersistInjectedProps, +} from '@kbn/shared-ux-table-persist'; import type { SavedObjectManagementTypeInfo } from '../../../../common/types'; import { getDefaultTitle, getSavedObjectLabel } from '../../../lib'; import type { v1 } from '../../../../common'; @@ -83,8 +87,11 @@ const relationshipColumn = { }, }; -export class Relationships extends Component { - constructor(props: RelationshipsProps) { +export class RelationshipsClass extends Component< + RelationshipsProps & EuiTablePersistInjectedProps, + RelationshipsState +> { + constructor(props: RelationshipsProps & EuiTablePersistInjectedProps) { super(props); this.state = { @@ -218,7 +225,14 @@ export class Relationships extends Component ({ 'data-test-subj': `relationshipsTableRow`, @@ -420,3 +435,8 @@ export class Relationships extends Component { { - const panel = await this.testSubjects.find('inspectorPanel'); - await this.find.clickByButtonText('Rows per page: 20', panel); + await this.testSubjects.click('tablePaginationPopoverButton'); // The buttons for setting table page size are in a popover element. This popover // element appears as if it's part of the inspectorPanel but it's really attached // to the body element by a portal. diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index fbb7b971bfcb4..543367fda127a 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -135,6 +135,7 @@ export const AddEmbeddableFlyout: FC = ({ { onIndexPatternSelected(indexPattern as IndexPatternSavedObject); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx index 5aa0ccc46a5cd..ff173c47a5320 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/source_selection/source_selection.tsx @@ -125,6 +125,7 @@ export const SourceSelection: FC = () => { )} { = ({ onClose }) => { = ({ = ({ Date: Tue, 19 Nov 2024 00:38:12 +1100 Subject: [PATCH 26/50] Unauthorized route migration for routes owned by security-defend-workflows (#198375) ### Authz API migration for unauthorized routes Fix unauthorized routes --- .../routes/live_query/create_live_query_route.ts | 7 +++++++ .../server/endpoint/routes/metadata/index.ts | 5 +++++ .../server/endpoint/routes/policy/index.ts | 5 +++++ .../server/endpoint/routes/resolver.ts | 15 +++++++++++++++ 4 files changed, 32 insertions(+) diff --git a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts index 6372a48c89b6e..31edbd59f5400 100644 --- a/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts +++ b/x-pack/plugins/osquery/server/routes/live_query/create_live_query_route.ts @@ -31,6 +31,13 @@ export const createLiveQueryRoute = (router: IRouter, osqueryContext: OsqueryApp .addVersion( { version: API_VERSIONS.public.v1, + security: { + authz: { + enabled: false, + reason: + 'We do the check for 2 different scenarios below (const isInvalid): writeLiveQueries and runSavedQueries with saved_query_id, or pack_id', + }, + }, validate: { request: { body: buildRouteValidation< diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 955b11f198a74..950fd37bce2de 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -83,6 +83,11 @@ export function registerEndpointRoutes( .addVersion( { version: '2023-10-31', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: GetMetadataRequestSchema, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts index cbbd53555767e..7b60f4fba8be8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/policy/index.ts @@ -29,6 +29,11 @@ export function registerPolicyRoutes( .addVersion( { version: '2023-10-31', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: { request: GetPolicyResponseSchema, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts index d8cb4db4b0a65..0edd0e90d4907 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver.ts @@ -35,6 +35,11 @@ export const registerResolverRoutes = ( router.post( { path: '/api/endpoint/resolver/tree', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateTree, options: { authRequired: true }, }, @@ -44,6 +49,11 @@ export const registerResolverRoutes = ( router.post( { path: '/api/endpoint/resolver/events', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateEvents, options: { authRequired: true }, }, @@ -56,6 +66,11 @@ export const registerResolverRoutes = ( router.get( { path: '/api/endpoint/resolver/entity', + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, validate: validateEntities, options: { authRequired: true }, }, From f01c4e00a5e81a97004730bfef3533cea2d6fdc8 Mon Sep 17 00:00:00 2001 From: David Luna Date: Mon, 18 Nov 2024 14:58:16 +0100 Subject: [PATCH 27/50] feat: update OTEL Node.js metrics dashboard (#199353) ## Summary Update the current metrics dashboard for Node.js services that are instrumented using OTEL SDK or Elastic's distribution. The current dashboard had some issues: - it was using a view that is not available in latest deployments. - it was showing labels instead of OTEL native fields New dashboard is fixing this: - using `metrics-*` indices - using OTEL native fields - and also adding new panels for `nodejs.*` metrics ([ref](https://github.com/open-telemetry/semantic-conventions/blob/e0e93bad394a15161af7942d65d197defac7b88b/model/nodejs/metrics.yaml)) --- .../metrics/static_dashboard/dashboards/dashboard_catalog.ts | 1 + .../static_dashboard/dashboards/opentelemetry_nodejs.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts index 6f81ef6db535b..ea3c468a6c829 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/dashboard_catalog.ts @@ -8,6 +8,7 @@ export const AGENT_NAME_DASHBOARD_FILE_MAPPING: Record = { nodejs: 'nodejs', 'opentelemetry/nodejs': 'opentelemetry_nodejs', + 'opentelemetry/nodejs/elastic': 'opentelemetry_nodejs', java: 'java', 'opentelemetry/java': 'opentelemetry_java', 'opentelemetry/java/opentelemetry-java-instrumentation': 'opentelemetry_java', diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json index b9552e182893b..2eeb4d48b11d1 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json +++ b/x-pack/plugins/observability_solution/apm/public/components/app/metrics/static_dashboard/dashboards/opentelemetry_nodejs.json @@ -1 +1 @@ -{"attributes":{"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\"},\"panelIndex\":\"f3de253a-8c79-46d0-acb2-05eef8d056be\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"shouldTruncate\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\",\"accessors\":[\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\"}],\"yTitle\":\"Usage [bytes]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ed2c0b0b-d8c5-415d-aec6-7681d578d3db\":{\"columns\":{\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\":{\"label\":\"Free\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\":{\"label\":\"Used\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.usage\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"free\\\"\",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":false},\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\":{\"label\":\"Part of average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"dataType\":\"number\",\"operationType\":\"math\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"tinymathAst\":{\"type\":\"function\",\"name\":\"add\",\"args\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"location\":{\"min\":0,\"max\":115},\"text\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\"}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\"],\"customLabel\":true},\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\":{\"label\":\"Total\",\"dataType\":\"number\",\"operationType\":\"formula\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"formula\":\"average(system.memory.usage, kql='labels.state: \\\"used\\\"') + average(system.memory.usage, kql='labels.state: \\\"free\\\"')\",\"isFormulaBroken\":false,\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"references\":[\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"customLabel\":true}},\"columnOrder\":[\"719ac5fd-a2f0-4bf3-8dbc-c00357e96228\",\"8a334ede-47b6-4eae-aeea-1e2bcc472f94\",\"3b9cf7ee-613d-4c9c-8a1b-5fca7fdc5dc7\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X0\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X1\",\"282f5b98-13c4-41dd-bbf4-5a6f8928a857X2\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Usage\"},{\"version\":\"8.10.2\",\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\"},\"panelIndex\":\"9dcf1114-6984-4238-8229-cb9e802e0bdb\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"description\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"apm_static_index_pattern_id\",\"name\":\"indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":false,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\",\"accessors\":[\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"24a38611-9513-4e32-bb79-2a723c11a513\"}],\"yTitle\":\"Utilization [%]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"1633fa19-f9f3-4149-90c3-bffd1ba4e6c4\":{\"columns\":{\"24a38611-9513-4e32-bb79-2a723c11a513\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"74908758-6e28-43d5-929a-b4070c9026a1\":{\"label\":\"Average\",\"dataType\":\"number\",\"operationType\":\"average\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"},\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\":{\"label\":\"Max\",\"dataType\":\"number\",\"operationType\":\"max\",\"sourceField\":\"system.memory.utilization\",\"isBucketed\":false,\"scale\":\"ratio\",\"params\":{\"emptyAsNull\":true,\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true,\"filter\":{\"query\":\"labels.state: \\\"used\\\" \",\"language\":\"kuery\"}}},\"columnOrder\":[\"24a38611-9513-4e32-bb79-2a723c11a513\",\"74908758-6e28-43d5-929a-b4070c9026a1\",\"a49dd439-fc3c-4ebb-a2ca-46c7992cc29f\"],\"incompleteColumns\":{},\"sampling\":1}}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"System Memory Utilization\"}]","timeRestore":false,"title":"OpenTelemetry Memory Metrics","version":1},"coreMigrationVersion":"8.8.0","created_at":"2023-11-06T12:28:32.691Z","id":"fef77323-303f-4e39-a81f-553c268d16ec","managed":false,"references":[{"id":"metrics-*","name":"f3de253a-8c79-46d0-acb2-05eef8d056be:indexpattern-datasource-layer-ed2c0b0b-d8c5-415d-aec6-7681d578d3db","type":"index-pattern"},{"id":"apm_static_index_pattern_id","name":"9dcf1114-6984-4238-8229-cb9e802e0bdb:indexpattern-datasource-layer-1633fa19-f9f3-4149-90c3-bffd1ba4e6c4","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"8.9.0","updated_at":"2023-11-06T12:28:32.691Z","version":"WzM4OSwyXQ=="} \ No newline at end of file +{"attributes":{"controlGroupInput":{"chainingSystem":"HIERARCHICAL","controlStyle":"oneLine","ignoreParentSettingsJSON":"{\"ignoreFilters\":false,\"ignoreQuery\":false,\"ignoreTimerange\":false,\"ignoreValidations\":false}","panelsJSON":"{\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\":{\"grow\":true,\"order\":0,\"type\":\"optionsListControl\",\"width\":\"medium\",\"explicitInput\":{\"id\":\"c27c120b-4674-4ad7-9972-cd10bd9c1a34\",\"dataViewId\":\"metrics-*\",\"fieldName\":\"service.node.name\",\"title\":\"Node\",\"searchTechnique\":\"prefix\",\"selectedOptions\":[],\"sort\":{\"by\":\"_count\",\"direction\":\"desc\"},\"exclude\":true}}}","showApplySelections":false},"description":"","kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"syncCursor\":true,\"syncTooltips\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":24,\"h\":15,\"i\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\"},\"panelIndex\":\"48508429-15ea-4628-aa6f-23ca15aa1f55\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"969de0e0-6fd8-493b-b789-9edd46b5c67c\",\"accessors\":[\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"splitAccessor\":\"bcb37e21-93fb-4913-a749-75bb1b594002\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"969de0e0-6fd8-493b-b789-9edd46b5c67c\":{\"columns\":{\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"0c1e94e4-2029-4558-8963-ceeb3169486a\":{\"label\":\"Usage[bytes]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.memory.usage\",\"filter\":{\"query\":\"\\\"process.memory.usage\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"bytes\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"bcb37e21-93fb-4913-a749-75bb1b594002\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"0c1e94e4-2029-4558-8963-ceeb3169486a\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"bcb37e21-93fb-4913-a749-75bb1b594002\",\"82a0c81c-a712-46b2-a6c1-c178c3c1c848\",\"0c1e94e4-2029-4558-8963-ceeb3169486a\"],\"incompleteColumns\":{},\"sampling\":1,\"indexPatternId\":\"metrics-*\"}},\"currentIndexPatternId\":\"metrics-*\"},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process Memory Usage\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":15,\"w\":24,\"h\":15,\"i\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\"},\"panelIndex\":\"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\",\"accessors\":[\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"splitAccessor\":\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}],\"yTitle\":\"Delay [sec]\"},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"ecea9aed-08bf-47a0-8050-cb8b168bfc05\":{\"columns\":{\"a137d8d5-8283-45ac-b145-e364dcec6697\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}},\"ed17ee1c-af87-455d-96d9-ca21614b5eed\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\":{\"label\":\"p90\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p90\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p90\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true},\"003ad900-7138-4989-9b8c-a7290d7243d1\":{\"label\":\"p50\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.delay.p50\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.delay.p50\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\"},\"customLabel\":true}},\"columnOrder\":[\"a137d8d5-8283-45ac-b145-e364dcec6697\",\"ed17ee1c-af87-455d-96d9-ca21614b5eed\",\"9c53bcf0-b852-4b71-a1d4-ad21c324f478\",\"003ad900-7138-4989-9b8c-a7290d7243d1\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Delay\"},{\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":15,\"w\":24,\"h\":15,\"i\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\"},\"panelIndex\":\"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\",\"showSingleSeries\":true},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\",\"accessors\":[\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"b9505654-28b7-4121-883b-6745d4660cd3\",\"splitAccessor\":\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"collapseFn\":\"\",\"palette\":{\"type\":\"palette\",\"name\":\"default\"}}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"0b0f2810-b098-4db1-a3a9-607e0361b2f1\":{\"columns\":{\"b9505654-28b7-4121-883b-6745d4660cd3\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"64d972ab-b5b0-4c18-884b-a245d99fdc39\":{\"label\":\"Utilization[%]\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"nodejs.eventloop.utilization\",\"filter\":{\"query\":\"\\\"nodejs.eventloop.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}},\"customLabel\":true},\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"dac92d9d-c37d-432e-9bc9-7f82d97a5a8d\",\"b9505654-28b7-4121-883b-6745d4660cd3\",\"64d972ab-b5b0-4c18-884b-a245d99fdc39\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Event Loop Utilization\"},{\"type\":\"lens\",\"gridData\":{\"x\":24,\"y\":0,\"w\":24,\"h\":15,\"i\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\"},\"panelIndex\":\"d0e1efe6-23b1-4e61-8602-6df59ac0609b\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"type\":\"index-pattern\",\"id\":\"metrics-*\",\"name\":\"indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"bottom\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yTitle\":\"Utilization[%]\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"line\",\"layers\":[{\"layerId\":\"176f3949-5c04-44e2-a200-810c8fe28183\",\"accessors\":[\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"position\":\"top\",\"seriesType\":\"line\",\"showGridlines\":false,\"layerType\":\"data\",\"colorMapping\":{\"assignments\":[],\"specialAssignments\":[{\"rule\":{\"type\":\"other\"},\"color\":{\"type\":\"loop\"},\"touched\":false}],\"paletteId\":\"eui_amsterdam_color_blind\",\"colorMode\":{\"type\":\"categorical\"}},\"xAccessor\":\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"splitAccessor\":\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"formBased\":{\"layers\":{\"176f3949-5c04-44e2-a200-810c8fe28183\":{\"columns\":{\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\":{\"label\":\"@timestamp\",\"dataType\":\"date\",\"operationType\":\"date_histogram\",\"sourceField\":\"@timestamp\",\"isBucketed\":true,\"scale\":\"interval\",\"params\":{\"interval\":\"auto\",\"includeEmptyRows\":true,\"dropPartials\":false}},\"d5901d17-43c6-44ff-acd3-02824a9aed37\":{\"label\":\"Last value of process.cpu.utilization\",\"dataType\":\"number\",\"operationType\":\"last_value\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"process.cpu.utilization\",\"filter\":{\"query\":\"\\\"process.cpu.utilization\\\": *\",\"language\":\"kuery\"},\"params\":{\"sortField\":\"@timestamp\",\"format\":{\"id\":\"percent\",\"params\":{\"decimals\":2}}}},\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\":{\"label\":\"Top 5 values of service.node.name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"service.node.name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"d5901d17-43c6-44ff-acd3-02824a9aed37\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false,\"parentFormat\":{\"id\":\"terms\"},\"include\":[],\"exclude\":[],\"includeIsRegex\":false,\"excludeIsRegex\":false}}},\"columnOrder\":[\"f33b7f72-9b78-4fc3-8c09-4b321b794f31\",\"bfb6cb06-0dcd-4ef8-a49e-993012519d03\",\"d5901d17-43c6-44ff-acd3-02824a9aed37\"],\"incompleteColumns\":{},\"sampling\":1}}},\"indexpattern\":{\"layers\":{}},\"textBased\":{\"layers\":{}}},\"internalReferences\":[],\"adHocDataViews\":{}}},\"enhancements\":{}},\"title\":\"Process CPU Utilization\"}]","timeRestore":false,"title":"OTEL nodejs runtime metrics","version":2},"coreMigrationVersion":"8.8.0","created_at":"2024-10-15T10:37:56.030Z","id":"4fdfa1d4-6dfa-45fa-8f21-37b9f93d30d2","managed":false,"references":[{"id":"metrics-*","name":"48508429-15ea-4628-aa6f-23ca15aa1f55:indexpattern-datasource-layer-969de0e0-6fd8-493b-b789-9edd46b5c67c","type":"index-pattern"},{"id":"metrics-*","name":"9110e9e1-ef3e-4c17-93cf-ba5770a3c2b0:indexpattern-datasource-layer-ecea9aed-08bf-47a0-8050-cb8b168bfc05","type":"index-pattern"},{"id":"metrics-*","name":"3e4d0b5c-0d67-421a-8ce2-b7f4914481c2:indexpattern-datasource-layer-0b0f2810-b098-4db1-a3a9-607e0361b2f1","type":"index-pattern"},{"id":"metrics-*","name":"d0e1efe6-23b1-4e61-8602-6df59ac0609b:indexpattern-datasource-layer-176f3949-5c04-44e2-a200-810c8fe28183","type":"index-pattern"},{"id":"metrics-*","name":"controlGroup_c27c120b-4674-4ad7-9972-cd10bd9c1a34:optionsListDataView","type":"index-pattern"}],"type":"dashboard","typeMigrationVersion":"10.2.0","updated_at":"2024-11-07T18:37:51.713Z","updated_by":"u_elastic_found","version":"WzE1MzcsMTRd"} \ No newline at end of file From 79ba16518e93907385bedf2d4273dc05e95290bf Mon Sep 17 00:00:00 2001 From: Larry Gregory Date: Mon, 18 Nov 2024 09:34:09 -0500 Subject: [PATCH 28/50] Dependency ownership for AI Infra Team (#200238) ## Summary This updates our `renovate.json` configuration to mark the AI Infra team as owners of their set of dependencies. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- renovate.json | 1040 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 822 insertions(+), 218 deletions(-) diff --git a/renovate.json b/renovate.json index 6a762330029cc..e7ab31d95f756 100644 --- a/renovate.json +++ b/renovate.json @@ -1,9 +1,24 @@ { "$schema": "https://docs.renovatebot.com/renovate-schema.json", - "extends": ["config:recommended", "helpers:pinGitHubActionDigests", "helpers:pinGitHubActionDigestsToSemver"], - "ignorePaths": ["**/__fixtures__/**", "**/fixtures/**"], - "enabledManagers": ["npm", "github-actions", "custom.regex", "devcontainer"], - "baseBranches": ["main", "7.17"], + "extends": [ + "config:recommended", + "helpers:pinGitHubActionDigests", + "helpers:pinGitHubActionDigestsToSemver" + ], + "ignorePaths": [ + "**/__fixtures__/**", + "**/fixtures/**" + ], + "enabledManagers": [ + "npm", + "github-actions", + "custom.regex", + "devcontainer" + ], + "baseBranches": [ + "main", + "7.17" + ], "prConcurrentLimit": 0, "prHourlyLimit": 0, "separateMajorMinor": false, @@ -17,28 +32,51 @@ }, "packageRules": [ { - "matchDepPatterns": [".*"], + "matchDepPatterns": [ + ".*" + ], "enabled": false }, { "groupName": "devcontainer", - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip", "backport:current-major"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip", + "backport:current-major" + ], "enabled": true, - "matchManagers": ["devcontainer"] + "matchManagers": [ + "devcontainer" + ] }, { "groupName": "chainguard", - "matchPackageNames": ["docker.elastic.co/wolfi/chainguard-base"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchPackageNames": [ + "docker.elastic.co/wolfi/chainguard-base" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "enabled": true }, { "groupName": "operations actions", - "matchManagers": ["github-actions"], + "matchManagers": [ + "github-actions" + ], "matchPackageNames": [ "actions/checkout", "actions/github-script", @@ -48,33 +86,111 @@ "sergeysova/jq-action", "sourenlouv/backport" ], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "backport:all-open", "release_note:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "backport:all-open", + "release_note:skip" + ], + "enabled": true + }, + { + "groupName": "@elastic/appex-ai-infra", + "matchDepNames": [ + "@aws-crypto/sha256-js", + "@aws-crypto/util", + "@langtrase/trace-attributes", + "@opentelemetry/sdk-trace-base", + "@smithy/eventstream-serde-node", + "@smithy/protocol-http", + "@smithy/signature-v4", + "@smithy/types", + "ajv", + "aws4", + "eventsource-parser", + "fast-glob", + "gpt-tokenizer", + "langsmith", + "openai", + "@types/json-schema", + "table" + ], + "reviewers": [ + "team:appex-ai-infra" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:AI Infra", + "release_note:skip", + "backport:all-open" + ], "enabled": true }, { "groupName": "@elastic/charts", - "matchDepNames": ["@elastic/charts"], - "reviewers": ["team:visualizations", "markov00", "nickofthyme"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:prev-minor", "Team:Visualizations"], + "matchDepNames": [ + "@elastic/charts" + ], + "reviewers": [ + "team:visualizations", + "markov00", + "nickofthyme" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:prev-minor", + "Team:Visualizations" + ], "enabled": true }, { "groupName": "@elastic/elasticsearch", - "matchDepNames": ["@elastic/elasticsearch"], - "reviewers": ["team:kibana-operations", "team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "Team:Operations", "Team:Core"], + "matchDepNames": [ + "@elastic/elasticsearch" + ], + "reviewers": [ + "team:kibana-operations", + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip", + "Team:Operations", + "Team:Core" + ], "enabled": true }, { "groupName": "@elastic/elasticsearch", - "matchDepNames": ["@elastic/elasticsearch"], - "reviewers": ["team:kibana-operations", "team:kibana-core"], - "matchBaseBranches": ["7.17"], - "labels": ["release_note:skip", "Team:Operations", "Team:Core", "backport:skip"], + "matchDepNames": [ + "@elastic/elasticsearch" + ], + "reviewers": [ + "team:kibana-operations", + "team:kibana-core" + ], + "matchBaseBranches": [ + "7.17" + ], + "labels": [ + "release_note:skip", + "Team:Operations", + "Team:Core", + "backport:skip" + ], "enabled": true }, { @@ -86,178 +202,392 @@ "@launchdarkly/openfeature-node-server", "launchdarkly/find-code-references" ], - "reviewers": ["team:kibana-security", "team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Security", "Team:Core", "backport:prev-minor"], + "reviewers": [ + "team:kibana-security", + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Security", + "Team:Core", + "backport:prev-minor" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "OpenFeature", - "matchDepNames": ["@openfeature/core", "@openfeature/server-sdk", "@openfeature/web-sdk"], - "reviewers": ["team:kibana-security", "team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Security", "Team:Core", "backport:prev-minor"], + "matchDepNames": [ + "@openfeature/core", + "@openfeature/server-sdk", + "@openfeature/web-sdk" + ], + "reviewers": [ + "team:kibana-security", + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Security", + "Team:Core", + "backport:prev-minor" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "APM", - "matchDepNames": ["elastic-apm-node", "@elastic/apm-rum", "@elastic/apm-rum-react", "@elastic/apm-rum-core"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "elastic-apm-node", + "@elastic/apm-rum", + "@elastic/apm-rum-react", + "@elastic/apm-rum-core" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "enabled": true }, { "groupName": "RxJS", - "matchDepNames": ["rxjs"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "rxjs" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "enabled": true }, { "groupName": "stack traces", - "matchDepNames": ["trace", "clarify"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "trace", + "clarify" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "enabled": true }, { "groupName": "@elastic/ebt", - "matchDepNames": ["@elastic/ebt"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "@elastic/ebt" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "enabled": true }, { "groupName": "lodash", - "matchDepNames": ["lodash", "@types/lodash"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "lodash", + "@types/lodash" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "enabled": true }, { "groupName": "ansi-regex", - "matchDepNames": ["ansi-regex"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "ansi-regex" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "OpenAPI Spec", - "matchDepNames": ["@redocly/cli"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:skip"], + "matchDepNames": [ + "@redocly/cli" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "HAPI ecosystem", - "matchDepNames": ["@hapi/**", "brok", "joi"], - "reviewers": ["team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "Team:Core", "backport:prev-minor"], + "matchDepNames": [ + "@hapi/**", + "brok", + "joi" + ], + "reviewers": [ + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "Team:Core", + "backport:prev-minor" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "babel", - "matchDepNames": ["@types/babel__core"], - "matchDepPatterns": ["^@babel", "^babel-plugin"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepNames": [ + "@types/babel__core" + ], + "matchDepPatterns": [ + "^@babel", + "^babel-plugin" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "typescript", - "matchDepNames": ["typescript", "@types/jsdom"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepNames": [ + "typescript", + "@types/jsdom" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "prettier", - "matchDepNames": ["prettier", "eslint-plugin-prettier", "eslint-config-prettier"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepNames": [ + "prettier", + "eslint-plugin-prettier", + "eslint-config-prettier" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "allowedVersions": "<3.0", "enabled": true }, { "groupName": "typescript-eslint", - "matchDepPatterns": ["^@typescript-eslint"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepPatterns": [ + "^@typescript-eslint" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "eslint-plugin-depend", - "matchDepPatterns": ["eslint-plugin-depend"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepPatterns": [ + "eslint-plugin-depend" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "polyfills", - "matchDepNames": ["core-js"], - "matchDepPatterns": ["polyfill"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepNames": [ + "core-js" + ], + "matchDepPatterns": [ + "polyfill" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "CLI tooling", - "matchDepNames": ["listr2"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "backport:all-open", "release_note:skip"], + "matchDepNames": [ + "listr2" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "backport:all-open", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "vega related modules", - "matchDepNames": ["vega", "vega-lite", "vega-schema-url-parser", "vega-tooltip"], - "reviewers": ["team:kibana-visualizations"], - "matchBaseBranches": ["main"], - "labels": ["Feature:Vega", "Team:Visualizations"], + "matchDepNames": [ + "vega", + "vega-lite", + "vega-schema-url-parser", + "vega-tooltip" + ], + "reviewers": [ + "team:kibana-visualizations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Feature:Vega", + "Team:Visualizations" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "cypress", - "matchDepPatterns": ["cypress"], - "reviewers": ["Team:apm", "Team: SecuritySolution"], - "matchBaseBranches": ["main"], - "labels": ["buildkite-ci", "ci:all-cypress-suites"], + "matchDepPatterns": [ + "cypress" + ], + "reviewers": [ + "Team:apm", + "Team: SecuritySolution" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "buildkite-ci", + "ci:all-cypress-suites" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "security solution modules", - "matchDepNames": ["zod", "langchain"], - "reviewers": ["Team: SecuritySolution"], - "matchBaseBranches": ["main"], - "labels": ["Team: SecuritySolution"], + "matchDepNames": [ + "zod", + "langchain" + ], + "reviewers": [ + "Team: SecuritySolution" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team: SecuritySolution" + ], "minimumReleaseAge": "7 days", "enabled": true }, @@ -275,9 +605,17 @@ "@types/xml-crypto", "@kayahr/text-encoding" ], - "reviewers": ["team:kibana-security"], - "matchBaseBranches": ["main"], - "labels": ["Team:Security", "release_note:skip", "backport:all-open"], + "reviewers": [ + "team:kibana-security" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Security", + "release_note:skip", + "backport:all-open" + ], "minimumReleaseAge": "7 days", "enabled": true }, @@ -287,9 +625,17 @@ "github/codeql-action/analyze", "github/codeql-action/init" ], - "reviewers": ["team:kibana-security"], - "matchBaseBranches": ["main"], - "labels": ["Team:Security", "release_note:skip", "backport:all-open"], + "reviewers": [ + "team:kibana-security" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Security", + "release_note:skip", + "backport:all-open" + ], "minimumReleaseAge": "7 days", "enabled": true }, @@ -303,27 +649,54 @@ "ms-chromium-edge-driver", "selenium-webdriver" ], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "scss", - "matchDepNames": ["sass-embedded"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip", "backport:all-open"], + "matchDepNames": [ + "sass-embedded" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip", + "backport:all-open" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "minify", - "matchDepNames": ["gulp-terser", "terser"], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "matchDepNames": [ + "gulp-terser", + "terser" + ], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, @@ -336,9 +709,16 @@ "@testing-library/react-hooks", "@testing-library/user-event" ], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, @@ -360,36 +740,70 @@ "jest-runtime", "jest-snapshot" ], - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "labels": ["Team:Operations", "release_note:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Operations", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "@storybook", - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "matchDepPatterns": ["^@storybook"], - "excludeDepNames": ["@storybook/testing-react"], - "labels": ["Team:Operations", "release_note:skip", "ci:build-storybooks", "backport:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "matchDepPatterns": [ + "^@storybook" + ], + "excludeDepNames": [ + "@storybook/testing-react" + ], + "labels": [ + "Team:Operations", + "release_note:skip", + "ci:build-storybooks", + "backport:skip" + ], "minimumReleaseAge": "7 days", "allowedVersions": "<7.0", "enabled": true }, { "groupName": "@storybook/testing-react", - "reviewers": ["team:kibana-operations"], - "matchBaseBranches": ["main"], - "matchDepNames": ["@storybook/testing-react"], - "labels": ["Team:Operations", "release_note:skip", "ci:build-storybooks", "backport:skip"], + "reviewers": [ + "team:kibana-operations" + ], + "matchBaseBranches": [ + "main" + ], + "matchDepNames": [ + "@storybook/testing-react" + ], + "labels": [ + "Team:Operations", + "release_note:skip", + "ci:build-storybooks", + "backport:skip" + ], "minimumReleaseAge": "7 days", "allowedVersions": "<2.0", "enabled": true }, { "groupName": "react-query", - "matchDepNames": ["@tanstack/react-query", "@tanstack/react-query-devtools"], + "matchDepNames": [ + "@tanstack/react-query", + "@tanstack/react-query-devtools" + ], "reviewers": [ "team:response-ops", "team:kibana-cloud-security-posture", @@ -398,23 +812,43 @@ "team:awp-platform", "team:security-onboarding-and-lifecycle-mgt" ], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "ci:all-cypress-suites"], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip", + "ci:all-cypress-suites" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "react-hook-form", - "matchDepNames": ["react-hook-form"], - "reviewers": ["team:security-asset-management", "team:uptime"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "ci:all-cypress-suites"], + "matchDepNames": [ + "react-hook-form" + ], + "reviewers": [ + "team:security-asset-management", + "team:uptime" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip", + "ci:all-cypress-suites" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "redux", - "matchDepNames": ["redux", "react-redux"], + "matchDepNames": [ + "redux", + "react-redux" + ], "reviewers": [ "team:search-kibana", "team:kibana-presentation", @@ -422,151 +856,321 @@ "team:kibana-management", "team:security-solution" ], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "ci:all-cypress-suites"], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip", + "ci:all-cypress-suites" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "Profiling", - "matchDepNames": ["peggy", "@types/dagre"], - "reviewers": ["team:obs-ux-infra_services-team"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip"], + "matchDepNames": [ + "peggy", + "@types/dagre" + ], + "reviewers": [ + "team:obs-ux-infra_services-team" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "TTY Output", - "matchDepNames": ["xterm", "byte-size", "@types/byte-size"], - "reviewers": ["team:sec-cloudnative-integrations"], - "matchBaseBranches": ["main"], - "labels": ["Team: AWP: Visualization", "release_note:skip", "backport:skip"], + "matchDepNames": [ + "xterm", + "byte-size", + "@types/byte-size" + ], + "reviewers": [ + "team:sec-cloudnative-integrations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team: AWP: Visualization", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "Cloud Defend", - "matchDepNames": ["monaco-yaml"], - "reviewers": ["team:sec-cloudnative-integrations"], - "matchBaseBranches": ["main"], - "labels": ["Team: Cloud Native Integrations", "release_note:skip", "backport:skip"], + "matchDepNames": [ + "monaco-yaml" + ], + "reviewers": [ + "team:sec-cloudnative-integrations" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team: Cloud Native Integrations", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "JSON Web Token", - "matchDepNames": ["jsonwebtoken"], - "reviewers": ["team:response-ops", "team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:all-open"], + "matchDepNames": [ + "jsonwebtoken" + ], + "reviewers": [ + "team:response-ops", + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:all-open" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "XState", - "matchDepNames": ["xstate"], - "matchDepPrefixes": ["@xstate/"], - "reviewers": ["team:obs-ux-logs-team"], - "matchBaseBranches": ["main"], - "labels": ["Team:Obs UX Logs", "release_note:skip"], + "matchDepNames": [ + "xstate" + ], + "matchDepPrefixes": [ + "@xstate/" + ], + "reviewers": [ + "team:obs-ux-logs-team" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Obs UX Logs", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "OpenTelemetry modules", - "matchDepPrefixes": ["@opentelemetry/"], - "reviewers": ["team:stack-monitoring"], - "matchBaseBranches": ["main"], - "labels": ["Team:Monitoring"], + "matchDepPrefixes": [ + "@opentelemetry/" + ], + "reviewers": [ + "team:stack-monitoring" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Monitoring" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "csp", - "matchDepNames": ["content-security-policy-parser"], - "reviewers": ["team:kibana-security", "team:kibana-core"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:skip", "ci:serverless-test-all"], + "matchDepNames": [ + "content-security-policy-parser" + ], + "reviewers": [ + "team:kibana-security", + "team:kibana-core" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:skip", + "ci:serverless-test-all" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "AlertingEmails", - "matchDepNames": ["nodemailer"], - "reviewers": ["team:response-ops"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:prev-minor"], + "matchDepNames": [ + "nodemailer" + ], + "reviewers": [ + "team:response-ops" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:prev-minor" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "Kibana ES|QL Team", - "matchDepNames": ["recast"], - "reviewers": ["team:kibana-esql"], - "matchBaseBranches": ["main"], - "labels": ["Team:ESQL", "release_note:skip"], + "matchDepNames": [ + "recast" + ], + "reviewers": [ + "team:kibana-esql" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:ESQL", + "release_note:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "MSW", - "matchPackageNames": ["msw"], - "reviewers": ["team:kibana-cloud-security-posture"], - "matchBaseBranches": ["main"], - "labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"], + "matchPackageNames": [ + "msw" + ], + "reviewers": [ + "team:kibana-cloud-security-posture" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Cloud Security", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "re2js", - "matchDepNames": ["re2js"], - "reviewers": ["team:visualizations", "dej611"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "backport:all-open", "Team:Visualizations"], + "matchDepNames": [ + "re2js" + ], + "reviewers": [ + "team:visualizations", + "dej611" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "backport:all-open", + "Team:Visualizations" + ], "enabled": true }, { "groupName": "Serve swagger docs", - "matchDepNames": ["express", "swagger-jsdoc", "swagger-ui-express"], - "reviewers": ["team:obs-entities"], - "matchBaseBranches": ["main"], - "labels": ["release_note:skip", "team:obs-entities"], + "matchDepNames": [ + "express", + "swagger-jsdoc", + "swagger-ui-express" + ], + "reviewers": [ + "team:obs-entities" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "release_note:skip", + "team:obs-entities" + ], "enabled": true }, { "groupName": "Security Engineering Productivity", - "matchDepNames": ["dotenv", "playwright-chromium", "@playwright/test"], - "reviewers": ["team:security-engineering-productivity"], - "matchBaseBranches": ["main"], - "labels": ["Team: Sec Eng Productivity", "release_note:skip", "backport:all-open"], + "matchDepNames": [ + "dotenv", + "playwright-chromium", + "@playwright/test" + ], + "reviewers": [ + "team:security-engineering-productivity" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team: Sec Eng Productivity", + "release_note:skip", + "backport:all-open" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "@mswjs/http-middleware", - "matchPackageNames": ["@mswjs/http-middleware"], - "reviewers": ["team:kibana-cloud-security-posture"], - "matchBaseBranches": ["main"], - "labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"], + "matchPackageNames": [ + "@mswjs/http-middleware" + ], + "reviewers": [ + "team:kibana-cloud-security-posture" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Cloud Security", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "@xyflow/react", - "matchPackageNames": ["@xyflow/react"], - "reviewers": ["team:kibana-cloud-security-posture"], - "matchBaseBranches": ["main"], - "labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"], + "matchPackageNames": [ + "@xyflow/react" + ], + "reviewers": [ + "team:kibana-cloud-security-posture" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Cloud Security", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true }, { "groupName": "@dagrejs/dagre", - "matchPackageNames": ["@dagrejs/dagre"], - "reviewers": ["team:kibana-cloud-security-posture"], - "matchBaseBranches": ["main"], - "labels": ["Team:Cloud Security", "release_note:skip", "backport:skip"], + "matchPackageNames": [ + "@dagrejs/dagre" + ], + "reviewers": [ + "team:kibana-cloud-security-posture" + ], + "matchBaseBranches": [ + "main" + ], + "labels": [ + "Team:Cloud Security", + "release_note:skip", + "backport:skip" + ], "minimumReleaseAge": "7 days", "enabled": true } @@ -596,4 +1200,4 @@ "datasourceTemplate": "docker" } ] -} +} \ No newline at end of file From 01e03fd480446b9b8c0c53aefab01b723ff8fb9b Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 18 Nov 2024 07:47:29 -0700 Subject: [PATCH 29/50] Improve logging pattern schema validation (#200317) ## Summary This makes pattern schema validation stricter by avoiding unrestricted matching, enforces realistic timezone constraints, and sets an upper bound to the date format string. | Characteristic | Before | After | ----|----|----| | Optional Groups | Capturing groups for optional sections. | Non-capturing groups for optional sections.| |Timezone Validation | Allows any characters except }. | Restricts to A-Za-z/_+-.| |Clarity | More complex and verbose. | Cleaner, more restrictive, and simpler.| |Performance | Captures unnecessary groups. | Limits captures to needed named groups.| --------- Co-authored-by: Elastic Machine --- .../src/layouts/conversions/date.ts | 3 +-- .../src/layouts/pattern_layout.test.ts | 15 +++++++++++++++ .../src/layouts/pattern_layout.ts | 1 + 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts index 53dadaafde667..e78ba6b62a7e3 100644 --- a/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts +++ b/packages/core/logging/core-logging-common-internal/src/layouts/conversions/date.ts @@ -12,7 +12,7 @@ import { last } from 'lodash'; import { LogRecord } from '@kbn/logging'; import { Conversion } from './types'; -const dateRegExp = /%date({(?[^}]+)})?({(?[^}]+)})?/g; +const dateRegExp = /%date(?:\{(?[^}]+)\})?(?:\{(?[A-Za-z/_+-]+)\})?/g; const formats = { ISO8601: 'ISO8601', @@ -29,7 +29,6 @@ function formatDate( ): string { const momentDate = moment(date); momentDate.tz(timezone ?? moment.tz.guess()); - switch (dateFormat) { case formats.ISO8601: return momentDate.toISOString(); diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.test.ts b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.test.ts index 2965703eda5d7..53409a5851bd6 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.test.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.test.ts @@ -326,6 +326,21 @@ describe('schema', () => { `"Date format expected one of ISO8601, ISO8601_TZ, ABSOLUTE, UNIX, UNIX_MILLIS, but given: HH"` ); }); + + it('fails on %date with schema too long', () => { + const generateLongFormat = () => { + const longFormat = []; + for (let i = 1; i < 1001; i++) { + longFormat.push(`${i}`); + } + return longFormat.join(''); + }; + expect(() => + patternSchema.validate(`%date${generateLongFormat()}`) + ).toThrowErrorMatchingInlineSnapshot( + `"value has length [2898] but it must have a maximum length of [1000]."` + ); + }); }); }); }); diff --git a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts index d4ee822b27f93..758dcc65af637 100644 --- a/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts +++ b/packages/core/logging/core-logging-server-internal/src/layouts/pattern_layout.ts @@ -24,6 +24,7 @@ import { const DEFAULT_PATTERN = `[%date][%level][%logger] %message`; export const patternSchema = schema.string({ + maxLength: 1000, validate: (string) => { DateConversion.validate!(string); }, From 3f86fd0c078f889b3b3811631a56ee842ea90153 Mon Sep 17 00:00:00 2001 From: Sander Philipse <94373878+sphilipse@users.noreply.github.com> Date: Mon, 18 Nov 2024 16:00:12 +0100 Subject: [PATCH 30/50] Search fix documents table a11y (#200232) ## Summary This improves the accessibility of the documents table by: 1. Adding header rows with titles 2. Adding an accessible table description 3. Adding an accessible popover to the field type icon --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../components/result/result.tsx | 1 + .../components/result/result_field.tsx | 57 ++++++++++++------- .../components/result/results_fields.tsx | 26 ++++++++- .../kbn-search-index-documents/tsconfig.json | 1 - 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx index 14b6c58ebf861..207a4770b97f2 100644 --- a/packages/kbn-search-index-documents/components/result/result.tsx +++ b/packages/kbn-search-index-documents/components/result/result.tsx @@ -121,6 +121,7 @@ export const Result: React.FC = ({ diff --git a/packages/kbn-search-index-documents/components/result/result_field.tsx b/packages/kbn-search-index-documents/components/result/result_field.tsx index e82991032afbc..acd495f71cd44 100644 --- a/packages/kbn-search-index-documents/components/result/result_field.tsx +++ b/packages/kbn-search-index-documents/components/result/result_field.tsx @@ -7,11 +7,19 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React from 'react'; +import React, { useState } from 'react'; -import { EuiTableRow, EuiTableRowCell, EuiText, EuiToken } from '@elastic/eui'; +import { + EuiButtonIcon, + EuiFlexGroup, + EuiFlexItem, + EuiPopover, + EuiTableRow, + EuiTableRowCell, + EuiText, +} from '@elastic/eui'; -import { euiThemeVars } from '@kbn/ui-theme'; +import { i18n } from '@kbn/i18n'; import { ResultFieldProps } from './result_types'; import { PERMANENTLY_TRUNCATED_FIELDS } from './constants'; import { ResultFieldValue } from './result_field_value'; @@ -63,26 +71,35 @@ export const ResultField: React.FC = ({ isExpanded, }) => { const shouldTruncate = !isExpanded || PERMANENTLY_TRUNCATED_FIELDS.includes(fieldType); + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const fieldTypeLabel = i18n.translate('searchIndexDocuments.result.fieldTypeAriaLabel', { + defaultMessage: 'This field is of the type {fieldType}', + values: { fieldType }, + }); return ( - - - - - - - - {fieldName} - + + + + setIsPopoverOpen(!isPopoverOpen)} + iconType={iconType || (fieldType ? iconMap[fieldType] : defaultToken)} + /> + } + isOpen={isPopoverOpen} + > + {fieldTypeLabel} + + + + + {fieldName} + + + diff --git a/packages/kbn-search-index-documents/components/result/results_fields.tsx b/packages/kbn-search-index-documents/components/result/results_fields.tsx index e25ede0fe0463..9684d41d28c6b 100644 --- a/packages/kbn-search-index-documents/components/result/results_fields.tsx +++ b/packages/kbn-search-index-documents/components/result/results_fields.tsx @@ -9,19 +9,39 @@ import React from 'react'; -import { EuiTable, EuiTableBody } from '@elastic/eui'; +import { EuiTable, EuiTableBody, EuiTableHeader, EuiTableHeaderCell } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { ResultField } from './result_field'; import { ResultFieldProps } from './result_types'; interface Props { + documentId: string; fields: ResultFieldProps[]; isExpanded: boolean; } -export const ResultFields: React.FC = ({ fields, isExpanded }) => { +export const ResultFields: React.FC = ({ documentId, fields, isExpanded }) => { return ( - + + + + {i18n.translate('searchIndexDocuments.resultFields.fieldTypeHeaderLabel', { + defaultMessage: 'Field', + })} + + + {i18n.translate('searchIndexDocuments.resultFields.contentstableHeaderLabel', { + defaultMessage: 'Contents', + })} + + + {fields.map((field) => ( Date: Mon, 18 Nov 2024 10:00:42 -0500 Subject: [PATCH 31/50] Update doc on ruleTaskTimeout to mention requests to Elasticsearch also receive the same timeout, up to 5m (#200172) Updating the docs to mention ruleTaskTimeout also affects requests made to Elasticsearch --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index b14f2e30202b7..5139b05d4f4fa 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -92,7 +92,7 @@ The following table describes the properties of the `options` object. |executor|This is where the code for the rule type lives. This is a function to be called when executing a rule on an interval basis. For full details, see the executor section below.|Function| |producer|The id of the application producing this rule type.|string| |minimumLicenseRequired|The value of a minimum license. Most of the rules are licensed as "basic".|string| -|ruleTaskTimeout|(Optional) The length of time a rule can run before being cancelled due to timeout. If not specified, the default value of "5m" is used.|string| +|ruleTaskTimeout|(Optional) The length of time a rule can run before being cancelled due to timeout. If not specified, the default value of "5m" is used. Requests made to Elasticsearch will also receive the same timeout configuration, up to 5m.|string| |cancelAlertsOnRuleTimeout|(Optional) Whether to skip writing alerts and scheduling actions if a rule execution is cancelled due to timeout. If not specified, the default value of "true" is used.|boolean| |useSavedObjectReferences.extractReferences|(Optional) When developing a rule type, you can choose to implement hooks for extracting saved object references from rule parameters. This hook will be invoked when a rule is created or updated. Implementing this hook is optional, but if an extract hook is implemented, an inject hook must also be implemented.|Function |useSavedObjectReferences.injectReferences|(Optional) When developing a rule type, you can choose to implement hooks for injecting saved object references into rule parameters. This hook will be invoked when a rule is retrieved (get or find). Implementing this hook is optional, but if an inject hook is implemented, an extract hook must also be implemented.|Function From ad1b0fe881f0bff4e2ffca57eac7ec6a68806e2a Mon Sep 17 00:00:00 2001 From: Abhishek Bhatia <117628830+abhishekbhatia1710@users.noreply.github.com> Date: Mon, 18 Nov 2024 20:37:13 +0530 Subject: [PATCH 32/50] [Entity Analytics] API changes for right placement of deleting the old component template (#199734) ## Summary - Delete the old component template after the index template has referenced to the new component template - Test cases for the same flow ```JSON # Let's assume this is 8.15.3 # Create the component template when Risk Score engine is initialised # Create the index template which references the created component template PUT /_component_template/.risk-score-mappings { "template": { "settings": { "number_of_shards": 1 }, "mappings": { "properties": { "timestamp": { "type": "date" }, "user": { "properties": { "id": { "type": "keyword" }, "name": { "type": "text" } } } } } }, "version": 1 } PUT /_index_template/.risk-score.risk-score-default-index-template { "index_patterns": [".risk-score.risk-score-default-index-template"], "template": { "settings": { "number_of_replicas": 1 } }, "composed_of": [".risk-score-mappings"], "priority": 100, "version": 1, "_meta": { "description": "Index template for indices with the pattern my_index-*" } } # The deployment is updated to 8.16 # User tries to enable the Entity store which init's the Risk Score engine (again!!) # Fails, but creates the component template and cannot update the index template to reference the new component template due to the error PUT /_component_template/.risk-score-mappings-default { "template": { "settings": { "number_of_shards": 1 }, "mappings": { "properties": { "timestamp": { "type": "date" }, "user": { "properties": { "id": { "type": "keyword" }, "name": { "type": "text" } } } } } }, "version": 1 } GET /_component_template?filter_path=component_templates.name&name=.risk-score-mappings* DELETE /_component_template/.risk-score-mappings # Fails # changed flow PUT /_index_template/.risk-score.risk-score-default-index-template { "index_patterns": [".risk-score.risk-score-default-index-template"], "template": { "settings": { "number_of_replicas": 1 } }, "composed_of": [".risk-score-mappings-default"], "priority": 100, "version": 1, "_meta": { "description": "Index template for indices with the pattern my_index-*" } } DELETE /_component_template/.risk-score-mappings # Succeeds ######### ``` ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [ ] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed ### For maintainers - [ ] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#_add_your_labels) - [ ] This will appear in the **Release Notes** and follow the [guidelines](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) --- .../risk_score/risk_score_data_client.ts | 46 ++++++----- .../init_and_status_apis.ts | 80 +++++++++++++++++++ 2 files changed, 105 insertions(+), 21 deletions(-) diff --git a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts index 2ae05e4c86227..981e89c8d8ec5 100644 --- a/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts +++ b/x-pack/plugins/security_solution/server/lib/entity_analytics/risk_score/risk_score_data_client.ts @@ -120,30 +120,10 @@ export class RiskScoreDataClient { const oldComponentTemplateExists = await esClient.cluster.existsComponentTemplate({ name: mappingComponentName, }); - // If present then copy the contents to a new component template with the namespace in the name if (oldComponentTemplateExists) { - const oldComponentTemplateResponse = await esClient.cluster.getComponentTemplate( - { - name: mappingComponentName, - }, - { ignore: [404] } - ); - const oldComponentTemplate = oldComponentTemplateResponse?.component_templates[0]; - const newComponentTemplateName = nameSpaceAwareMappingsComponentName(namespace); - await esClient.cluster.putComponentTemplate({ - name: newComponentTemplateName, - body: oldComponentTemplate.component_template, - }); + await this.updateComponentTemplateNamewithNamespace(namespace); } - // Delete the component template without the namespace in the name - await esClient.cluster.deleteComponentTemplate( - { - name: mappingComponentName, - }, - { ignore: [404] } - ); - // Update the new component template with the required data await Promise.all([ createOrUpdateComponentTemplate({ @@ -188,6 +168,14 @@ export class RiskScoreDataClient { }, }); + // Delete the component template without the namespace in the name + await esClient.cluster.deleteComponentTemplate( + { + name: mappingComponentName, + }, + { ignore: [404] } + ); + await createDataStream({ logger: this.options.logger, esClient, @@ -327,4 +315,20 @@ export class RiskScoreDataClient { { logger: this.options.logger } ); } + + private async updateComponentTemplateNamewithNamespace(namespace: string): Promise { + const esClient = this.options.esClient; + const oldComponentTemplateResponse = await esClient.cluster.getComponentTemplate( + { + name: mappingComponentName, + }, + { ignore: [404] } + ); + const oldComponentTemplate = oldComponentTemplateResponse?.component_templates[0]; + const newComponentTemplateName = nameSpaceAwareMappingsComponentName(namespace); + await esClient.cluster.putComponentTemplate({ + name: newComponentTemplateName, + body: oldComponentTemplate.component_template, + }); + } } diff --git a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts index 9483343436018..bbcfd976abeb7 100644 --- a/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts +++ b/x-pack/test/security_solution_api_integration/test_suites/entity_analytics/risk_engine/trial_license_complete_tier/init_and_status_apis.ts @@ -539,6 +539,86 @@ export default ({ getService }: FtrProviderContext) => { firstResponse?.saved_objects?.[0]?.id ); }); + + it('should update the existing component template and index template without any errors', async () => { + const componentTemplateName = '.risk-score-mappings'; + const indexTemplateName = '.risk-score.risk-score-default-index-template'; + const newComponentTemplateName = '.risk-score-mappings-default'; + + // Call API to put the component template and index template + + await es.cluster.putComponentTemplate({ + name: componentTemplateName, + body: { + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + timestamp: { + type: 'date', + }, + user: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + }, + }, + }, + }, + }, + version: 1, + }, + }); + + // Call an API to put the index template + + await es.indices.putIndexTemplate({ + name: indexTemplateName, + body: { + index_patterns: [indexTemplateName], + composed_of: [componentTemplateName], + template: { + settings: { + number_of_shards: 1, + }, + mappings: { + properties: { + timestamp: { + type: 'date', + }, + user: { + properties: { + id: { + type: 'keyword', + }, + name: { + type: 'text', + }, + }, + }, + }, + }, + }, + }, + }); + + const response = await riskEngineRoutes.init(); + expect(response.status).to.eql(200); + expect(response.body.result.errors).to.eql([]); + + const response2 = await es.cluster.getComponentTemplate({ + name: newComponentTemplateName, + }); + expect(response2.component_templates.length).to.eql(1); + expect(response2.component_templates[0].name).to.eql(newComponentTemplateName); + }); + // Failing: See https://github.com/elastic/kibana/issues/191637 describe.skip('remove legacy risk score transform', function () { this.tags('skipFIPS'); From f399a5f5da85520ab3a100e2f75c5d5e98b17e3f Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 18 Nov 2024 16:33:30 +0100 Subject: [PATCH 33/50] [Visualize] Fix embeddable panel title behavior (#200548) ## Summary This PR adds a fix for a regression bug introduced with the new embeddable refactor in 8.16 . I've added an extra 8.16 FTR test to ensure it works. ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../visualizations/public/embeddable/state.ts | 11 ++++++++-- .../visualize/group3/_add_to_dashboard.ts | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/plugins/visualizations/public/embeddable/state.ts b/src/plugins/visualizations/public/embeddable/state.ts index ca0721a3a8395..52641703e04b6 100644 --- a/src/plugins/visualizations/public/embeddable/state.ts +++ b/src/plugins/visualizations/public/embeddable/state.ts @@ -112,6 +112,9 @@ export const deserializeSavedObjectState = async ({ enhancements, uiState, timeRange, + title: embeddableTitle, + description: embeddableDescription, + hidePanelTitles, }: VisualizeSavedObjectInputState) => { // Load a saved visualization from the library const { @@ -137,6 +140,8 @@ export const deserializeSavedObjectState = async ({ }, savedObjectId ); + const panelTitle = embeddableTitle ?? title; + const panelDescription = embeddableDescription ?? description; return { savedVis: { title, @@ -149,8 +154,9 @@ export const deserializeSavedObjectState = async ({ savedSearchId, }, }, - title, - description, + title: panelTitle, + description: panelDescription, + hidePanelTitles, savedObjectId, savedObjectProperties, linkedToLibrary: true, @@ -188,6 +194,7 @@ export const serializeState: (props: { if (linkedToLibrary) { return { rawState: { + ...titlesWithDefaults, savedObjectId: id, ...(enhancements ? { enhancements } : {}), ...(!isEmpty(serializedVis.uiState) ? { uiState: serializedVis.uiState } : {}), diff --git a/test/functional/apps/visualize/group3/_add_to_dashboard.ts b/test/functional/apps/visualize/group3/_add_to_dashboard.ts index 77125bc372934..d0921f5ab1d7d 100644 --- a/test/functional/apps/visualize/group3/_add_to_dashboard.ts +++ b/test/functional/apps/visualize/group3/_add_to_dashboard.ts @@ -13,6 +13,7 @@ import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const dashboardExpect = getService('dashboardExpect'); const dashboardPanelActions = getService('dashboardPanelActions'); + const dashboardCustomizePanel = getService('dashboardCustomizePanel'); const testSubjects = getService('testSubjects'); const listingTable = getService('listingTable'); @@ -287,5 +288,24 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dashboardPanelActions.expectLinkedToLibrary('Neat Saved Vis 2 Copy'); }); + + it('should persist correctly panel title on a by reference visualization', async () => { + await dashboard.navigateToApp(); + + await dashboard.clickNewDashboard(); + await dashboard.addVisualizations(['Visualization AreaChart']); + + await dashboardPanelActions.customizePanel(); + await dashboardCustomizePanel.setCustomPanelTitle('My New panel title'); + await dashboardCustomizePanel.clickSaveButton(); + + await dashboard.saveDashboard('My Very Entitled Dashboard'); + + await dashboard.gotoDashboardLandingPage(); + await listingTable.clickItemLink('dashboard', 'My Very Entitled Dashboard'); + + const [newPanelTitle] = await dashboard.getPanelTitles(); + expect(newPanelTitle).to.equal('My New panel title'); + }); }); } From e1ce698c554fc479d04a4ed48299b0a7f13f88ac Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Mon, 18 Nov 2024 10:42:47 -0500 Subject: [PATCH 34/50] [Fleet] Fix agent policy namespace validation (#200258) --- .../agent_policy_advanced_fields/index.tsx | 4 +-- .../services/spaces/space_settings.test.ts | 34 ++++++++++++++++++- .../server/services/spaces/space_settings.ts | 2 +- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx index 0277184acabf2..305148584f545 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/components/agent_policy_advanced_fields/index.tsx @@ -375,8 +375,8 @@ export const AgentPolicyAdvancedOptionsContent: React.FunctionComponent = > { }); }); }); + +describe('getSpaceSettings', () => { + function createSavedsClientMock(settingsAttributes?: any) { + const client = savedObjectsClientMock.create(); + + if (settingsAttributes) { + client.get.mockResolvedValue({ + attributes: settingsAttributes, + } as any); + } else { + client.get.mockRejectedValue( + SavedObjectsErrorHelpers.createGenericNotFoundError('Not found') + ); + } + + jest.mocked(appContextService.getInternalUserSOClientForSpaceId).mockReturnValue(client); + + return client; + } + it('should work with managedBy:null', async () => { + createSavedsClientMock({ + allowed_namespace_prefixes: ['test'], + managed_by: null, + }); + const res = await getSpaceSettings(); + + expect(res).toEqual({ + allowed_namespace_prefixes: ['test'], + managed_by: undefined, + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/spaces/space_settings.ts b/x-pack/plugins/fleet/server/services/spaces/space_settings.ts index ece0291ff4f7c..dff4df63b6a9d 100644 --- a/x-pack/plugins/fleet/server/services/spaces/space_settings.ts +++ b/x-pack/plugins/fleet/server/services/spaces/space_settings.ts @@ -36,7 +36,7 @@ export async function getSpaceSettings(spaceId?: string) { return { allowed_namespace_prefixes: settings?.attributes?.allowed_namespace_prefixes ?? [], - managed_by: settings?.attributes?.managed_by, + managed_by: settings?.attributes?.managed_by ?? undefined, }; } From 5edb9abafefe7ecb04ce7b69489a5e7d5017c067 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 18 Nov 2024 16:45:25 +0100 Subject: [PATCH 35/50] [ES|QL] Don't suggest unsupported fields (#200544) ## Summary Closes https://github.com/elastic/kibana/issues/189666 Specifically: - we do not suggest unsupported fields - we add a warning if the field is used in the query ### How to test 1. Create an index with unsupported fields. I used this ``` PUT my-index-test-unsupported { "mappings" : { "properties" : { "my_histogram" : { "type" : "histogram" }, "my_text" : { "type" : "keyword" } } } } PUT my-index-test-unsupported/_doc/1 { "my_text" : "histogram_1", "my_histogram" : { "values" : [0.1, 0.2, 0.3, 0.4, 0.5], "counts" : [3, 7, 23, 12, 6] } } PUT my-index-test-unsupported/_doc/2 { "my_text" : "histogram_2", "my_histogram" : { "values" : [0.1, 0.25, 0.35, 0.4, 0.45, 0.5], "counts" : [8, 17, 8, 7, 6, 2] } } ``` 2. Go to the editor and try the autocomplete, it should not suggest the histogram field image 3. Use a query like `FROM my-index-test-unsupported | KEEP my_histogram `. You should see a warning image ### Checklist - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --- .../src/__tests__/helpers.ts | 4 +-- .../src/autocomplete/__tests__/helpers.ts | 11 +++++-- .../src/autocomplete/autocomplete.test.ts | 1 + .../src/shared/resources_helpers.ts | 8 ++++- .../esql_validation_meta_tests.json | 11 +++++++ .../src/validation/validation.test.ts | 10 ++++++ .../src/validation/validation.ts | 33 ++++++++++--------- 7 files changed, 57 insertions(+), 21 deletions(-) diff --git a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts index 2f46356acee37..02d2c062ccca7 100644 --- a/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/__tests__/helpers.ts @@ -12,9 +12,7 @@ import { ESQLRealField } from '../validation/types'; import { fieldTypes } from '../definitions/types'; export const fields: ESQLRealField[] = [ - ...fieldTypes - .map((type) => ({ name: `${camelCase(type)}Field`, type })) - .filter((f) => f.type !== 'unsupported'), + ...fieldTypes.map((type) => ({ name: `${camelCase(type)}Field`, type })), { name: 'any#Char$Field', type: 'double' }, { name: 'kubernetes.something.something', type: 'double' }, { name: '@timestamp', type: 'date' }, diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts index 2221f4dc1582f..c49b05985c86a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/__tests__/helpers.ts @@ -11,6 +11,7 @@ import { camelCase } from 'lodash'; import { parse } from '@kbn/esql-ast'; import { scalarFunctionDefinitions } from '../../definitions/generated/scalar_functions'; import { builtinFunctions } from '../../definitions/builtin'; +import { NOT_SUGGESTED_TYPES } from '../../shared/resources_helpers'; import { aggregationFunctionDefinitions } from '../../definitions/generated/aggregation_functions'; import { timeUnitsToSuggest } from '../../definitions/literals'; import { groupingFunctionDefinitions } from '../../definitions/grouping'; @@ -229,7 +230,11 @@ export function getFieldNamesByType( ) { const requestedType = Array.isArray(_requestedType) ? _requestedType : [_requestedType]; return fields - .filter(({ type }) => requestedType.includes('any') || requestedType.includes(type)) + .filter( + ({ type }) => + (requestedType.includes('any') || requestedType.includes(type)) && + !NOT_SUGGESTED_TYPES.includes(type) + ) .map(({ name, suggestedAs }) => suggestedAs || name); } @@ -267,7 +272,9 @@ export function createCustomCallbackMocks( enrichFields: string[]; }> ) { - const finalColumnsSinceLastCommand = customColumnsSinceLastCommand || fields; + const finalColumnsSinceLastCommand = + customColumnsSinceLastCommand || + fields.filter(({ type }) => !NOT_SUGGESTED_TYPES.includes(type)); const finalSources = customSources || indexes; const finalPolicies = customPolicies || policies; return { diff --git a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts index 5f3a2e45f9e1f..f8d72fecf229a 100644 --- a/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/autocomplete/autocomplete.test.ts @@ -887,6 +887,7 @@ describe('autocomplete', () => { 'FROM a | ENRICH policy /', ['ON $0', 'WITH $0', '| '].map(attachTriggerCommand) ); + testSuggestions( 'FROM a | ENRICH policy ON /', getFieldNamesByType('any') diff --git a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts index 5e7d951d8bdbf..5659a585ed758 100644 --- a/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts +++ b/packages/kbn-esql-validation-autocomplete/src/shared/resources_helpers.ts @@ -12,6 +12,8 @@ import type { ESQLCallbacks } from './types'; import type { ESQLRealField } from '../validation/types'; import { enrichFieldsWithECSInfo } from '../autocomplete/utils/ecs_metadata_helper'; +export const NOT_SUGGESTED_TYPES = ['unsupported']; + export function buildQueryUntilPreviousCommand(ast: ESQLAst, queryString: string) { const prevCommand = ast[Math.max(ast.length - 2, 0)]; return prevCommand ? queryString.substring(0, prevCommand.location.max + 1) : queryString; @@ -54,7 +56,11 @@ export function getFieldsByTypeHelper(queryText: string, resourceRetriever?: ESQ return ( Array.from(cacheFields.values())?.filter(({ name, type }) => { const ts = Array.isArray(type) ? type : [type]; - return !ignored.includes(name) && ts.some((t) => types[0] === 'any' || types.includes(t)); + return ( + !ignored.includes(name) && + ts.some((t) => types[0] === 'any' || types.includes(t)) && + !NOT_SUGGESTED_TYPES.includes(type) + ); }) || [] ); }, diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json index bf0e9782a3395..fee9f90f38c93 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json +++ b/packages/kbn-esql-validation-autocomplete/src/validation/esql_validation_meta_tests.json @@ -76,6 +76,10 @@ "name": "counterDoubleField", "type": "counter_double" }, + { + "name": "unsupportedField", + "type": "unsupported" + }, { "name": "dateNanosField", "type": "date_nanos" @@ -9690,6 +9694,13 @@ ], "warning": [] }, + { + "query": "from a_index | keep unsupportedField", + "error": [], + "warning": [ + "Field [unsupportedField] cannot be retrieved, it is unsupported or not indexed; returning null" + ] + }, { "query": "f", "error": [ diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts index 9d737d542bd1a..68d8ebb233f5e 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.test.ts @@ -1695,6 +1695,16 @@ describe('validation logic', () => { ['Argument of [trim] must be [keyword], found value [doubleField] type [double]'] ); }); + + describe('unsupported fields', () => { + testErrorsAndWarnings( + `from a_index | keep unsupportedField`, + [], + [ + 'Field [unsupportedField] cannot be retrieved, it is unsupported or not indexed; returning null', + ] + ); + }); }); describe('Ignoring errors based on callbacks', () => { diff --git a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts index b4d095e2c0442..b3076d107f850 100644 --- a/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts +++ b/packages/kbn-esql-validation-autocomplete/src/validation/validation.ts @@ -1223,21 +1223,24 @@ function validateFieldsShadowing( return messages; } -function validateUnsupportedTypeFields(fields: Map) { +function validateUnsupportedTypeFields(fields: Map, ast: ESQLAst) { + const usedColumnsInQuery: string[] = []; + + walk(ast, { + visitColumn: (node) => usedColumnsInQuery.push(node.name), + }); const messages: ESQLMessage[] = []; - for (const field of fields.values()) { - if (field.type === 'unsupported') { - // Removed temporarily to supress all these warnings - // Issue to re-enable in a better way: https://github.com/elastic/kibana/issues/189666 - // messages.push( - // getMessageFromId({ - // messageId: 'unsupportedFieldType', - // values: { - // field: field.name, - // }, - // locations: { min: 1, max: 1 }, - // }) - // ); + for (const column of usedColumnsInQuery) { + if (fields.has(column) && fields.get(column)!.type === 'unsupported') { + messages.push( + getMessageFromId({ + messageId: 'unsupportedFieldType', + values: { + field: column, + }, + locations: { min: 1, max: 1 }, + }) + ); } } return messages; @@ -1350,7 +1353,7 @@ async function validateAst( const variables = collectVariables(ast, availableFields, queryString); // notify if the user is rewriting a column as variable with another type messages.push(...validateFieldsShadowing(availableFields, variables)); - messages.push(...validateUnsupportedTypeFields(availableFields)); + messages.push(...validateUnsupportedTypeFields(availableFields, ast)); for (const [index, command] of ast.entries()) { const references: ReferenceMaps = { From 050550c8e3b82329a7139997a49f3b34d9314ae7 Mon Sep 17 00:00:00 2001 From: Sid Date: Mon, 18 Nov 2024 16:46:07 +0100 Subject: [PATCH 36/50] Fix issue with duplicate references in error object when copying saved objects to space (#200053) Closes https://github.com/elastic/kibana/issues/158027 ## Summary Simply dedupes references to objects if they are part of the missing_references in the copy saved objects to SO endpoint ### Notes - Update forEach over SOs to a regular for loop since we had a couple of early exit scenarios - Checks against the set for references already added to the missing list and adds only if not present ------ **Old response: Note the duplicate references** Screenshot 2024-11-14 at 01 52 54 **New response** Screenshot 2024-11-14 at 01 50 41 ### Release note Dedupe results from copy saved objects to spaces API when object contains references to other objects. --------- Co-authored-by: Elastic Machine --- .../import/lib/validate_references.test.ts | 32 ++++++++++++++++ .../src/import/lib/validate_references.ts | 37 +++++++++++-------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.test.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.test.ts index 3702b46fdd790..6e3198f153df1 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.test.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.test.ts @@ -267,4 +267,36 @@ describe('validateReferences()', () => { 'Error fetching references for imported objects' ); }); + + // test that when references are missing returns only deduplicated errors + test('returns only deduplicated errors when references are missing', async () => { + const params = setup({ + objects: [ + { + id: '2', + type: 'visualization', + attributes: { title: 'My Visualization 2' }, + references: [ + { name: 'ref_0', type: 'index-pattern', id: '3' }, + { name: 'ref_0', type: 'index-pattern', id: '3' }, + ], + }, + ], + }); + params.savedObjectsClient.bulkGet.mockResolvedValue({ + saved_objects: [createNotFoundError({ type: 'index-pattern', id: '3' })], + }); + + const result = await validateReferences(params); + expect(result).toEqual([ + expect.objectContaining({ + type: 'visualization', + id: '2', + error: { + type: 'missing_references', + references: [{ type: 'index-pattern', id: '3' }], + }, + }), + ]); + }); }); diff --git a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.ts b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.ts index e83fafe3348f7..b482bceb8ae0a 100644 --- a/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.ts +++ b/packages/core/saved-objects/core-saved-objects-import-export-server-internal/src/import/lib/validate_references.ts @@ -102,30 +102,35 @@ export async function validateReferences(params: ValidateReferencesParams) { const nonExistingReferenceKeys = await getNonExistingReferenceAsKeys(params); // Filter out objects with missing references, add to error object - objects.forEach(({ type, id, references, attributes }) => { - if (objectsToSkip.has(`${type}:${id}`)) { + for (const obj of objects) { + const { type, id, references, attributes } = obj; + const objectKey = `${type}:${id}`; + if (objectsToSkip.has(objectKey)) { // skip objects with retries that have specified `ignoreMissingReferences` - return; + continue; } - const missingReferences = []; - const enforcedTypeReferences = (references || []).filter(filterReferencesToValidate); + const missingReferences: Array<{ type: string; id: string }> = []; + const enforcedTypeReferences = references?.filter(filterReferencesToValidate) || []; + + const seenReferences = new Set(); for (const { type: refType, id: refId } of enforcedTypeReferences) { - if (nonExistingReferenceKeys.includes(`${refType}:${refId}`)) { + const refKey = `${refType}:${refId}`; + + if (nonExistingReferenceKeys.includes(refKey) && !seenReferences.has(refKey)) { missingReferences.push({ type: refType, id: refId }); + seenReferences.add(refKey); } } - if (missingReferences.length === 0) { - return; + if (missingReferences.length > 0) { + errorMap[objectKey] = { + id, + type, + meta: { title: attributes.title }, + error: { type: 'missing_references', references: missingReferences }, + }; } - const { title } = attributes; - errorMap[`${type}:${id}`] = { - id, - type, - meta: { title }, - error: { type: 'missing_references', references: missingReferences }, - }; - }); + } return Object.values(errorMap); } From ef9621d61a7ed57412910b47b4ec9329637e399f Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 18 Nov 2024 09:49:10 -0600 Subject: [PATCH 37/50] Respect system dark mode in ESQL editor (#200233) When `uiSettings.overrides.theme:darkMode: true` is set, the ESQL editor uses dark mode. When `uiSettings.overrides.theme:darkMode: system` is set, the ESQL editor uses light mode, while the rest of the UI uses dark mode. Update the ESQL theme creation to create both light and dark variations of the theme at startup and use the theme in React component to determine which ESQL theme to use at runtime. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Stratoula Kalafateli --- .../kbn-esql-editor/src/esql_editor.test.tsx | 7 +- packages/kbn-esql-editor/src/esql_editor.tsx | 14 +- packages/kbn-monaco/index.ts | 2 +- packages/kbn-monaco/src/esql/index.ts | 4 +- packages/kbn-monaco/src/esql/lib/constants.ts | 3 +- .../src/esql/lib/esql_theme.test.ts | 8 +- .../kbn-monaco/src/esql/lib/esql_theme.ts | 316 +++++++++--------- packages/kbn-monaco/src/register_globals.ts | 5 +- 8 files changed, 187 insertions(+), 172 deletions(-) diff --git a/packages/kbn-esql-editor/src/esql_editor.test.tsx b/packages/kbn-esql-editor/src/esql_editor.test.tsx index ac00604e5508b..c572ff5355585 100644 --- a/packages/kbn-esql-editor/src/esql_editor.test.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.test.tsx @@ -16,23 +16,20 @@ import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; import { ESQLEditor } from './esql_editor'; import type { ESQLEditorProps } from './types'; import { ReactWrapper } from 'enzyme'; -import { of } from 'rxjs'; +import { coreMock } from '@kbn/core/server/mocks'; describe('ESQLEditor', () => { const uiConfig: Record = {}; const uiSettings = { get: (key: string) => uiConfig[key], } as IUiSettingsClient; - const theme = { - theme$: of({ darkMode: false }), - }; const services = { uiSettings, settings: { client: uiSettings, }, - theme, + core: coreMock.createStart(), }; function renderESQLEditorComponent(testProps: ESQLEditorProps) { diff --git a/packages/kbn-esql-editor/src/esql_editor.tsx b/packages/kbn-esql-editor/src/esql_editor.tsx index e8ca582ac5229..636bb0b13ff17 100644 --- a/packages/kbn-esql-editor/src/esql_editor.tsx +++ b/packages/kbn-esql-editor/src/esql_editor.tsx @@ -25,7 +25,14 @@ import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { AggregateQuery } from '@kbn/es-query'; import type { ExpressionsStart } from '@kbn/expressions-plugin/public'; import { useKibana } from '@kbn/kibana-react-plugin/public'; -import { ESQLLang, ESQL_LANG_ID, ESQL_THEME_ID, monaco, type ESQLCallbacks } from '@kbn/monaco'; +import { + ESQLLang, + ESQL_LANG_ID, + ESQL_DARK_THEME_ID, + ESQL_LIGHT_THEME_ID, + monaco, + type ESQLCallbacks, +} from '@kbn/monaco'; import memoize from 'lodash/memoize'; import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; @@ -91,7 +98,8 @@ export const ESQLEditor = memo(function ESQLEditor({ fieldsMetadata, uiSettings, } = kibana.services; - const timeZone = core?.uiSettings?.get('dateFormat:tz'); + const darkMode = core.theme?.getTheme().darkMode; + const timeZone = uiSettings?.get('dateFormat:tz'); const histogramBarTarget = uiSettings?.get('histogram:barTarget') ?? 50; const [code, setCode] = useState(query.esql ?? ''); // To make server side errors less "sticky", register the state of the code when submitting @@ -597,7 +605,7 @@ export const ESQLEditor = memo(function ESQLEditor({ vertical: 'auto', }, scrollBeyondLastLine: false, - theme: ESQL_THEME_ID, + theme: darkMode ? ESQL_DARK_THEME_ID : ESQL_LIGHT_THEME_ID, wordWrap: 'on', wrappingIndent: 'none', }; diff --git a/packages/kbn-monaco/index.ts b/packages/kbn-monaco/index.ts index ba8b0edb68e1a..283c3150302b7 100644 --- a/packages/kbn-monaco/index.ts +++ b/packages/kbn-monaco/index.ts @@ -20,7 +20,7 @@ export { } from './src/monaco_imports'; export { XJsonLang } from './src/xjson'; export { SQLLang } from './src/sql'; -export { ESQL_LANG_ID, ESQL_THEME_ID, ESQLLang } from './src/esql'; +export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID, ESQLLang } from './src/esql'; export type { ESQLCallbacks } from '@kbn/esql-validation-autocomplete'; export * from './src/painless'; diff --git a/packages/kbn-monaco/src/esql/index.ts b/packages/kbn-monaco/src/esql/index.ts index b14a2ab18ba75..64d49b155cc42 100644 --- a/packages/kbn-monaco/src/esql/index.ts +++ b/packages/kbn-monaco/src/esql/index.ts @@ -7,6 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { ESQL_LANG_ID, ESQL_THEME_ID } from './lib/constants'; +export { ESQL_LANG_ID, ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID } from './lib/constants'; export { ESQLLang } from './language'; -export { buildESQlTheme } from './lib/esql_theme'; +export { buildESQLTheme } from './lib/esql_theme'; diff --git a/packages/kbn-monaco/src/esql/lib/constants.ts b/packages/kbn-monaco/src/esql/lib/constants.ts index b0b0588b3ff4a..56f2f85ab074e 100644 --- a/packages/kbn-monaco/src/esql/lib/constants.ts +++ b/packages/kbn-monaco/src/esql/lib/constants.ts @@ -8,6 +8,7 @@ */ export const ESQL_LANG_ID = 'esql'; -export const ESQL_THEME_ID = 'esqlTheme'; +export const ESQL_LIGHT_THEME_ID = 'esqlThemeLight'; +export const ESQL_DARK_THEME_ID = 'esqlThemeDark'; export const ESQL_TOKEN_POSTFIX = '.esql'; diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts index 237996a7fbcaa..c2a200e650804 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.test.ts @@ -9,12 +9,12 @@ import { ESQLErrorListener, getLexer as _getLexer } from '@kbn/esql-ast'; import { ESQL_TOKEN_POSTFIX } from './constants'; -import { buildESQlTheme } from './esql_theme'; +import { buildESQLTheme } from './esql_theme'; import { CharStreams } from 'antlr4'; describe('ESQL Theme', () => { it('should not have multiple rules for a single token', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); const seen = new Set(); const duplicates: string[] = []; @@ -40,7 +40,7 @@ describe('ESQL Theme', () => { .map((name) => name!.toLowerCase()); it('every rule should apply to a valid lexical name', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); // These names aren't from the lexer... they are added on our side // see packages/kbn-monaco/src/esql/lib/esql_token_helpers.ts @@ -62,7 +62,7 @@ describe('ESQL Theme', () => { }); it('every valid lexical name should have a corresponding rule', () => { - const theme = buildESQlTheme(); + const theme = buildESQLTheme({ darkMode: false }); const tokenIDs = theme.rules.map((rule) => rule.token.replace(ESQL_TOKEN_POSTFIX, '')); const validExceptions = [ diff --git a/packages/kbn-monaco/src/esql/lib/esql_theme.ts b/packages/kbn-monaco/src/esql/lib/esql_theme.ts index 330e55de86155..07a4d723b63e8 100644 --- a/packages/kbn-monaco/src/esql/lib/esql_theme.ts +++ b/packages/kbn-monaco/src/esql/lib/esql_theme.ts @@ -7,169 +7,177 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { euiThemeVars, darkMode } from '@kbn/ui-theme'; +import { euiDarkVars, euiLightVars } from '@kbn/ui-theme'; import { themeRuleGroupBuilderFactory } from '../../common/theme'; import { ESQL_TOKEN_POSTFIX } from './constants'; import { monaco } from '../../monaco_imports'; const buildRuleGroup = themeRuleGroupBuilderFactory(ESQL_TOKEN_POSTFIX); -export const buildESQlTheme = (): monaco.editor.IStandaloneThemeData => ({ - base: darkMode ? 'vs-dark' : 'vs', - inherit: true, - rules: [ - // base - ...buildRuleGroup( - [ - 'explain', - 'ws', - 'assign', - 'comma', - 'dot', - 'opening_bracket', - 'closing_bracket', - 'quoted_identifier', - 'unquoted_identifier', - 'pipe', - ], - euiThemeVars.euiTextColor - ), +export const buildESQLTheme = ({ + darkMode, +}: { + darkMode: boolean; +}): monaco.editor.IStandaloneThemeData => { + const euiThemeVars = darkMode ? euiDarkVars : euiLightVars; - // source commands - ...buildRuleGroup( - ['from', 'row', 'show'], - euiThemeVars.euiColorPrimaryText, - true // isBold - ), + return { + base: darkMode ? 'vs-dark' : 'vs', + inherit: true, + rules: [ + // base + ...buildRuleGroup( + [ + 'explain', + 'ws', + 'assign', + 'comma', + 'dot', + 'opening_bracket', + 'closing_bracket', + 'quoted_identifier', + 'unquoted_identifier', + 'pipe', + ], + euiThemeVars.euiTextColor + ), - // commands - ...buildRuleGroup( - [ - 'dev_metrics', - 'metadata', - 'mv_expand', - 'stats', - 'dev_inlinestats', - 'dissect', - 'grok', - 'keep', - 'rename', - 'drop', - 'eval', - 'sort', - 'by', - 'where', - 'not', - 'is', - 'like', - 'rlike', - 'in', - 'as', - 'limit', - 'dev_lookup', - 'null', - 'enrich', - 'on', - 'with', - 'asc', - 'desc', - 'nulls_order', - ], - euiThemeVars.euiColorAccentText, - true // isBold - ), + // source commands + ...buildRuleGroup( + ['from', 'row', 'show'], + euiThemeVars.euiColorPrimaryText, + true // isBold + ), - // functions - ...buildRuleGroup(['functions'], euiThemeVars.euiColorPrimaryText), + // commands + ...buildRuleGroup( + [ + 'dev_metrics', + 'metadata', + 'mv_expand', + 'stats', + 'dev_inlinestats', + 'dissect', + 'grok', + 'keep', + 'rename', + 'drop', + 'eval', + 'sort', + 'by', + 'where', + 'not', + 'is', + 'like', + 'rlike', + 'in', + 'as', + 'limit', + 'dev_lookup', + 'null', + 'enrich', + 'on', + 'with', + 'asc', + 'desc', + 'nulls_order', + ], + euiThemeVars.euiColorAccentText, + true // isBold + ), - // operators - ...buildRuleGroup( - [ - 'or', - 'and', - 'rp', // ')' - 'lp', // '(' - 'eq', // '==' - 'cieq', // '=~' - 'neq', // '!=' - 'lt', // '<' - 'lte', // '<=' - 'gt', // '>' - 'gte', // '>=' - 'plus', // '+' - 'minus', // '-' - 'asterisk', // '*' - 'slash', // '/' - 'percent', // '%' - 'cast_op', // '::' - ], - euiThemeVars.euiColorPrimaryText - ), + // functions + ...buildRuleGroup(['functions'], euiThemeVars.euiColorPrimaryText), - // comments - ...buildRuleGroup( - [ - 'line_comment', - 'multiline_comment', - 'expr_line_comment', - 'expr_multiline_comment', - 'explain_line_comment', - 'explain_multiline_comment', - 'project_line_comment', - 'project_multiline_comment', - 'rename_line_comment', - 'rename_multiline_comment', - 'from_line_comment', - 'from_multiline_comment', - 'enrich_line_comment', - 'enrich_multiline_comment', - 'mvexpand_line_comment', - 'mvexpand_multiline_comment', - 'enrich_field_line_comment', - 'enrich_field_multiline_comment', - 'lookup_line_comment', - 'lookup_multiline_comment', - 'lookup_field_line_comment', - 'lookup_field_multiline_comment', - 'show_line_comment', - 'show_multiline_comment', - 'setting', - 'setting_line_comment', - 'settting_multiline_comment', - 'metrics_line_comment', - 'metrics_multiline_comment', - 'closing_metrics_line_comment', - 'closing_metrics_multiline_comment', - ], - euiThemeVars.euiColorDisabledText - ), + // operators + ...buildRuleGroup( + [ + 'or', + 'and', + 'rp', // ')' + 'lp', // '(' + 'eq', // '==' + 'cieq', // '=~' + 'neq', // '!=' + 'lt', // '<' + 'lte', // '<=' + 'gt', // '>' + 'gte', // '>=' + 'plus', // '+' + 'minus', // '-' + 'asterisk', // '*' + 'slash', // '/' + 'percent', // '%' + 'cast_op', // '::' + ], + euiThemeVars.euiColorPrimaryText + ), - // values - ...buildRuleGroup( - [ - 'quoted_string', - 'integer_literal', - 'decimal_literal', - 'named_or_positional_param', - 'param', - 'timespan_literal', - ], - euiThemeVars.euiColorSuccessText - ), - ], - colors: { - 'editor.foreground': euiThemeVars.euiTextColor, - 'editor.background': euiThemeVars.euiColorEmptyShade, - 'editor.lineHighlightBackground': euiThemeVars.euiColorLightestShade, - 'editor.lineHighlightBorder': euiThemeVars.euiColorLightestShade, - 'editor.selectionHighlightBackground': euiThemeVars.euiColorLightestShade, - 'editor.selectionHighlightBorder': euiThemeVars.euiColorLightShade, - 'editorSuggestWidget.background': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.border': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.focusHighlightForeground': euiThemeVars.euiColorEmptyShade, - 'editorSuggestWidget.foreground': euiThemeVars.euiTextColor, - 'editorSuggestWidget.highlightForeground': euiThemeVars.euiColorPrimary, - 'editorSuggestWidget.selectedBackground': euiThemeVars.euiColorPrimary, - 'editorSuggestWidget.selectedForeground': euiThemeVars.euiColorEmptyShade, - }, -}); + // comments + ...buildRuleGroup( + [ + 'line_comment', + 'multiline_comment', + 'expr_line_comment', + 'expr_multiline_comment', + 'explain_line_comment', + 'explain_multiline_comment', + 'project_line_comment', + 'project_multiline_comment', + 'rename_line_comment', + 'rename_multiline_comment', + 'from_line_comment', + 'from_multiline_comment', + 'enrich_line_comment', + 'enrich_multiline_comment', + 'mvexpand_line_comment', + 'mvexpand_multiline_comment', + 'enrich_field_line_comment', + 'enrich_field_multiline_comment', + 'lookup_line_comment', + 'lookup_multiline_comment', + 'lookup_field_line_comment', + 'lookup_field_multiline_comment', + 'show_line_comment', + 'show_multiline_comment', + 'setting', + 'setting_line_comment', + 'settting_multiline_comment', + 'metrics_line_comment', + 'metrics_multiline_comment', + 'closing_metrics_line_comment', + 'closing_metrics_multiline_comment', + ], + euiThemeVars.euiColorDisabledText + ), + + // values + ...buildRuleGroup( + [ + 'quoted_string', + 'integer_literal', + 'decimal_literal', + 'named_or_positional_param', + 'param', + 'timespan_literal', + ], + euiThemeVars.euiColorSuccessText + ), + ], + colors: { + 'editor.foreground': euiThemeVars.euiTextColor, + 'editor.background': euiThemeVars.euiColorEmptyShade, + 'editor.lineHighlightBackground': euiThemeVars.euiColorLightestShade, + 'editor.lineHighlightBorder': euiThemeVars.euiColorLightestShade, + 'editor.selectionHighlightBackground': euiThemeVars.euiColorLightestShade, + 'editor.selectionHighlightBorder': euiThemeVars.euiColorLightShade, + 'editorSuggestWidget.background': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.border': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.focusHighlightForeground': euiThemeVars.euiColorEmptyShade, + 'editorSuggestWidget.foreground': euiThemeVars.euiTextColor, + 'editorSuggestWidget.highlightForeground': euiThemeVars.euiColorPrimary, + 'editorSuggestWidget.selectedBackground': euiThemeVars.euiColorPrimary, + 'editorSuggestWidget.selectedForeground': euiThemeVars.euiColorEmptyShade, + }, + }; +}; diff --git a/packages/kbn-monaco/src/register_globals.ts b/packages/kbn-monaco/src/register_globals.ts index b4d9c07f78c79..32b8fb0ef2ece 100644 --- a/packages/kbn-monaco/src/register_globals.ts +++ b/packages/kbn-monaco/src/register_globals.ts @@ -11,7 +11,7 @@ import { XJsonLang } from './xjson'; import { PainlessLang } from './painless'; import { SQLLang } from './sql'; import { monaco } from './monaco_imports'; -import { ESQL_THEME_ID, ESQLLang, buildESQlTheme } from './esql'; +import { ESQL_DARK_THEME_ID, ESQL_LIGHT_THEME_ID, ESQLLang, buildESQLTheme } from './esql'; import { YAML_LANG_ID } from './yaml'; import { registerLanguage, registerTheme } from './helpers'; import { ConsoleLang, ConsoleOutputLang, CONSOLE_THEME_ID, buildConsoleTheme } from './console'; @@ -50,7 +50,8 @@ registerLanguage(ConsoleOutputLang); /** * Register custom themes */ -registerTheme(ESQL_THEME_ID, buildESQlTheme()); +registerTheme(ESQL_LIGHT_THEME_ID, buildESQLTheme({ darkMode: false })); +registerTheme(ESQL_DARK_THEME_ID, buildESQLTheme({ darkMode: true })); registerTheme(CONSOLE_THEME_ID, buildConsoleTheme()); registerTheme(CODE_EDITOR_LIGHT_THEME_ID, buildLightTheme()); registerTheme(CODE_EDITOR_DARK_THEME_ID, buildDarkTheme()); From 89f4eac4116f71329739f572bb63c87a37ba9fdd Mon Sep 17 00:00:00 2001 From: Saarika Bhasi <55930906+saarikabhasi@users.noreply.github.com> Date: Mon, 18 Nov 2024 21:45:05 +0530 Subject: [PATCH 38/50] [Search][Onboarding] Enable or disable index actions based on user privileges (#197869) ## Summary Disable or show index actions in the search indices index details page based on user privileges. Stack should work as it is, this change is relevant to Serverless search index details page. based on user privilege below buttons are disabled: 1. delete index in index action context menu 2. delete documents in data tab 3. add field in mappings tab 4. edit index settings in setting tab ### Screen recording https://github.com/user-attachments/assets/047f0d9f-b6c3-4665-9cff-a7a4d978b03b ### Checklist - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios - [x] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../components/result/result.tsx | 3 + .../components/result/result_types.ts | 1 + .../components/result/rich_result_header.tsx | 67 ++- .../src/types.ts | 2 + .../details_page/details_page_mappings.tsx | 4 +- .../details_page_mappings_content.tsx | 40 +- .../details_page/details_page_settings.tsx | 4 +- .../details_page_settings_content.tsx | 38 +- .../index_mapping_with_context.tsx | 7 +- .../index_settings_with_context.tsx | 6 +- .../plugins/search_indices/common/routes.ts | 2 +- x-pack/plugins/search_indices/common/types.ts | 3 +- .../components/create_index/create_index.tsx | 13 +- .../create_index/create_index_page.tsx | 4 +- .../index_documents/document_list.tsx | 9 +- .../index_documents/index_documents.tsx | 15 +- .../components/indices/details_page.tsx | 12 +- .../indices/details_page_mappings.tsx | 18 +- .../indices/details_page_menu_item.tsx | 22 +- .../indices/details_page_settings.tsx | 16 +- .../components/shared/create_index_form.tsx | 6 +- .../components/start/elasticsearch_start.tsx | 14 +- .../public/components/start/start_page.tsx | 5 +- .../public/hooks/api/use_user_permissions.ts | 8 +- .../search_indices/server/lib/status.test.ts | 51 ++- .../search_indices/server/lib/status.ts | 10 +- .../search_indices/server/routes/status.ts | 14 +- .../translations/translations/fr-FR.json | 1 - .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../page_objects/index_management_page.ts | 22 + .../search/search_indices/status.ts | 45 +- .../svl_search_index_detail_page.ts | 64 ++- .../index_management/index_detail.ts | 9 + .../test_suites/search/search_index_detail.ts | 406 +++++++++++------- 35 files changed, 638 insertions(+), 305 deletions(-) diff --git a/packages/kbn-search-index-documents/components/result/result.tsx b/packages/kbn-search-index-documents/components/result/result.tsx index 207a4770b97f2..ff3447229d8ed 100644 --- a/packages/kbn-search-index-documents/components/result/result.tsx +++ b/packages/kbn-search-index-documents/components/result/result.tsx @@ -37,6 +37,7 @@ export interface ResultProps { compactCard?: boolean; onDocumentClick?: () => void; onDocumentDelete?: () => void; + hasDeleteDocumentsPrivilege?: boolean; } export const Result: React.FC = ({ @@ -47,6 +48,7 @@ export const Result: React.FC = ({ showScore = false, onDocumentClick, onDocumentDelete, + hasDeleteDocumentsPrivilege, }) => { const [isExpanded, setIsExpanded] = useState(false); const tooltipText = @@ -97,6 +99,7 @@ export const Result: React.FC = ({ metaData={{ ...metaData, onDocumentDelete, + hasDeleteDocumentsPrivilege, }} /> )} diff --git a/packages/kbn-search-index-documents/components/result/result_types.ts b/packages/kbn-search-index-documents/components/result/result_types.ts index 420951333a05d..fd132b8d30069 100644 --- a/packages/kbn-search-index-documents/components/result/result_types.ts +++ b/packages/kbn-search-index-documents/components/result/result_types.ts @@ -23,6 +23,7 @@ export interface MetaDataProps { title?: string; score?: SearchHit['_score']; showScore?: boolean; + hasDeleteDocumentsPrivilege?: boolean; } export interface FieldProps { diff --git a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx index 98d2ea5a64de0..d4fa39fbe5edf 100644 --- a/packages/kbn-search-index-documents/components/result/rich_result_header.tsx +++ b/packages/kbn-search-index-documents/components/result/rich_result_header.tsx @@ -24,10 +24,12 @@ import { EuiTextColor, EuiTitle, useEuiTheme, + EuiToolTip, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { css } from '@emotion/react'; +import { FormattedMessage } from '@kbn/i18n-react'; import { MetaDataProps } from './result_types'; interface Props { @@ -60,6 +62,7 @@ const MetadataPopover: React.FC = ({ onDocumentDelete, score, showScore = false, + hasDeleteDocumentsPrivilege, }) => { const [popoverIsOpen, setPopoverIsOpen] = useState(false); const closePopover = () => setPopoverIsOpen(false); @@ -85,9 +88,10 @@ const MetadataPopover: React.FC = ({ return ( - {i18n.translate('searchIndexDocuments.result.header.metadata.title', { - defaultMessage: 'Document metadata', - })} + = ({ @@ -118,22 +125,40 @@ const MetadataPopover: React.FC = ({ {onDocumentDelete && ( - ) => { - e.stopPropagation(); - onDocumentDelete(); - closePopover(); - }} - fullWidth + - {i18n.translate('searchIndexDocuments.result.header.metadata.deleteDocument', { - defaultMessage: 'Delete document', - })} - + ) => { + e.stopPropagation(); + onDocumentDelete(); + closePopover(); + }} + fullWidth + > + + + )} diff --git a/x-pack/packages/index-management/index_management_shared_types/src/types.ts b/x-pack/packages/index-management/index_management_shared_types/src/types.ts index ec5c7938d6b4b..02404ddec6213 100644 --- a/x-pack/packages/index-management/index_management_shared_types/src/types.ts +++ b/x-pack/packages/index-management/index_management_shared_types/src/types.ts @@ -79,9 +79,11 @@ export interface Index { export interface IndexMappingProps { index?: Index; showAboutMappings?: boolean; + hasUpdateMappingsPrivilege?: boolean; } export interface IndexSettingProps { indexName: string; + hasUpdateSettingsPrivilege?: boolean; } export interface SendRequestResponse { data: D | null; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx index 77360fd85ad9a..10f5f8be36b85 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings.tsx @@ -19,7 +19,8 @@ import { useLoadIndexMappings } from '../../../../services'; export const DetailsPageMappings: FunctionComponent<{ index?: Index; showAboutMappings?: boolean; -}> = ({ index, showAboutMappings = true }) => { + hasUpdateMappingsPrivilege?: boolean; +}> = ({ index, showAboutMappings = true, hasUpdateMappingsPrivilege }) => { const { isLoading, data, error, resendRequest } = useLoadIndexMappings(index?.name || ''); const [jsonError, setJsonError] = useState(false); @@ -95,6 +96,7 @@ export const DetailsPageMappings: FunctionComponent<{ jsonData={data} showAboutMappings={showAboutMappings} refetchMapping={resendRequest} + hasUpdateMappingsPrivilege={hasUpdateMappingsPrivilege} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx index 567d3f782f6f1..e2f9cb68ad90d 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_mappings_content.tsx @@ -22,6 +22,7 @@ import { EuiText, EuiTitle, useGeneratedHtmlId, + EuiToolTip, } from '@elastic/eui'; import { css } from '@emotion/react'; import { i18n } from '@kbn/i18n'; @@ -68,7 +69,8 @@ export const DetailsPageMappingsContent: FunctionComponent<{ showAboutMappings: boolean; jsonData: any; refetchMapping: () => void; -}> = ({ index, data, jsonData, refetchMapping, showAboutMappings }) => { + hasUpdateMappingsPrivilege?: boolean; +}> = ({ index, data, jsonData, refetchMapping, showAboutMappings, hasUpdateMappingsPrivilege }) => { const { services: { extensionsService }, core: { @@ -475,18 +477,32 @@ export const DetailsPageMappingsContent: FunctionComponent<{ {!index.hidden && ( {!isAddingFields ? ( - - - + + + + ) : ( updateMappings()} diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx index e04b4798c4041..0c6f844f2c068 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings.tsx @@ -15,7 +15,8 @@ import { DetailsPageSettingsContent } from './details_page_settings_content'; export const DetailsPageSettings: FunctionComponent<{ indexName: string; -}> = ({ indexName }) => { + hasUpdateSettingsPrivilege?: boolean; +}> = ({ indexName, hasUpdateSettingsPrivilege }) => { const { isLoading, data, error, resendRequest } = useLoadIndexSettings(indexName); if (isLoading) { @@ -76,6 +77,7 @@ export const DetailsPageSettings: FunctionComponent<{ data={data} indexName={indexName} reloadIndexSettings={resendRequest} + hasUpdateSettingsPrivilege={hasUpdateSettingsPrivilege} /> ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx index 51ca47ba5c673..95ce72cf59abf 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/details_page_settings_content.tsx @@ -19,6 +19,7 @@ import { EuiSwitch, EuiSwitchEvent, EuiText, + EuiToolTip, } from '@elastic/eui'; import { css } from '@emotion/react'; import _ from 'lodash'; @@ -69,12 +70,14 @@ interface Props { data: IndexSettingsResponse; indexName: string; reloadIndexSettings: () => void; + hasUpdateSettingsPrivilege?: boolean; } export const DetailsPageSettingsContent: FunctionComponent = ({ data, indexName, reloadIndexSettings, + hasUpdateSettingsPrivilege, }) => { const [isEditMode, setIsEditMode] = useState(false); const { @@ -184,17 +187,32 @@ export const DetailsPageSettingsContent: FunctionComponent = ({ - + + data-test-subj="indexDetailsSettingsEditModeSwitchToolTip" + > + + } + checked={isEditMode} + onChange={onEditModeChange} + disabled={hasUpdateSettingsPrivilege === false} + /> + + diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx index a341b0fb67813..5b795f57c161b 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_mapping_with_context.tsx @@ -20,6 +20,7 @@ export const IndexMappingWithContext: React.FC = ( dependencies, index, showAboutMappings, + hasUpdateMappingsPrivilege, }) => { // this normally happens when the index management app is rendered // but if components are embedded elsewhere that setup is skipped, so we have to do it here @@ -42,7 +43,11 @@ export const IndexMappingWithContext: React.FC = ( }; return ( - + ); }; diff --git a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx index d56c2c46e8ec4..57aba9cda5941 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/index_list/details_page/with_context_components/index_settings_with_context.tsx @@ -20,6 +20,7 @@ export const IndexSettingsWithContext: React.FC = dependencies, indexName, usageCollection, + hasUpdateSettingsPrivilege, }) => { // this normally happens when the index management app is rendered // but if components are embedded elsewhere that setup is skipped, so we have to do it here @@ -46,7 +47,10 @@ export const IndexSettingsWithContext: React.FC = }; return ( - + ); }; diff --git a/x-pack/plugins/search_indices/common/routes.ts b/x-pack/plugins/search_indices/common/routes.ts index 9ffe1d09d3db5..f527fa676e2a0 100644 --- a/x-pack/plugins/search_indices/common/routes.ts +++ b/x-pack/plugins/search_indices/common/routes.ts @@ -6,7 +6,7 @@ */ export const GET_STATUS_ROUTE = '/internal/search_indices/status'; -export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges'; +export const GET_USER_PRIVILEGES_ROUTE = '/internal/search_indices/start_privileges/{indexName}'; export const POST_CREATE_INDEX_ROUTE = '/internal/search_indices/indices/create'; diff --git a/x-pack/plugins/search_indices/common/types.ts b/x-pack/plugins/search_indices/common/types.ts index ef3a46e0301b5..ae5f53a9d073c 100644 --- a/x-pack/plugins/search_indices/common/types.ts +++ b/x-pack/plugins/search_indices/common/types.ts @@ -12,7 +12,8 @@ export interface IndicesStatusResponse { export interface UserStartPrivilegesResponse { privileges: { canCreateApiKeys: boolean; - canCreateIndex: boolean; + canManageIndex: boolean; + canDeleteDocuments: boolean; }; } diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx index d8ce8073c691e..f09ae3856c097 100644 --- a/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index.tsx @@ -7,10 +7,11 @@ import React, { useCallback, useState } from 'react'; -import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; +import type { IndicesStatusResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { AvailableLanguages } from '../../code_examples'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { useKibana } from '../../hooks/use_kibana'; import { useUsageTracker } from '../../hooks/use_usage_tracker'; import { CreateIndexFormState } from '../../types'; @@ -31,9 +32,8 @@ function initCreateIndexState() { }; } -export interface CreateIndexProps { +interface CreateIndexProps { indicesData?: IndicesStatusResponse; - userPrivileges?: UserStartPrivilegesResponse; } enum CreateIndexViewMode { @@ -41,14 +41,15 @@ enum CreateIndexViewMode { Code = 'code', } -export const CreateIndex = ({ indicesData, userPrivileges }: CreateIndexProps) => { +export const CreateIndex = ({ indicesData }: CreateIndexProps) => { const { application } = useKibana().services; + const [formState, setFormState] = useState(initCreateIndexState); + const { data: userPrivileges } = useUserPrivilegesQuery(formState.defaultIndexName); const [createIndexView, setCreateIndexView] = useState( - userPrivileges?.privileges.canCreateIndex === false + userPrivileges?.privileges.canManageIndex === false ? CreateIndexViewMode.Code : CreateIndexViewMode.UI ); - const [formState, setFormState] = useState(initCreateIndexState); const usageTracker = useUsageTracker(); const onChangeView = useCallback( (id: string) => { diff --git a/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx index d8601e95760d7..56ee5f49c5339 100644 --- a/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx +++ b/x-pack/plugins/search_indices/public/components/create_index/create_index_page.tsx @@ -13,7 +13,6 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../../hooks/use_kibana'; import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; -import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { LoadIndicesStatusError } from '../shared/load_indices_status_error'; import { CreateIndex } from './create_index'; @@ -32,7 +31,6 @@ export const CreateIndexPage = () => { isError: hasIndicesStatusFetchError, error: indicesFetchError, } = useIndicesStatusQuery(); - const { data: userPrivileges } = useUserPrivilegesQuery(); const embeddableConsole = useMemo( () => (consolePlugin?.EmbeddableConsole ? : null), @@ -51,7 +49,7 @@ export const CreateIndexPage = () => { {isInitialLoading && } {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( - + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx index e86d1c5ad818a..cf9cce4928d01 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/document_list.tsx @@ -20,9 +20,15 @@ export interface DocumentListProps { indexName: string; docs: SearchHit[]; mappingProperties: Record; + hasDeleteDocumentsPrivilege: boolean; } -export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentListProps) => { +export const DocumentList = ({ + indexName, + docs, + mappingProperties, + hasDeleteDocumentsPrivilege, +}: DocumentListProps) => { const { mutate } = useDeleteDocument(indexName); return ( @@ -39,6 +45,7 @@ export const DocumentList = ({ indexName, docs, mappingProperties }: DocumentLis mutate({ id: doc._id! }); }} compactCard={false} + hasDeleteDocumentsPrivilege={hasDeleteDocumentsPrivilege} /> diff --git a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx index 83595913cece3..5e14275a492f8 100644 --- a/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx +++ b/x-pack/plugins/search_indices/public/components/index_documents/index_documents.tsx @@ -5,28 +5,34 @@ * 2.0. */ -import React from 'react'; +import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiProgress, EuiSpacer } from '@elastic/eui'; import { useIndexMapping } from '../../hooks/api/use_index_mappings'; import { AddDocumentsCodeExample } from './add_documents_code_example'; import { IndexDocuments as IndexDocumentsType } from '../../hooks/api/use_document_search'; import { DocumentList } from './document_list'; +import type { UserStartPrivilegesResponse } from '../../../common'; interface IndexDocumentsProps { indexName: string; indexDocuments?: IndexDocumentsType; isInitialLoading: boolean; + userPrivileges?: UserStartPrivilegesResponse; } export const IndexDocuments: React.FC = ({ indexName, indexDocuments, isInitialLoading, + userPrivileges, }) => { const { data: mappingData } = useIndexMapping(indexName); const docs = indexDocuments?.results?.data ?? []; const mappingProperties = mappingData?.mappings?.properties ?? {}; + const hasDeleteDocumentsPrivilege: boolean = useMemo(() => { + return userPrivileges?.privileges.canDeleteDocuments ?? false; + }, [userPrivileges]); return ( @@ -38,7 +44,12 @@ export const IndexDocuments: React.FC = ({ )} {docs.length > 0 && ( - + )} diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx index c672bb51493f6..fb09943710dc6 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page.tsx @@ -37,6 +37,7 @@ import { SearchIndexDetailsPageMenuItemPopover } from './details_page_menu_item' import { useIndexDocumentSearch } from '../../hooks/api/use_document_search'; import { useUsageTracker } from '../../contexts/usage_tracker_context'; import { AnalyticsEvents } from '../../analytics/constants'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { usePageChrome } from '../../hooks/use_page_chrome'; import { IndexManagementBreadcrumbs } from '../shared/breadcrumbs'; @@ -60,6 +61,7 @@ export const SearchIndexDetailsPage = () => { } = useIndexMapping(indexName); const { data: indexDocuments, isInitialLoading: indexDocumentsIsInitialLoading } = useIndexDocumentSearch(indexName); + const { data: userPrivileges } = useUserPrivilegesQuery(indexName); const navigateToPlayground = useCallback(async () => { const playgroundLocator = share.url.locators.get('PLAYGROUND_LOCATOR_ID'); @@ -97,6 +99,7 @@ export const SearchIndexDetailsPage = () => { indexName={indexName} indexDocuments={indexDocuments} isInitialLoading={indexDocumentsIsInitialLoading} + userPrivileges={userPrivileges} /> ), 'data-test-subj': `${SearchIndexDetailsTabs.DATA}Tab`, @@ -106,7 +109,7 @@ export const SearchIndexDetailsPage = () => { name: i18n.translate('xpack.searchIndices.mappingsTabLabel', { defaultMessage: 'Mappings', }), - content: , + content: , 'data-test-subj': `${SearchIndexDetailsTabs.MAPPINGS}Tab`, }, { @@ -114,11 +117,13 @@ export const SearchIndexDetailsPage = () => { name: i18n.translate('xpack.searchIndices.settingsTabLabel', { defaultMessage: 'Settings', }), - content: , + content: ( + + ), 'data-test-subj': `${SearchIndexDetailsTabs.SETTINGS}Tab`, }, ]; - }, [index, indexName, indexDocuments, indexDocumentsIsInitialLoading]); + }, [index, indexName, indexDocuments, indexDocumentsIsInitialLoading, userPrivileges]); const [selectedTab, setSelectedTab] = useState(detailsPageTabs[0]); useEffect(() => { @@ -256,6 +261,7 @@ export const SearchIndexDetailsPage = () => { , diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx index c90d5cad94c83..4ce415b5aba3c 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_mappings.tsx @@ -10,10 +10,16 @@ import { Index } from '@kbn/index-management-shared-types'; import React from 'react'; import { useMemo } from 'react'; import { useKibana } from '../../hooks/use_kibana'; +import type { UserStartPrivilegesResponse } from '../../../common'; + export interface SearchIndexDetailsMappingsProps { index?: Index; + userPrivileges?: UserStartPrivilegesResponse; } -export const SearchIndexDetailsMappings = ({ index }: SearchIndexDetailsMappingsProps) => { +export const SearchIndexDetailsMappings = ({ + index, + userPrivileges, +}: SearchIndexDetailsMappingsProps) => { const { indexManagement, history } = useKibana().services; const IndexMappingComponent = useMemo( @@ -21,10 +27,18 @@ export const SearchIndexDetailsMappings = ({ index }: SearchIndexDetailsMappings [indexManagement, history] ); + const hasUpdateMappingsPrivilege = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); + return ( <> - + ); }; diff --git a/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx index df45cdab7fba7..9e059660b01ab 100644 --- a/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx +++ b/x-pack/plugins/search_indices/public/components/indices/details_page_menu_item.tsx @@ -14,21 +14,27 @@ import { EuiText, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { ReactElement, useState } from 'react'; +import React, { ReactElement, useMemo, useState } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { useKibana } from '../../hooks/use_kibana'; +import type { UserStartPrivilegesResponse } from '../../../common'; interface SearchIndexDetailsPageMenuItemPopoverProps { handleDeleteIndexModal: () => void; showApiReference: boolean; + userPrivileges?: UserStartPrivilegesResponse; } export const SearchIndexDetailsPageMenuItemPopover = ({ showApiReference = false, handleDeleteIndexModal, + userPrivileges, }: SearchIndexDetailsPageMenuItemPopoverProps) => { const [showMoreOptions, setShowMoreOptions] = useState(false); const { docLinks } = useKibana().services; + const canManageIndex = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); const contextMenuItems = [ showApiReference && ( } + icon={} size="s" onClick={handleDeleteIndexModal} data-test-subj="moreOptionsDeleteIndex" - color="danger" + toolTipContent={ + !canManageIndex + ? i18n.translate('xpack.searchIndices.moreOptions.deleteIndex.permissionToolTip', { + defaultMessage: 'You do not have permission to delete an index', + }) + : undefined + } + toolTipProps={{ 'data-test-subj': 'moreOptionsDeleteIndexTooltip' }} + disabled={!canManageIndex} > - + { +export const SearchIndexDetailsSettings = ({ + indexName, + userPrivileges, +}: SearchIndexDetailsSettingsProps) => { const { indexManagement, history } = useKibana().services; + const hasUpdateSettingsPrivilege = useMemo(() => { + return userPrivileges?.privileges.canManageIndex === true; + }, [userPrivileges]); + const IndexSettingsComponent = useMemo( () => indexManagement.getIndexSettingsComponent({ history }), [indexManagement, history] @@ -24,7 +33,10 @@ export const SearchIndexDetailsSettings = ({ indexName }: SearchIndexDetailsSett return ( <> - + ); }; diff --git a/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx index ba2f83cb273da..56c8be57a04d3 100644 --- a/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx +++ b/x-pack/plugins/search_indices/public/components/shared/create_index_form.tsx @@ -73,7 +73,7 @@ export const CreateIndexForm = ({ name="indexName" value={indexName} isInvalid={indexNameHasError} - disabled={userPrivileges?.privileges?.canCreateIndex === false} + disabled={userPrivileges?.privileges?.canManageIndex === false} onChange={onIndexNameChange} placeholder={i18n.translate('xpack.searchIndices.shared.createIndex.name.placeholder', { defaultMessage: 'Enter a name for your index', @@ -85,7 +85,7 @@ export const CreateIndexForm = ({ {i18n.translate('xpack.searchIndices.shared.createIndex.permissionTooltip', { defaultMessage: 'You do not have permission to create an index.', @@ -101,7 +101,7 @@ export const CreateIndexForm = ({ iconType="sparkles" data-test-subj="createIndexBtn" disabled={ - userPrivileges?.privileges?.canCreateIndex === false || + userPrivileges?.privileges?.canManageIndex === false || indexNameHasError || isLoading } diff --git a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx index 3f3063ddb150e..7b525250ff493 100644 --- a/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx +++ b/x-pack/plugins/search_indices/public/components/start/elasticsearch_start.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import type { IndicesStatusResponse, UserStartPrivilegesResponse } from '../../../common'; +import type { IndicesStatusResponse } from '../../../common'; import { AnalyticsEvents } from '../../analytics/constants'; import { AvailableLanguages } from '../../code_examples'; @@ -22,6 +22,7 @@ import { CreateIndexFormState, CreateIndexViewMode } from '../../types'; import { CreateIndexPanel } from '../shared/create_index_panel'; import { useKibana } from '../../hooks/use_kibana'; +import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; function initCreateIndexState(): CreateIndexFormState { const defaultIndexName = generateRandomIndexName(); @@ -34,17 +35,18 @@ function initCreateIndexState(): CreateIndexFormState { export interface ElasticsearchStartProps { indicesData?: IndicesStatusResponse; - userPrivileges?: UserStartPrivilegesResponse; } -export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) => { +export const ElasticsearchStart: React.FC = () => { const { application } = useKibana().services; + const [formState, setFormState] = useState(initCreateIndexState); + const { data: userPrivileges } = useUserPrivilegesQuery(formState.defaultIndexName); + const [createIndexView, setCreateIndexViewMode] = useState( - userPrivileges?.privileges.canCreateIndex === false + userPrivileges?.privileges.canManageIndex === false ? CreateIndexViewMode.Code : CreateIndexViewMode.UI ); - const [formState, setFormState] = useState(initCreateIndexState); const usageTracker = useUsageTracker(); useEffect(() => { @@ -52,7 +54,7 @@ export const ElasticsearchStart = ({ userPrivileges }: ElasticsearchStartProps) }, [usageTracker]); useEffect(() => { if (userPrivileges === undefined) return; - if (userPrivileges.privileges.canCreateIndex === false) { + if (userPrivileges.privileges.canManageIndex === false) { setCreateIndexViewMode(CreateIndexViewMode.Code); } }, [userPrivileges]); diff --git a/x-pack/plugins/search_indices/public/components/start/start_page.tsx b/x-pack/plugins/search_indices/public/components/start/start_page.tsx index 4dabec2e5fa98..b21eb82c8dcbc 100644 --- a/x-pack/plugins/search_indices/public/components/start/start_page.tsx +++ b/x-pack/plugins/search_indices/public/components/start/start_page.tsx @@ -13,7 +13,6 @@ import { KibanaPageTemplate } from '@kbn/shared-ux-page-kibana-template'; import { useKibana } from '../../hooks/use_kibana'; import { useIndicesStatusQuery } from '../../hooks/api/use_indices_status'; -import { useUserPrivilegesQuery } from '../../hooks/api/use_user_permissions'; import { useIndicesRedirect } from './hooks/use_indices_redirect'; import { ElasticsearchStart } from './elasticsearch_start'; @@ -33,7 +32,7 @@ export const ElasticsearchStartPage = () => { isError: hasIndicesStatusFetchError, error: indicesFetchError, } = useIndicesStatusQuery(); - const { data: userPrivileges } = useUserPrivilegesQuery(); + usePageChrome(PageTitle, [...IndexManagementBreadcrumbs, { text: PageTitle }]); const embeddableConsole = useMemo( @@ -53,7 +52,7 @@ export const ElasticsearchStartPage = () => { {isInitialLoading && } {hasIndicesStatusFetchError && } {!isInitialLoading && !hasIndicesStatusFetchError && ( - + )} {embeddableConsole} diff --git a/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts b/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts index d3f4f34887157..ca5cbd10468e9 100644 --- a/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts +++ b/x-pack/plugins/search_indices/public/hooks/api/use_user_permissions.ts @@ -7,16 +7,18 @@ import { useQuery } from '@tanstack/react-query'; -import { GET_USER_PRIVILEGES_ROUTE } from '../../../common/routes'; import type { UserStartPrivilegesResponse } from '../../../common/types'; import { QueryKeys } from '../../constants'; import { useKibana } from '../use_kibana'; -export const useUserPrivilegesQuery = () => { +export const useUserPrivilegesQuery = (indexName: string) => { const { http } = useKibana().services; return useQuery({ queryKey: [QueryKeys.FetchUserStartPrivileges], - queryFn: () => http.get(GET_USER_PRIVILEGES_ROUTE), + queryFn: () => + http.get( + `/internal/search_indices/start_privileges/${indexName}` + ), }); }; diff --git a/x-pack/plugins/search_indices/server/lib/status.test.ts b/x-pack/plugins/search_indices/server/lib/status.test.ts index ff5a8fc1eadd5..bf2250fc8707e 100644 --- a/x-pack/plugins/search_indices/server/lib/status.test.ts +++ b/x-pack/plugins/search_indices/server/lib/status.test.ts @@ -116,6 +116,7 @@ describe('status api lib', function () { }); describe('fetchUserStartPrivileges', function () { + const testIndexName = 'search-zbd1'; it('should return privileges true', async () => { const result: SecurityHasPrivilegesResponse = { application: {}, @@ -124,17 +125,20 @@ describe('status api lib', function () { }, has_all_requested: true, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + delete: true, + manage: true, }, }, username: 'unit-test', }; + mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: true, canCreateApiKeys: true, }, }); @@ -144,8 +148,8 @@ describe('status api lib', function () { cluster: ['manage_api_key'], index: [ { - names: ['test-index-name'], - privileges: ['create_index'], + names: [testIndexName], + privileges: ['manage', 'delete'], }, ], }); @@ -158,17 +162,19 @@ describe('status api lib', function () { }, has_all_requested: false, index: { - 'test-index-name': { - create_index: false, + [testIndexName]: { + manage: false, + delete: false, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); @@ -181,17 +187,19 @@ describe('status api lib', function () { }, has_all_requested: false, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + manage: true, + delete: true, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: true, canCreateApiKeys: false, }, }); @@ -202,17 +210,19 @@ describe('status api lib', function () { cluster: {}, has_all_requested: true, index: { - 'test-index-name': { - create_index: true, + [testIndexName]: { + manage: true, + delete: false, }, }, username: 'unit-test', }; mockClient.security.hasPrivileges.mockResolvedValue(result); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: true, + canManageIndex: true, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); @@ -220,9 +230,10 @@ describe('status api lib', function () { it('should default privileges on exceptions', async () => { mockClient.security.hasPrivileges.mockRejectedValue(new Error('Boom!!')); - await expect(fetchUserStartPrivileges(client, logger)).resolves.toEqual({ + await expect(fetchUserStartPrivileges(client, logger, testIndexName)).resolves.toEqual({ privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }); diff --git a/x-pack/plugins/search_indices/server/lib/status.ts b/x-pack/plugins/search_indices/server/lib/status.ts index 752e897ab1707..44ee6cf59abd3 100644 --- a/x-pack/plugins/search_indices/server/lib/status.ts +++ b/x-pack/plugins/search_indices/server/lib/status.ts @@ -38,7 +38,7 @@ export async function fetchIndicesStatus( export async function fetchUserStartPrivileges( client: ElasticsearchClient, logger: Logger, - indexName: string = 'test-index-name' + indexName: string ): Promise { try { const securityCheck = await client.security.hasPrivileges({ @@ -46,14 +46,15 @@ export async function fetchUserStartPrivileges( index: [ { names: [indexName], - privileges: ['create_index'], + privileges: ['manage', 'delete'], }, ], }); return { privileges: { - canCreateIndex: securityCheck?.index?.[indexName]?.create_index ?? false, + canManageIndex: securityCheck?.index?.[indexName]?.manage ?? false, + canDeleteDocuments: securityCheck?.index?.[indexName]?.delete ?? false, canCreateApiKeys: securityCheck?.cluster?.manage_api_key ?? false, }, }; @@ -62,7 +63,8 @@ export async function fetchUserStartPrivileges( logger.error(e); return { privileges: { - canCreateIndex: false, + canManageIndex: false, + canDeleteDocuments: false, canCreateApiKeys: false, }, }; diff --git a/x-pack/plugins/search_indices/server/routes/status.ts b/x-pack/plugins/search_indices/server/routes/status.ts index b135499634487..3ed068780f7d8 100644 --- a/x-pack/plugins/search_indices/server/routes/status.ts +++ b/x-pack/plugins/search_indices/server/routes/status.ts @@ -8,6 +8,7 @@ import type { IRouter } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; +import { schema } from '@kbn/config-schema'; import { GET_STATUS_ROUTE, GET_USER_PRIVILEGES_ROUTE } from '../../common/routes'; import { fetchIndicesStatus, fetchUserStartPrivileges } from '../lib/status'; @@ -35,15 +36,22 @@ export function registerStatusRoutes(router: IRouter, logger: Logger) { router.get( { path: GET_USER_PRIVILEGES_ROUTE, - validate: {}, + validate: { + params: schema.object({ + indexName: schema.string(), + }), + }, options: { access: 'internal', }, }, - async (context, _request, response) => { + async (context, request, response) => { const core = await context.core; const client = core.elasticsearch.client.asCurrentUser; - const body = await fetchUserStartPrivileges(client, logger); + + const { indexName } = request.params; + + const body = await fetchUserStartPrivileges(client, logger, indexName); return response.ok({ body, diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index a0c5e0e47f608..3562add33c037 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -7465,7 +7465,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "Afficher {amount} champs en plus", "searchIndexDocuments.result.header.metadata.deleteDocument": "Supprimer le document", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "Métadonnées pour le document : {id}", - "searchIndexDocuments.result.header.metadata.score": "Score", "searchIndexDocuments.result.header.metadata.title": "Métadonnées du document", "searchIndexDocuments.result.title.id": "ID de document : {id}", "searchIndexDocuments.result.value.denseVector.copy": "Copier le vecteur", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 86704cb692733..213ca78b9a35e 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -7457,7 +7457,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "表示するフィールド数を{amount}個増やす", "searchIndexDocuments.result.header.metadata.deleteDocument": "ドキュメントを削除", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "ドキュメント{id}のメタデータ", - "searchIndexDocuments.result.header.metadata.score": "スコア", "searchIndexDocuments.result.header.metadata.title": "ドキュメントメタデータ", "searchIndexDocuments.result.title.id": "ドキュメントID:{id}", "searchIndexDocuments.result.value.denseVector.copy": "ベクトルをコピー", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 10d49d7024598..2e775b82f8474 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -7305,7 +7305,6 @@ "searchIndexDocuments.result.expandTooltip.showMore": "显示多于 {amount} 个字段", "searchIndexDocuments.result.header.metadata.deleteDocument": "删除文档", "searchIndexDocuments.result.header.metadata.icon.ariaLabel": "以下文档的元数据:{id}", - "searchIndexDocuments.result.header.metadata.score": "分数", "searchIndexDocuments.result.header.metadata.title": "文档元数据", "searchIndexDocuments.result.title.id": "文档 ID:{id}", "searchIndexDocuments.result.value.denseVector.copy": "复制向量", diff --git a/x-pack/test/functional/page_objects/index_management_page.ts b/x-pack/test/functional/page_objects/index_management_page.ts index 8053293f98633..e5a2604294675 100644 --- a/x-pack/test/functional/page_objects/index_management_page.ts +++ b/x-pack/test/functional/page_objects/index_management_page.ts @@ -159,6 +159,28 @@ export function IndexManagementPageProvider({ getService }: FtrProviderContext) const url = await browser.getCurrentUrl(); expect(url).to.contain(`tab=${tabId}`); }, + async expectEditSettingsToBeEnabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(true); + }, + async expectIndexDetailsMappingsAddFieldToBeEnabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(true); + }, + async expectTabsExists() { + await testSubjects.existOrFail('indexDetailsTab-mappings', { timeout: 2000 }); + await testSubjects.existOrFail('indexDetailsTab-overview', { timeout: 2000 }); + await testSubjects.existOrFail('indexDetailsTab-settings', { timeout: 2000 }); + }, + async changeTab( + tab: 'indexDetailsTab-mappings' | 'indexDetailsTab-overview' | 'indexDetailsTab-settings' + ) { + await testSubjects.click(tab); + }, }, async clickCreateIndexButton() { await testSubjects.click('createIndexButton'); diff --git a/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts b/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts index 33a2a438016b9..e92cc62296849 100644 --- a/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts +++ b/x-pack/test_serverless/api_integration/test_suites/search/search_indices/status.ts @@ -13,6 +13,7 @@ export default function ({ getService }: FtrProviderContext) { const roleScopedSupertest = getService('roleScopedSupertest'); let supertestDeveloperWithCookieCredentials: SupertestWithRoleScopeType; let supertestViewerWithCookieCredentials: SupertestWithRoleScopeType; + const testIndexName = 'search-test-index'; describe('search_indices Status APIs', function () { describe('indices status', function () { @@ -37,37 +38,41 @@ export default function ({ getService }: FtrProviderContext) { describe('developer', function () { it('returns expected privileges', async () => { const { body } = await supertestDeveloperWithCookieCredentials - .get('/internal/search_indices/start_privileges') + .get(`/internal/search_indices/start_privileges/${testIndexName}`) .expect(200); expect(body).toEqual({ privileges: { canCreateApiKeys: true, - canCreateIndex: true, + canDeleteDocuments: true, + canManageIndex: true, }, }); }); }); - describe('viewer', function () { - before(async () => { - supertestViewerWithCookieCredentials = - await roleScopedSupertest.getSupertestWithRoleScope('viewer', { - useCookieHeader: true, - withInternalHeaders: true, - }); - }); + }); + describe('viewer', function () { + before(async () => { + supertestViewerWithCookieCredentials = await roleScopedSupertest.getSupertestWithRoleScope( + 'viewer', + { + useCookieHeader: true, + withInternalHeaders: true, + } + ); + }); - it('returns expected privileges', async () => { - const { body } = await supertestViewerWithCookieCredentials - .get('/internal/search_indices/start_privileges') - .expect(200); + it('returns expected privileges', async () => { + const { body } = await supertestViewerWithCookieCredentials + .get(`/internal/search_indices/start_privileges/${testIndexName}`) + .expect(200); - expect(body).toEqual({ - privileges: { - canCreateApiKeys: false, - canCreateIndex: false, - }, - }); + expect(body).toEqual({ + privileges: { + canCreateApiKeys: false, + canDeleteDocuments: false, + canManageIndex: false, + }, }); }); }); diff --git a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts index 277b4d2c7ada2..0609b2bec4aed 100644 --- a/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts +++ b/x-pack/test_serverless/functional/page_objects/svl_search_index_detail_page.ts @@ -100,6 +100,18 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectAPIReferenceDocLinkMissingInMoreOptions() { await testSubjects.missingOrFail('moreOptionsApiReference', { timeout: 2000 }); }, + async expectDeleteIndexButtonToBeDisabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(false); + await testSubjects.moveMouseTo('moreOptionsDeleteIndex'); + await testSubjects.existOrFail('moreOptionsDeleteIndexTooltip'); + }, + async expectDeleteIndexButtonToBeEnabled() { + await testSubjects.existOrFail('moreOptionsDeleteIndex'); + const deleteIndexButton = await testSubjects.isEnabled('moreOptionsDeleteIndex'); + expect(deleteIndexButton).to.be(true); + }, async expectDeleteIndexButtonExistsInMoreOptions() { await testSubjects.existOrFail('moreOptionsDeleteIndex'); }, @@ -132,11 +144,11 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont await testSubjects.click('reloadButton', 2000); }); }, - async expectWithDataTabsExists() { + async expectTabsExists() { await testSubjects.existOrFail('mappingsTab', { timeout: 2000 }); await testSubjects.existOrFail('dataTab', { timeout: 2000 }); }, - async withDataChangeTabs(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { + async changeTab(tab: 'dataTab' | 'mappingsTab' | 'settingsTab') { await testSubjects.click(tab); }, async expectUrlShouldChangeTo(tab: 'data' | 'mappings' | 'settings') { @@ -148,6 +160,22 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont async expectSettingsComponentIsVisible() { await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); }, + async expectEditSettingsIsDisabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsSettingsEditModeSwitch'); + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitchToolTip'); + }, + async expectEditSettingsToBeEnabled() { + await testSubjects.existOrFail('indexDetailsSettingsEditModeSwitch', { timeout: 2000 }); + const isEditSettingsButtonDisabled = await testSubjects.isEnabled( + 'indexDetailsSettingsEditModeSwitch' + ); + expect(isEditSettingsButtonDisabled).to.be(true); + }, async expectSelectedLanguage(language: string) { await testSubjects.existOrFail('codeExampleLanguageSelect'); expect( @@ -186,12 +214,28 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont await testSubjects.existOrFail('deleteDocumentButton'); await testSubjects.click('deleteDocumentButton'); }, - async expectDeleteDocumentActionNotVisible() { await testSubjects.existOrFail('documentMetadataButton'); await testSubjects.click('documentMetadataButton'); await testSubjects.missingOrFail('deleteDocumentButton'); }, + async expectDeleteDocumentActionIsDisabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(false); + await testSubjects.moveMouseTo('deleteDocumentButton'); + await testSubjects.existOrFail('deleteDocumentButtonToolTip'); + }, + async expectDeleteDocumentActionToBeEnabled() { + await testSubjects.existOrFail('documentMetadataButton'); + await testSubjects.click('documentMetadataButton'); + await testSubjects.existOrFail('deleteDocumentButton'); + const isDeleteDocumentEnabled = await testSubjects.isEnabled('deleteDocumentButton'); + expect(isDeleteDocumentEnabled).to.be(true); + }, + async openIndicesDetailFromIndexManagementIndicesListTable(indexOfRow: number) { const indexList = await testSubjects.findAll('indexTableIndexNameLink'); await indexList[indexOfRow].click(); @@ -219,5 +263,19 @@ export function SvlSearchIndexDetailPageProvider({ getService }: FtrProviderCont } } }, + + async expectAddFieldToBeDisabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(false); + await testSubjects.moveMouseTo('indexDetailsMappingsAddField'); + await testSubjects.existOrFail('indexDetailsMappingsAddFieldTooltip'); + }, + + async expectAddFieldToBeEnabled() { + await testSubjects.existOrFail('indexDetailsMappingsAddField'); + const isMappingsFieldEnabled = await testSubjects.isEnabled('indexDetailsMappingsAddField'); + expect(isMappingsFieldEnabled).to.be(true); + }, }; } diff --git a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts index be3b683d9903a..7330a5d162240 100644 --- a/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/common/management/index_management/index_detail.ts @@ -38,6 +38,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { it('index with no documents', async () => { await pageObjects.indexManagement.indexDetailsPage.openIndexDetailsPage(0); await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsPageIsLoaded(); + await pageObjects.indexManagement.indexDetailsPage.expectTabsExists(); + }); + it('can add mappings', async () => { + await pageObjects.indexManagement.indexDetailsPage.changeTab('indexDetailsTab-mappings'); + await pageObjects.indexManagement.indexDetailsPage.expectIndexDetailsMappingsAddFieldToBeEnabled(); + }); + it('can edit settings', async () => { + await pageObjects.indexManagement.indexDetailsPage.changeTab('indexDetailsTab-settings'); + await pageObjects.indexManagement.indexDetailsPage.expectEditSettingsToBeEnabled(); }); }); }); diff --git a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts index 0070ce7e2cb43..5aa2627a3cdf4 100644 --- a/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts +++ b/x-pack/test_serverless/functional/test_suites/search/search_index_detail.ts @@ -24,210 +24,286 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esDeleteAllIndices = getService('esDeleteAllIndices'); const indexName = 'test-my-index'; - describe('Search index detail page', function () { - before(async () => { - await pageObjects.svlCommonPage.loginWithRole('developer'); - await pageObjects.svlApiKeys.deleteAPIKeys(); - }); - after(async () => { - await esDeleteAllIndices(indexName); - }); - - describe('index details page overview', () => { + describe('index details page - search solution', function () { + describe('developer', function () { before(async () => { - await es.indices.create({ index: indexName }); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlCommonPage.loginWithRole('developer'); + await pageObjects.svlApiKeys.deleteAPIKeys(); }); after(async () => { await esDeleteAllIndices(indexName); }); - it('can load index detail page', async () => { - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkMissingInMoreOptions(); - }); - it('should have embedded dev console', async () => { - await testHasEmbeddedConsole(pageObjects); - }); - it('should have connection details', async () => { - await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); - }); - - it.skip('should show api key', async () => { - await pageObjects.svlApiKeys.deleteAPIKeys(); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await pageObjects.svlApiKeys.expectAPIKeyAvailable(); - const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI(); - await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); - }); - - it('should have quick stats', async () => { - await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); - await es.indices.putMapping({ - index: indexName, - body: { - properties: { - my_field: { - type: 'dense_vector', - dims: 3, - }, - }, - }, + describe('search index details page', () => { + before(async () => { + await es.indices.create({ index: indexName }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + after(async () => { + await esDeleteAllIndices(indexName); + }); + it('can load index detail page', async () => { + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkMissingInMoreOptions(); + }); + it('should have embedded dev console', async () => { + await testHasEmbeddedConsole(pageObjects); + }); + it('should have connection details', async () => { + await pageObjects.svlSearchIndexDetailPage.expectConnectionDetails(); }); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); - }); - - it('should have breadcrumb navigation', async () => { - await pageObjects.svlSearchIndexDetailPage.expectBreadcrumbNavigationWithIndexName( - indexName - ); - await pageObjects.svlSearchIndexDetailPage.clickOnIndexManagementBreadcrumb(); - await pageObjects.indexManagement.expectToBeOnIndicesManagement(); - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - }); - it('should show code examples for adding documents', async () => { - await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); - await pageObjects.svlSearchIndexDetailPage.expectSelectedLanguage('python'); - await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( - 'installCodeExample', - 'pip install' - ); - await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('javascript'); - await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( - 'installCodeExample', - 'npm install' - ); - await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('curl'); - await pageObjects.svlSearchIndexDetailPage.openConsoleCodeExample(); - await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); - await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); - }); + it.skip('should show api key', async () => { + await pageObjects.svlApiKeys.deleteAPIKeys(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlApiKeys.expectAPIKeyAvailable(); + const apiKey = await pageObjects.svlApiKeys.getAPIKeyFromUI(); + await pageObjects.svlSearchIndexDetailPage.expectAPIKeyToBeVisibleInCodeBlock(apiKey); + }); - // FLAKY: https://github.com/elastic/kibana/issues/197144 - describe.skip('With data', () => { - before(async () => { - await es.index({ + it('should have quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStats(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappings(); + await es.indices.putMapping({ index: indexName, body: { - my_field: [1, 0, 1], + properties: { + my_field: { + type: 'dense_vector', + dims: 3, + }, + }, }, }); await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsAIMappingsToHaveVectorFields(); }); - it('should have index documents', async () => { - await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); - }); - it('menu action item should be replaced with playground', async () => { - await pageObjects.svlSearchIndexDetailPage.expectActionItemReplacedWhenHasDocs(); - }); - it('should have link to API reference doc link in options menu', async () => { - await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + + it('should have breadcrumb navigation', async () => { + await pageObjects.svlSearchIndexDetailPage.expectBreadcrumbNavigationWithIndexName( + indexName + ); + await pageObjects.svlSearchIndexDetailPage.clickOnIndexManagementBreadcrumb(); + await pageObjects.indexManagement.expectToBeOnIndicesManagement(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); }); - it('should have one document in quick stats', async () => { - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(1); + + it('should show code examples for adding documents', async () => { + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + await pageObjects.svlSearchIndexDetailPage.expectSelectedLanguage('python'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'pip install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('javascript'); + await pageObjects.svlSearchIndexDetailPage.codeSampleContainsValue( + 'installCodeExample', + 'npm install' + ); + await pageObjects.svlSearchIndexDetailPage.selectCodingLanguage('curl'); + await pageObjects.svlSearchIndexDetailPage.openConsoleCodeExample(); + await pageObjects.embeddedConsole.expectEmbeddedConsoleToBeOpen(); + await pageObjects.embeddedConsole.clickEmbeddedConsoleControlBar(); }); - it('should have with data tabs', async () => { - await pageObjects.svlSearchIndexDetailPage.expectWithDataTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + + // FLAKY: https://github.com/elastic/kibana/issues/197144 + describe.skip('With data', () => { + before(async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + it('should have index documents', async () => { + await pageObjects.svlSearchIndexDetailPage.expectHasIndexDocuments(); + }); + it('menu action item should be replaced with playground', async () => { + await pageObjects.svlSearchIndexDetailPage.expectActionItemReplacedWhenHasDocs(); + }); + it('should have link to API reference doc link in options menu', async () => { + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExistsInMoreOptions(); + }); + it('should have one document in quick stats', async () => { + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(1); + }); + it('should have with data tabs', async () => { + await pageObjects.svlSearchIndexDetailPage.expectTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + }); + it('should be able to change tabs to mappings and mappings is shown', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible(); + }); + it('should be able to change tabs to settings and settings is shown', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); + }); + it('should be able to delete document', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('dataTab'); + await pageObjects.svlSearchIndexDetailPage.clickFirstDocumentDeleteAction(); + await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); + await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(0); + }); }); - it('should be able to change tabs to mappings and mappings is shown', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('mappingsTab'); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); - await pageObjects.svlSearchIndexDetailPage.expectMappingsComponentIsVisible(); + describe('has index actions enabled', () => { + before(async () => { + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + + beforeEach(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + + it('delete document button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteDocumentActionToBeEnabled(); + }); + it('add field button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectAddFieldToBeEnabled(); + }); + it('edit settings button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectEditSettingsToBeEnabled(); + }); + it('delete index button is enabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonToBeEnabled(); + }); }); - it('should be able to change tabs to settings and settings is shown', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('settingsTab'); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); - await pageObjects.svlSearchIndexDetailPage.expectSettingsComponentIsVisible(); + + describe('page loading error', () => { + before(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + await esDeleteAllIndices(indexName); + }); + it('has page load error section', async () => { + await pageObjects.svlSearchIndexDetailPage.expectPageLoadErrorExists(); + await pageObjects.svlSearchIndexDetailPage.expectIndexNotFoundErrorExists(); + }); + it('reload button shows details page again', async () => { + await es.indices.create({ index: indexName }); + await pageObjects.svlSearchIndexDetailPage.clickPageReload(); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + }); }); - it('should be able to delete document', async () => { - await pageObjects.svlSearchIndexDetailPage.withDataChangeTabs('dataTab'); - await pageObjects.svlSearchIndexDetailPage.clickFirstDocumentDeleteAction(); - await pageObjects.svlSearchIndexDetailPage.expectAddDocumentCodeExamples(); - await pageObjects.svlSearchIndexDetailPage.expectQuickStatsToHaveDocumentCount(0); + describe('Index more options menu', () => { + before(async () => { + await svlSearchNavigation.navigateToIndexDetailPage(indexName); + }); + it('shows action menu in actions popover', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + }); + it('should delete index', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.clickDeleteIndexButton(); + await pageObjects.svlSearchIndexDetailPage.clickConfirmingDeleteIndex(); + }); }); }); - - describe('page loading error', () => { + describe('index management index list page', () => { before(async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); - await esDeleteAllIndices(indexName); - }); - it('has page load error section', async () => { - await pageObjects.svlSearchIndexDetailPage.expectPageLoadErrorExists(); - await pageObjects.svlSearchIndexDetailPage.expectIndexNotFoundErrorExists(); - }); - it('reload button shows details page again', async () => { await es.indices.create({ index: indexName }); - await pageObjects.svlSearchIndexDetailPage.clickPageReload(); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await security.testUser.setRoles(['index_management_user']); }); - }); - describe('Index more options menu', () => { - before(async () => { - await svlSearchNavigation.navigateToIndexDetailPage(indexName); + beforeEach(async () => { + await pageObjects.common.navigateToApp('indexManagement'); + // Navigate to the indices tab + await pageObjects.indexManagement.changeTabs('indicesTab'); + await pageObjects.header.waitUntilLoadingHasFinished(); }); - it('shows action menu in actions popover', async () => { - await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); - await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); - await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + after(async () => { + await esDeleteAllIndices(indexName); }); - it('should delete index', async () => { - await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); - await pageObjects.svlSearchIndexDetailPage.clickDeleteIndexButton(); - await pageObjects.svlSearchIndexDetailPage.clickConfirmingDeleteIndex(); + describe('manage index action', () => { + beforeEach(async () => { + await pageObjects.indexManagement.manageIndex(indexName); + await pageObjects.indexManagement.manageIndexContextMenuExists(); + }); + it('navigates to overview tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + }); + + it('navigates to settings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + }); + it('navigates to mappings tab', async () => { + await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + }); + }); + describe('can view search index details', function () { + it('renders search index details with no documents', async () => { + await pageObjects.svlSearchIndexDetailPage.openIndicesDetailFromIndexManagementIndicesListTable( + 0 + ); + await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); + await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); + await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + }); }); }); }); - describe('index management index details', () => { + + describe('viewer', function () { before(async () => { - await es.indices.create({ index: indexName }); - await security.testUser.setRoles(['index_management_user']); - }); - beforeEach(async () => { - await pageObjects.common.navigateToApp('indexManagement'); - // Navigate to the indices tab - await pageObjects.indexManagement.changeTabs('indicesTab'); - await pageObjects.header.waitUntilLoadingHasFinished(); + await esDeleteAllIndices(indexName); + await es.index({ + index: indexName, + body: { + my_field: [1, 0, 1], + }, + }); }); after(async () => { await esDeleteAllIndices(indexName); }); - describe('manage index action', () => { + describe('search index details page', function () { + before(async () => { + await pageObjects.svlCommonPage.loginAsViewer(); + }); beforeEach(async () => { - await pageObjects.indexManagement.manageIndex(indexName); - await pageObjects.indexManagement.manageIndexContextMenuExists(); + await svlSearchNavigation.navigateToIndexDetailPage(indexName); }); - it('navigates to overview tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showOverviewIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('data'); + it('delete document button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectDeleteDocumentActionIsDisabled(); }); - - it('navigates to settings tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showSettingsIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('settings'); + it('add field button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('mappingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectAddFieldToBeDisabled(); }); - it('navigates to mappings tab', async () => { - await pageObjects.indexManagement.changeManageIndexTab('showMappingsIndexMenuButton'); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectUrlShouldChangeTo('mappings'); + it('edit settings button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.changeTab('settingsTab'); + await pageObjects.svlSearchIndexDetailPage.expectEditSettingsIsDisabled(); }); - }); - describe('can view search index details', function () { - it('renders search index details with no documents', async () => { - await pageObjects.svlSearchIndexDetailPage.openIndicesDetailFromIndexManagementIndicesListTable( - 0 - ); - await pageObjects.svlSearchIndexDetailPage.expectIndexDetailPageHeader(); - await pageObjects.svlSearchIndexDetailPage.expectSearchIndexDetailsTabsExists(); - await pageObjects.svlSearchIndexDetailPage.expectAPIReferenceDocLinkExists(); + it('delete index button is disabled', async () => { + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsActionButtonExists(); + await pageObjects.svlSearchIndexDetailPage.clickMoreOptionsActionsButton(); + await pageObjects.svlSearchIndexDetailPage.expectMoreOptionsOverviewMenuIsShown(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonExistsInMoreOptions(); + await pageObjects.svlSearchIndexDetailPage.expectDeleteIndexButtonToBeDisabled(); }); }); }); From 1122a80d019081c0cec1c348ba82a8d96bb34592 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 16:17:13 +0000 Subject: [PATCH 39/50] [Ownership] Assign maps test files to presentation team (#200214) ## Summary Assign maps test files to presentation team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Nick Peihl --- .github/CODEOWNERS | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index c2450338f3e45..7b4cd85b0d8d0 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1382,6 +1382,17 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation /x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation +/x-pack/test/upgrade/services/maps_upgrade_services.ts @elastic/kibana-presentation +/x-pack/test/stack_functional_integration/apps/maps @elastic/kibana-presentation +/x-pack/test/functional/page_objects/geo_file_upload.ts @elastic/kibana-presentation +/x-pack/test/functional/page_objects/gis_page.ts @elastic/kibana-presentation +/x-pack/test/upgrade/apps/maps @elastic/kibana-presentation +/x-pack/test/api_integration/apis/maps/ @elastic/kibana-presentation +/x-pack/test/functional/apps/maps/ @elastic/kibana-presentation +/x-pack/test/functional/es_archives/maps/ @elastic/kibana-presentation +/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-presentation +/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-presentation + # Machine Learning /x-pack/test/stack_functional_integration/apps/ml @elastic/ml-ui @@ -1421,15 +1432,6 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai /x-pack/test/functional/services/aiops @elastic/ml-ui /x-pack/test/functional_basic/apps/transform/ @elastic/ml-ui -# Maps -#CC# /x-pack/plugins/maps/ @elastic/kibana-gis -/x-pack/test/api_integration/apis/maps/ @elastic/kibana-gis -/x-pack/test/functional/apps/maps/ @elastic/kibana-gis -/x-pack/test/functional/es_archives/maps/ @elastic/kibana-gis -/x-pack/plugins/stack_alerts/server/rule_types/geo_containment @elastic/kibana-gis -/x-pack/plugins/stack_alerts/public/rule_types/geo_containment @elastic/kibana-gis -#CC# /x-pack/plugins/file_upload @elastic/kibana-gis - # Operations /test/package @elastic/kibana-operations /test/package/roles @elastic/kibana-operations From d3d711ddbb95d78a5e2ef92637d1b127df60633d Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 18 Nov 2024 17:23:05 +0100 Subject: [PATCH 40/50] [Security Solution] [Bug] Index Values are not available in dropdown under New Index Enter for Knowledge Base. (#199610) (#199990) ## Summary This PR fixes the issue described here https://github.com/elastic/kibana/issues/199610#issuecomment-2473393109 In short, we do not show all indices with `semantic_text` fields. We only show those which have `semantic_text` fields named `content`. ### Testing notes Add the index with the `semantic_text` field by running this command in dev tools: ``` PUT test_index { "mappings": { "properties": { "attachment": { "properties": { "content": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "content_length": { "type": "long" }, "content_type": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "format": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "language": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } }, "content2": { "type": "semantic_text", "inference_id": "elastic-security-ai-assistant-elser2" } } } } ``` You should see the `test_index` index in the `Knowledge Base > New > Index > Index (dropdown)`. ### Checklist Delete any items that are not applicable to this PR. - [ ] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: Steph Milovic --- .../get_knowledge_base_indices.test.ts | 74 ++++++++++++++++--- .../get_knowledge_base_indices.ts | 8 +- 2 files changed, 68 insertions(+), 14 deletions(-) diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts index e7eaa75407248..d5e92cb8d682e 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.test.ts @@ -12,12 +12,15 @@ import { getGetKnowledgeBaseIndicesRequest } from '../../__mocks__/request'; const mockFieldCaps = { indices: [ - '.ds-logs-endpoint.alerts-default-2024.10.31-000001', - '.ds-metrics-endpoint.metadata-default-2024.10.31-000001', - '.internal.alerts-security.alerts-default-000001', + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-bug', + 'gtr-with-semantic-1', 'metrics-endpoint.metadata_current_default', - 'semantic-index-1', - 'semantic-index-2', + 'search-elastic-security-docs', ], fields: { content: { @@ -27,9 +30,12 @@ const mockFieldCaps = { searchable: false, aggregatable: false, indices: [ - '.ds-logs-endpoint.alerts-default-2024.10.31-000001', - '.ds-metrics-endpoint.metadata-default-2024.10.31-000001', - '.internal.alerts-security.alerts-default-000001', + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-bug', 'metrics-endpoint.metadata_current_default', ], }, @@ -38,7 +44,55 @@ const mockFieldCaps = { metadata_field: false, searchable: true, aggregatable: false, - indices: ['semantic-index-1', 'semantic-index-2'], + indices: ['gtr-with-semantic-1'], + }, + }, + ai_embeddings: { + unmapped: { + type: 'unmapped', + metadata_field: false, + searchable: false, + aggregatable: false, + indices: [ + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-semantic-1', + 'metrics-endpoint.metadata_current_default', + ], + }, + semantic_text: { + type: 'semantic_text', + metadata_field: false, + searchable: true, + aggregatable: false, + indices: ['gtr-with-bug', 'search-elastic-security-docs'], + }, + }, + semantic_text: { + unmapped: { + type: 'unmapped', + metadata_field: false, + searchable: false, + aggregatable: false, + indices: [ + '.ds-.items-default-2024.11.12-000001', + '.ds-.lists-default-2024.11.12-000001', + '.ds-logs-endpoint.alerts-default-2024.11.12-000001', + '.ds-logs-endpoint.events.process-default-2024.11.12-000001', + 'gtr-1', + 'gtr-with-semantic-1', + 'metrics-endpoint.metadata_current_default', + ], + }, + semantic_text: { + type: 'semantic_text', + metadata_field: false, + searchable: true, + aggregatable: false, + indices: ['search-elastic-security-docs'], }, }, }, @@ -66,7 +120,7 @@ describe('Get Knowledge Base Status Route', () => { expect(response.status).toEqual(200); expect(response.body).toEqual({ - indices: ['semantic-index-1', 'semantic-index-2'], + indices: ['gtr-with-bug', 'gtr-with-semantic-1', 'search-elastic-security-docs'], }); expect(context.core.elasticsearch.client.asCurrentUser.fieldCaps).toBeCalledWith({ index: '*', diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts index 18191291468de..5106c31d39e7d 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/get_knowledge_base_indices.ts @@ -53,10 +53,10 @@ export const getKnowledgeBaseIndicesRoute = (router: ElasticAssistantPluginRoute include_unmapped: true, }); - const indices = res.fields.content?.semantic_text?.indices; - if (indices) { - body.indices = Array.isArray(indices) ? indices : [indices]; - } + body.indices = Object.values(res.fields) + .flatMap((value) => value.semantic_text?.indices ?? []) + .filter((value, index, self) => self.indexOf(value) === index) + .sort(); return response.ok({ body }); } catch (err) { From 3e227c782a668ba0610f64863aad13e8341757b1 Mon Sep 17 00:00:00 2001 From: Ievgen Sorokopud Date: Mon, 18 Nov 2024 17:24:04 +0100 Subject: [PATCH 41/50] [Security GenAI] Fetching Assistant Knowledge Base fails when current user's username contains a : character (#11159) (#200131) ## Summary Original bug: [internal link](https://github.com/elastic/security-team/issues/11159) **This PR fixes the next bug**: When the user is logged in with a username that contains a `:` character, fetching Knowlege Base entries fails with an error. This is preventing customers from viewing their created KB entries. This problem affects ECE customers using the SSO login option. There were a similar bugfix which inspired this one https://github.com/elastic/kibana/pull/181709 There is no easy way to reproduce this but you can try and change the line in question so that the faulty username is used instead of the one passed in. @MadameSheema Do you know a way to login locally with the username that contains a `:` character? As mentioned above this situation is possible with the ECE customers using SSO login. ### Checklist Check the PR satisfies following conditions. Reviewers should verify this PR satisfies this list as well. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../knowledge_base/entries/utils.test.ts | 43 +++++++++++++++++++ .../routes/knowledge_base/entries/utils.ts | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts new file mode 100644 index 0000000000000..e718ff44630c7 --- /dev/null +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.test.ts @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AuthenticatedUser } from '@kbn/core-security-common'; +import { getKBUserFilter } from './utils'; + +describe('Utils', () => { + describe('getKBUserFilter', () => { + it('should return global filter when user is null', () => { + const filter = getKBUserFilter(null); + expect(filter).toEqual('(NOT users: {name:* OR id:* })'); + }); + + it('should return global filter when `username` and `profile_uid` are undefined', () => { + const filter = getKBUserFilter({} as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* })'); + }); + + it('should return global filter when `username` is undefined', () => { + const filter = getKBUserFilter({ profile_uid: 'fake_user_id' } as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* } OR users: {id: fake_user_id})'); + }); + + it('should return global filter when `profile_uid` is undefined', () => { + const filter = getKBUserFilter({ username: 'user1' } as AuthenticatedUser); + expect(filter).toEqual('(NOT users: {name:* OR id:* } OR users: {name: "user1"})'); + }); + + it('should return global filter when `username` has semicolon', () => { + const filter = getKBUserFilter({ + username: 'user:1', + profile_uid: 'fake_user_id', + } as AuthenticatedUser); + expect(filter).toEqual( + '(NOT users: {name:* OR id:* } OR (users: {name: "user:1"} OR users: {id: fake_user_id}))' + ); + }); + }); +}); diff --git a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts index 3a548cd812539..0f5a0ab97fb29 100644 --- a/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts +++ b/x-pack/plugins/elastic_assistant/server/routes/knowledge_base/entries/utils.ts @@ -11,7 +11,7 @@ export const getKBUserFilter = (user: AuthenticatedUser | null) => { // Only return the current users entries and all other global entries (where user[] is empty) const globalFilter = 'NOT users: {name:* OR id:* }'; - const nameFilter = user?.username ? `users: {name: ${user?.username}}` : ''; + const nameFilter = user?.username ? `users: {name: "${user?.username}"}` : ''; const idFilter = user?.profile_uid ? `users: {id: ${user?.profile_uid}}` : ''; const userFilter = user?.username && user?.profile_uid From 17f1925ccffbc8b3daab1f6765d968394065e7aa Mon Sep 17 00:00:00 2001 From: jennypavlova Date: Mon, 18 Nov 2024 17:27:32 +0100 Subject: [PATCH 42/50] [Inventory] Remove open in Discover button (#200574) Closes #199964 This PR removes the `open in discover` button from the inventory search bar section. | Before | After | | ------- | ----- | | image | image | --- .../inventory/e2e/cypress/e2e/home.cy.ts | 29 ------------- .../components/search_bar/discover_button.tsx | 35 ---------------- .../public/components/search_bar/index.tsx | 42 +++++++------------ .../translations/translations/fr-FR.json | 2 +- .../translations/translations/ja-JP.json | 2 +- .../translations/translations/zh-CN.json | 2 +- 6 files changed, 18 insertions(+), 94 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx diff --git a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts index 17b6cf502280a..c9d341c708965 100644 --- a/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts +++ b/x-pack/plugins/observability_solution/inventory/e2e/cypress/e2e/home.cy.ts @@ -121,35 +121,6 @@ describe('Home page', () => { cy.url().should('include', '/app/metrics/detail/host/server1'); }); - it('Navigates to discover with default filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:(language:kuery,query:'entity.definition_id%20:%20builtin*" - ); - }); - - it('Navigates to discover with kuery filter', () => { - cy.intercept('GET', '/internal/entities/managed/enablement', { - fixture: 'eem_enabled.json', - }).as('getEEMStatus'); - cy.visitKibana('/app/inventory'); - cy.wait('@getEEMStatus'); - cy.getByTestSubj('queryInput').type('service.name : foo'); - - cy.contains('Update').click(); - cy.contains('Open in discover').click(); - cy.url().should( - 'include', - "query:'service.name%20:%20foo%20AND%20entity.definition_id%20:%20builtin*'" - ); - }); - it('Navigates to infra when clicking on a container type entity', () => { cy.intercept('GET', '/internal/entities/managed/enablement', { fixture: 'eem_enabled.json', diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx deleted file mode 100644 index 13477d63e5f82..0000000000000 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/discover_button.tsx +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButton } from '@elastic/eui'; -import { DataView } from '@kbn/data-views-plugin/public'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { useDiscoverRedirect } from '../../hooks/use_discover_redirect'; - -export function DiscoverButton({ dataView }: { dataView: DataView }) { - const { getDiscoverRedirectUrl } = useDiscoverRedirect(); - - const discoverLink = getDiscoverRedirectUrl(); - - if (!discoverLink) { - return null; - } - - return ( - - {i18n.translate('xpack.inventory.searchBar.discoverButton', { - defaultMessage: 'Open in discover', - })} - - ); -} diff --git a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx index d1ccfd3f358e3..3464c5749dbc3 100644 --- a/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx +++ b/x-pack/plugins/observability_solution/inventory/public/components/search_bar/index.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { Query } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; import type { SearchBarOwnProps } from '@kbn/unified-search-plugin/public/search_bar'; @@ -14,7 +13,6 @@ import { useKibana } from '../../hooks/use_kibana'; import { useUnifiedSearchContext } from '../../hooks/use_unified_search_context'; import { getKqlFieldsWithFallback } from '../../utils/get_kql_field_names_with_fallback'; import { ControlGroups } from './control_groups'; -import { DiscoverButton } from './discover_button'; export function SearchBar() { const { refreshSubject$, dataView, searchState, onQueryChange } = useUnifiedSearchContext(); @@ -73,30 +71,20 @@ export function SearchBar() { ); return ( - - - } - onQuerySubmit={handleQuerySubmit} - placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { - defaultMessage: - 'Search for your entities by name or its metadata (e.g. entity.type : service)', - })} - showDatePicker={false} - showFilterBar - showQueryInput - showQueryMenu - /> - - - {dataView ? ( - - - - ) : null} - + } + onQuerySubmit={handleQuerySubmit} + placeholder={i18n.translate('xpack.inventory.searchBar.placeholder', { + defaultMessage: + 'Search for your entities by name or its metadata (e.g. entity.type : service)', + })} + showDatePicker={false} + showFilterBar + showQueryInput + showQueryMenu + /> ); } diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 3562add33c037..5b9817239e928 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -26199,6 +26199,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "Ouvrir la fenêtre contextuelle", "xpack.inventory.data_view.creation_failed": "Une erreur s'est produite lors de la création de la vue de données", "xpack.inventory.eemEnablement.errorTitle": "Erreur lors de l'activation du nouveau modèle d'entité", + "xpack.inventory.entityActions.discoverLink": "Ouvrir dans Discover", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "Alertes", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "Le nombre d'alertes actives", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "Nom de l'entité", @@ -26228,7 +26229,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "L'affichage de vos entités peut prendre quelques minutes. Essayez de rafraîchir à nouveau dans une minute ou deux.", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "En savoir plus", "xpack.inventory.noEntitiesEmptyState.title": "Aucune entité disponible", - "xpack.inventory.searchBar.discoverButton": "Ouvrir dans Discover", "xpack.inventory.searchBar.placeholder": "Recherchez vos entités par nom ou par leurs métadonnées (par exemple entity.type : service)", "xpack.inventory.shareLink.shareButtonLabel": "Partager", "xpack.inventory.shareLink.shareToastFailureLabel": "Les URL courtes ne peuvent pas être copiées.", diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 213ca78b9a35e..ba8df6e2f5337 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -26171,6 +26171,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "ポップオーバーを開く", "xpack.inventory.data_view.creation_failed": "データビューの作成中にエラーが発生しました", "xpack.inventory.eemEnablement.errorTitle": "新しいエンティティモデルの有効化エラー", + "xpack.inventory.entityActions.discoverLink": "Discoverで開く", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "アラート", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "アクティブなアラートの件数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "エンティティ名", @@ -26200,7 +26201,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "エンティティが表示されるまで数分かかる場合があります。1〜2分後に更新してください。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "詳細", "xpack.inventory.noEntitiesEmptyState.title": "エンティティがありません", - "xpack.inventory.searchBar.discoverButton": "Discoverで開く", "xpack.inventory.searchBar.placeholder": "エンティティを名前またはメタデータ(例:entity.type : service)で検索します。", "xpack.inventory.shareLink.shareButtonLabel": "共有", "xpack.inventory.shareLink.shareToastFailureLabel": "短縮URLをコピーできません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 2e775b82f8474..bab7f962f26c3 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25701,6 +25701,7 @@ "xpack.inventory.badgeFilterWithPopover.openPopoverBadgeLabel": "打开弹出框", "xpack.inventory.data_view.creation_failed": "创建数据视图时出错", "xpack.inventory.eemEnablement.errorTitle": "启用新实体模型时出错", + "xpack.inventory.entityActions.discoverLink": "在 Discover 中打开", "xpack.inventory.entitiesGrid.euiDataGrid.alertsLabel": "告警", "xpack.inventory.entitiesGrid.euiDataGrid.alertsTooltip": "活动告警计数", "xpack.inventory.entitiesGrid.euiDataGrid.entityNameLabel": "实体名称", @@ -25730,7 +25731,6 @@ "xpack.inventory.noEntitiesEmptyState.description": "您的实体可能需要数分钟才能显示。请尝试在一或两分钟后刷新。", "xpack.inventory.noEntitiesEmptyState.learnMore.link": "了解详情", "xpack.inventory.noEntitiesEmptyState.title": "无可用实体", - "xpack.inventory.searchBar.discoverButton": "在 Discover 中打开", "xpack.inventory.searchBar.placeholder": "按名称或其元数据(例如,entity.type:服务)搜索您的实体", "xpack.inventory.shareLink.shareButtonLabel": "共享", "xpack.inventory.shareLink.shareToastFailureLabel": "无法复制短 URL。", From aa1650e0a8a1886e439b2a9cbe17bc786c7992b4 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 16:32:46 +0000 Subject: [PATCH 43/50] [Ownership] Assign test files to platform security team (#199795) ## Summary Assign test files to platform security team Contributes to: #192979 --------- Co-authored-by: Elastic Machine Co-authored-by: Jeramy Soucy --- .github/CODEOWNERS | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7b4cd85b0d8d0..e51fceb9e2e0f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1586,6 +1586,8 @@ x-pack/test/api_integration/deployment_agnostic/services/ @elastic/appex-qa x-pack/test/**/deployment_agnostic/ @elastic/appex-qa #temporarily to monitor tests migration # Core +/test/api_integration/apis/general/*.js @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/199795/files/894a8ede3f9d0398c5af56bf5a82654a9bc0610b#r1846691639 +/x-pack/test/plugin_api_integration/plugins/feature_usage_test @elastic/kibana-core /test/plugin_functional/plugins/rendering_plugin @elastic/kibana-core /test/plugin_functional/plugins/session_notifications @elastic/kibana-core /x-pack/test/cloud_integration/plugins/saml_provider @elastic/kibana-core @@ -1642,6 +1644,28 @@ x-pack/plugins/cloud_integrations/cloud_full_story/server/config.ts @elastic/kib #CC# /x-pack/plugins/translations/ @elastic/kibana-localization @elastic/kibana-core # Kibana Platform Security +# security +/x-pack/test_serverless/functional/test_suites/observability/role_management @elastic/kibana-security +/x-pack/test/functional/config_security_basic.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/user_profile_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/space_selector_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/security_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/role_mappings_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/copy_saved_objects_to_space_page.ts @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/39002 +/x-pack/test/functional/page_objects/api_keys_page.ts @elastic/kibana-security +/x-pack/test/functional/page_objects/account_settings_page.ts @elastic/kibana-security +/x-pack/test/functional/apps/user_profiles @elastic/kibana-security +/x-pack/test/common/services/spaces.ts @elastic/kibana-security +/x-pack/test/api_integration/config_security_*.ts @elastic/kibana-security +/x-pack/test/functional/apps/api_keys @elastic/kibana-security +/x-pack/test/ftr_apis/security_and_spaces @elastic/kibana-security +/test/server_integration/services/supertest.js @elastic/kibana-security @elastic/kibana-core +/test/server_integration/http/ssl @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/53810 +/test/server_integration/http/ssl_with_p12 @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846522206 +/test/server_integration/http/ssl_with_p12_intermediate @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846522206 + +/test/server_integration/config.base.js @elastic/kibana-security @elastic/kibana-core # Assigned per https://github.com/elastic/kibana/pull/199795#discussion_r1846510782 +/test/server_integration/__fixtures__ @elastic/kibana-security # Assigned per https://github.com/elastic/kibana/pull/53810 /.github/codeql @elastic/kibana-security /.github/workflows/codeql.yml @elastic/kibana-security /.github/workflows/codeql-stats.yml @elastic/kibana-security From 9ae013878904af7764c4d62af55c8bb63df623f7 Mon Sep 17 00:00:00 2001 From: Khristinin Nikita Date: Mon, 18 Nov 2024 17:44:49 +0100 Subject: [PATCH 44/50] Update roles in serverless for value lists (#200128) Update serverless role for value lists related to this [PR](https://github.com/elastic/elasticsearch-controller/pull/771) --- .../kibana_roles/project_controller_security_roles.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml index 2d80c9d398210..22b3fd31c423b 100644 --- a/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml +++ b/x-pack/test_serverless/shared/lib/security/kibana_roles/project_controller_security_roles.yml @@ -151,6 +151,8 @@ t1_analyst: - write - maintenance - names: + - .lists* + - .items* - apm-*-transaction* - traces-apm* - auditbeat-* @@ -275,6 +277,7 @@ t3_analyst: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* @@ -406,6 +409,7 @@ rule_author: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* @@ -475,6 +479,7 @@ soc_manager: privileges: - read - write + - view_index_metadata - names: - metrics-endpoint.metadata_current_* - .fleet-agents* From d413c24dbe2371f2a2de42b5612cae5896e899a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Mon, 18 Nov 2024 17:45:27 +0100 Subject: [PATCH 45/50] [Obs AI Assistant] Support querying `semantic_text` fields in search connectors (#200184) This adds support for querying search connectors that have `semantic_text` fields. --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../server/plugin.ts | 42 +-- .../server/service/index.ts | 17 +- .../service/knowledge_base_service/index.ts | 27 +- .../recall_from_connectors.ts | 139 --------- .../recall_from_search_connectors.ts | 272 ++++++++++++++++++ .../observability_ai_assistant/tsconfig.json | 1 + .../tests/knowledge_base/helpers.ts | 8 +- 7 files changed, 297 insertions(+), 209 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts create mode 100644 x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts index b2b5736fd1d6f..361d13e6d77f2 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/plugin.ts @@ -12,14 +12,13 @@ import { Plugin, PluginInitializerContext, } from '@kbn/core/server'; -import { mapValues, once } from 'lodash'; +import { mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { CONNECTOR_TOKEN_SAVED_OBJECT_TYPE, ACTION_SAVED_OBJECT_TYPE, ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, } from '@kbn/actions-plugin/server/constants/saved_objects'; -import { firstValueFrom } from 'rxjs'; import { KibanaFeatureScope } from '@kbn/features-plugin/common'; import { OBSERVABILITY_AI_ASSISTANT_FEATURE_ID } from '../common/feature'; import type { ObservabilityAIAssistantConfig } from './config'; @@ -114,47 +113,10 @@ export class ObservabilityAIAssistantPlugin }; }) as ObservabilityAIAssistantRouteHandlerResources['plugins']; - // Using once to make sure the same model ID is used during service init and Knowledge base setup - const getSearchConnectorModelId = once(async () => { - const defaultModelId = '.elser_model_2'; - const [_, pluginsStart] = await core.getStartServices(); - // Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats - const license = await firstValueFrom(pluginsStart.licensing.license$); - if (!license.hasAtLeast('enterprise')) { - return defaultModelId; - } - - try { - // Wait for the ML plugin's dependency on the internal saved objects client to be ready - const { ml } = await core.plugins.onSetup('ml'); - - if (!ml.found) { - throw new Error('Could not find ML plugin'); - } - - const elserModelDefinition = await ( - ml.contract as { - trainedModelsProvider: ( - request: {}, - soClient: {} - ) => { getELSER: () => Promise<{ model_id: string }> }; - } - ) - .trainedModelsProvider({} as any, {} as any) // request, savedObjectsClient (but we fake it to use the internal user) - .getELSER(); - - return elserModelDefinition.model_id; - } catch (error) { - this.logger.error(`Failed to resolve ELSER model definition: ${error}`); - return defaultModelId; - } - }); - const service = (this.service = new ObservabilityAIAssistantService({ logger: this.logger.get('service'), core, - getSearchConnectorModelId, - enableKnowledgeBase: this.config.enableKnowledgeBase, + config: this.config, })); registerMigrateKnowledgeBaseEntriesTask({ diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts index 6dcfbf1796501..9c26bebdd8388 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/index.ts @@ -20,6 +20,7 @@ import { conversationComponentTemplate } from './conversation_component_template import { kbComponentTemplate } from './kb_component_template'; import { KnowledgeBaseService } from './knowledge_base_service'; import type { RegistrationCallback, RespondFunctionResources } from './types'; +import { ObservabilityAIAssistantConfig } from '../config'; function getResourceName(resource: string) { return `.kibana-observability-ai-assistant-${resource}`; @@ -47,27 +48,23 @@ export const resourceNames = { export class ObservabilityAIAssistantService { private readonly core: CoreSetup; private readonly logger: Logger; - private readonly getSearchConnectorModelId: () => Promise; private kbService?: KnowledgeBaseService; - private enableKnowledgeBase: boolean; + private config: ObservabilityAIAssistantConfig; private readonly registrations: RegistrationCallback[] = []; constructor({ logger, core, - getSearchConnectorModelId, - enableKnowledgeBase, + config, }: { logger: Logger; core: CoreSetup; - getSearchConnectorModelId: () => Promise; - enableKnowledgeBase: boolean; + config: ObservabilityAIAssistantConfig; }) { this.core = core; this.logger = logger; - this.getSearchConnectorModelId = getSearchConnectorModelId; - this.enableKnowledgeBase = enableKnowledgeBase; + this.config = config; this.resetInit(); } @@ -166,12 +163,12 @@ export class ObservabilityAIAssistantService { }); this.kbService = new KnowledgeBaseService({ + core: this.core, logger: this.logger.get('kb'), + config: this.config, esClient: { asInternalUser, }, - getSearchConnectorModelId: this.getSearchConnectorModelId, - enabled: this.enableKnowledgeBase, }); this.logger.info('Successfully set up index assets'); diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts index 66a49cdc29bee..a98cf6f810f2c 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/index.ts @@ -6,7 +6,7 @@ */ import { serverUnavailable } from '@hapi/boom'; -import type { ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; +import type { CoreSetup, ElasticsearchClient, IUiSettingsClient } from '@kbn/core/server'; import type { Logger } from '@kbn/logging'; import { orderBy } from 'lodash'; import { encode } from 'gpt-tokenizer'; @@ -26,15 +26,17 @@ import { getInferenceEndpoint, isInferenceEndpointMissingOrUnavailable, } from '../inference_endpoint'; -import { recallFromConnectors } from './recall_from_connectors'; +import { recallFromSearchConnectors } from './recall_from_search_connectors'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; +import { ObservabilityAIAssistantConfig } from '../../config'; interface Dependencies { + core: CoreSetup; esClient: { asInternalUser: ElasticsearchClient; }; logger: Logger; - getSearchConnectorModelId: () => Promise; - enabled: boolean; + config: ObservabilityAIAssistantConfig; } export interface RecalledEntry { @@ -141,14 +143,13 @@ export class KnowledgeBaseService { esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; uiSettingsClient: IUiSettingsClient; }): Promise => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return []; } this.dependencies.logger.debug( () => `Recalling entries from KB for queries: "${JSON.stringify(queries)}"` ); - const modelId = await this.dependencies.getSearchConnectorModelId(); const [documentsFromKb, documentsFromConnectors] = await Promise.all([ this.recallFromKnowledgeBase({ @@ -162,11 +163,11 @@ export class KnowledgeBaseService { } throw error; }), - recallFromConnectors({ + recallFromSearchConnectors({ esClient, uiSettingsClient, queries, - modelId, + core: this.dependencies.core, logger: this.dependencies.logger, }).catch((error) => { this.dependencies.logger.debug('Error getting data from search indices'); @@ -214,7 +215,7 @@ export class KnowledgeBaseService { namespace: string, user?: { name: string } ): Promise> => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return []; } try { @@ -257,7 +258,7 @@ export class KnowledgeBaseService { sortBy?: string; sortDirection?: 'asc' | 'desc'; }): Promise<{ entries: KnowledgeBaseEntry[] }> => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return { entries: [] }; } try { @@ -330,7 +331,7 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }) => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return null; } const res = await this.dependencies.esClient.asInternalUser.search({ @@ -393,7 +394,7 @@ export class KnowledgeBaseService { user?: { name: string; id?: string }; namespace?: string; }): Promise => { - if (!this.dependencies.enabled) { + if (!this.dependencies.config.enableKnowledgeBase) { return; } @@ -448,7 +449,7 @@ export class KnowledgeBaseService { errorMessage = error.message; }); - const enabled = this.dependencies.enabled; + const enabled = this.dependencies.config.enableKnowledgeBase; if (!endpoint) { return { ready: false, enabled, errorMessage }; } diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts deleted file mode 100644 index 27c133e7b88d0..0000000000000 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_connectors.ts +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; -import { IUiSettingsClient } from '@kbn/core-ui-settings-server'; -import { isEmpty } from 'lodash'; -import type { Logger } from '@kbn/logging'; -import { RecalledEntry } from '.'; -import { aiAssistantSearchConnectorIndexPattern } from '../../../common'; - -export async function recallFromConnectors({ - queries, - esClient, - uiSettingsClient, - modelId, - logger, -}: { - queries: Array<{ text: string; boost?: number }>; - esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; - uiSettingsClient: IUiSettingsClient; - modelId: string; - logger: Logger; -}): Promise { - const ML_INFERENCE_PREFIX = 'ml.inference.'; - const connectorIndices = await getConnectorIndices(esClient, uiSettingsClient, logger); - logger.debug(`Found connector indices: ${connectorIndices}`); - - const fieldCaps = await esClient.asCurrentUser.fieldCaps({ - index: connectorIndices, - fields: `${ML_INFERENCE_PREFIX}*`, - allow_no_indices: true, - types: ['sparse_vector'], - filters: '-metadata,-parent', - }); - - const fieldsWithVectors = Object.keys(fieldCaps.fields).map((field) => - field.replace('_expanded.predicted_value', '').replace(ML_INFERENCE_PREFIX, '') - ); - - if (!fieldsWithVectors.length) { - return []; - } - - const esQueries = fieldsWithVectors.flatMap((field) => { - const vectorField = `${ML_INFERENCE_PREFIX}${field}_expanded.predicted_value`; - const modelField = `${ML_INFERENCE_PREFIX}${field}_expanded.model_id`; - - return queries.map(({ text, boost = 1 }) => { - return { - bool: { - should: [ - { - text_expansion: { - [vectorField]: { - model_text: text, - model_id: modelId, - boost, - }, - }, - }, - ], - filter: [ - { - term: { - [modelField]: modelId, - }, - }, - ], - }, - }; - }); - }); - - const response = await esClient.asCurrentUser.search({ - index: connectorIndices, - query: { - bool: { - should: esQueries, - }, - }, - size: 20, - _source: { - exclude: ['_*', 'ml*'], - }, - }); - - const results = response.hits.hits.map((hit) => ({ - text: JSON.stringify(hit._source), - score: hit._score!, - is_correction: false, - id: hit._id!, - })); - - return results; -} - -async function getConnectorIndices( - esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }, - uiSettingsClient: IUiSettingsClient, - logger: Logger -) { - // improve performance by running this in parallel with the `uiSettingsClient` request - const responsePromise = esClient.asInternalUser.transport - .request<{ - results?: Array<{ index_name: string }>; - }>({ - method: 'GET', - path: '_connector', - querystring: { - filter_path: 'results.index_name', - }, - }) - .catch((e) => { - logger.warn(`Failed to fetch connector indices due to ${e.message}`); - return { results: [] }; - }); - - const customSearchConnectorIndex = await uiSettingsClient.get( - aiAssistantSearchConnectorIndexPattern - ); - - if (customSearchConnectorIndex) { - return customSearchConnectorIndex.split(','); - } - - const response = await responsePromise; - const connectorIndices = response.results?.map((result) => result.index_name); - - // preserve backwards compatibility with 8.14 (may not be needed in the future) - if (isEmpty(connectorIndices)) { - return ['search-*']; - } - - return connectorIndices; -} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts new file mode 100644 index 0000000000000..5abd6d850a8f4 --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/server/service/knowledge_base_service/recall_from_search_connectors.ts @@ -0,0 +1,272 @@ +/* + * 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 { ElasticsearchClient } from '@kbn/core-elasticsearch-server'; +import { IUiSettingsClient } from '@kbn/core-ui-settings-server'; +import { isEmpty, orderBy, compact } from 'lodash'; +import type { Logger } from '@kbn/logging'; +import { CoreSetup } from '@kbn/core-lifecycle-server'; +import { firstValueFrom } from 'rxjs'; +import { RecalledEntry } from '.'; +import { aiAssistantSearchConnectorIndexPattern } from '../../../common'; +import { ObservabilityAIAssistantPluginStartDependencies } from '../../types'; + +export async function recallFromSearchConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; +}): Promise { + const connectorIndices = await getConnectorIndices(esClient, uiSettingsClient, logger); + logger.debug(`Found connector indices: ${connectorIndices}`); + + const [semanticTextConnectors, legacyConnectors] = await Promise.all([ + recallFromSemanticTextConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, + connectorIndices, + }), + + recallFromLegacyConnectors({ + queries, + esClient, + uiSettingsClient, + logger, + core, + connectorIndices, + }), + ]); + + return orderBy([...semanticTextConnectors, ...legacyConnectors], (entry) => entry.score, 'desc'); +} + +async function recallFromSemanticTextConnectors({ + queries, + esClient, + logger, + core, + connectorIndices, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; + connectorIndices: string[] | undefined; +}): Promise { + const fieldCaps = await esClient.asCurrentUser.fieldCaps({ + index: connectorIndices, + fields: `*`, + allow_no_indices: true, + types: ['semantic_text'], + filters: '-metadata,-parent', + }); + + const semanticTextFields = Object.keys(fieldCaps.fields); + if (!semanticTextFields.length) { + return []; + } + logger.debug(`Semantic text field for search connectors: ${semanticTextFields}`); + + const params = { + index: connectorIndices, + size: 20, + _source: { + excludes: semanticTextFields.map((field) => `${field}.inference`), + }, + query: { + bool: { + should: semanticTextFields.flatMap((field) => { + return queries.map(({ text, boost = 1 }) => ({ + bool: { filter: [{ semantic: { field, query: text, boost } }] }, + })); + }), + minimum_should_match: 1, + }, + }, + }; + + const response = await esClient.asCurrentUser.search(params); + + const results = response.hits.hits.map((hit) => ({ + text: JSON.stringify(hit._source), + score: hit._score!, + is_correction: false, + id: hit._id!, + })); + + return results; +} + +async function recallFromLegacyConnectors({ + queries, + esClient, + logger, + core, + connectorIndices, +}: { + queries: Array<{ text: string; boost?: number }>; + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }; + uiSettingsClient: IUiSettingsClient; + logger: Logger; + core: CoreSetup; + connectorIndices: string[] | undefined; +}): Promise { + const ML_INFERENCE_PREFIX = 'ml.inference.'; + + const modelIdPromise = getElserModelId(core, logger); // pre-fetch modelId in parallel with fieldCaps + const fieldCaps = await esClient.asCurrentUser.fieldCaps({ + index: connectorIndices, + fields: `${ML_INFERENCE_PREFIX}*`, + allow_no_indices: true, + types: ['sparse_vector'], + filters: '-metadata,-parent', + }); + + const fieldsWithVectors = Object.keys(fieldCaps.fields).map((field) => + field.replace('_expanded.predicted_value', '').replace(ML_INFERENCE_PREFIX, '') + ); + + if (!fieldsWithVectors.length) { + return []; + } + + const modelId = await modelIdPromise; + const esQueries = fieldsWithVectors.flatMap((field) => { + const vectorField = `${ML_INFERENCE_PREFIX}${field}_expanded.predicted_value`; + const modelField = `${ML_INFERENCE_PREFIX}${field}_expanded.model_id`; + + return queries.map(({ text, boost = 1 }) => { + return { + bool: { + should: [ + { + text_expansion: { + [vectorField]: { + model_text: text, + model_id: modelId, + boost, + }, + }, + }, + ], + filter: [ + { + term: { + [modelField]: modelId, + }, + }, + ], + }, + }; + }); + }); + + const response = await esClient.asCurrentUser.search({ + index: connectorIndices, + size: 20, + _source: { + exclude: ['_*', 'ml*'], + }, + query: { + bool: { + should: esQueries, + }, + }, + }); + + const results = response.hits.hits.map((hit) => ({ + text: JSON.stringify(hit._source), + score: hit._score!, + is_correction: false, + id: hit._id!, + })); + + return results; +} + +async function getConnectorIndices( + esClient: { asCurrentUser: ElasticsearchClient; asInternalUser: ElasticsearchClient }, + uiSettingsClient: IUiSettingsClient, + logger: Logger +) { + // improve performance by running this in parallel with the `uiSettingsClient` request + const responsePromise = esClient.asInternalUser.connector + .list({ filter_path: 'results.index_name' }) + .catch((e) => { + logger.warn(`Failed to fetch connector indices due to ${e.message}`); + return { results: [] }; + }); + + const customSearchConnectorIndex = await uiSettingsClient.get( + aiAssistantSearchConnectorIndexPattern + ); + + if (customSearchConnectorIndex) { + return customSearchConnectorIndex.split(','); + } + + const response = await responsePromise; + + const connectorIndices = compact(response.results?.map((result) => result.index_name)); + + // preserve backwards compatibility with 8.14 (may not be needed in the future) + if (isEmpty(connectorIndices)) { + return ['search-*']; + } + + return connectorIndices; +} + +async function getElserModelId( + core: CoreSetup, + logger: Logger +) { + const defaultModelId = '.elser_model_2'; + const [_, pluginsStart] = await core.getStartServices(); + + // Wait for the license to be available so the ML plugin's guards pass once we ask for ELSER stats + const license = await firstValueFrom(pluginsStart.licensing.license$); + if (!license.hasAtLeast('enterprise')) { + return defaultModelId; + } + + try { + // Wait for the ML plugin's dependency on the internal saved objects client to be ready + const { ml } = await core.plugins.onSetup('ml'); + + if (!ml.found) { + throw new Error('Could not find ML plugin'); + } + + const elserModelDefinition = await ( + ml.contract as { + trainedModelsProvider: ( + request: {}, + soClient: {} + ) => { getELSER: () => Promise<{ model_id: string }> }; + } + ) + .trainedModelsProvider({} as any, {} as any) // request, savedObjectsClient (but we fake it to use the internal user) + .getELSER(); + + return elserModelDefinition.model_id; + } catch (error) { + logger.error(`Failed to resolve ELSER model definition: ${error}`); + return defaultModelId; + } +} diff --git a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json index 750bf69477653..d5acd7a365b50 100644 --- a/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json +++ b/x-pack/plugins/observability_solution/observability_ai_assistant/tsconfig.json @@ -46,6 +46,7 @@ "@kbn/management-settings-ids", "@kbn/ai-assistant-common", "@kbn/inference-common", + "@kbn/core-lifecycle-server", ], "exclude": ["target/**/*"] } diff --git a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts index fa1f15ddca4cd..25bbeb183a3b6 100644 --- a/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts +++ b/x-pack/test/observability_ai_assistant_api_integration/tests/knowledge_base/helpers.ts @@ -63,11 +63,5 @@ export async function deleteInferenceEndpoint({ es: Client; name?: string; }) { - return es.transport.request({ - method: 'DELETE', - path: `_inference/sparse_embedding/${name}`, - querystring: { - force: true, - }, - }); + return es.inference.delete({ inference_id: name, force: true }); } From 4fd0b10d8292ef9b81670452d9776bc2387bcff1 Mon Sep 17 00:00:00 2001 From: Kyra Cho Date: Mon, 18 Nov 2024 08:46:33 -0800 Subject: [PATCH 46/50] [Lens] rewrite BucketNestingEditor tests to use react testing library (#199888) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Hi! This PR rewrites the `BucketNestingEditor` tests to use the react testing library. Fixes #199839 Screenshot 2024-11-12 at 3 35 06 PM ### Checklist - [n/a] [Flaky Test Runner](https://ci-stats.kibana.dev/trigger_flaky_test_runner/1) was used on any tests changed --------- Co-authored-by: Marta Bondyra <4283304+mbondyra@users.noreply.github.com> --- .../bucket_nesting_editor.test.tsx | 107 ++++++++---------- 1 file changed, 49 insertions(+), 58 deletions(-) diff --git a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx index 6c09849df04ac..0479162855659 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/dimension_panel/bucket_nesting_editor.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { mount } from 'enzyme'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import React from 'react'; import { BucketNestingEditor } from './bucket_nesting_editor'; import { GenericIndexPatternColumn } from '../form_based'; @@ -21,7 +22,7 @@ const getFieldByName = (name: string): IndexPatternField | undefined => fieldMap describe('BucketNestingEditor', () => { function mockCol(col: Partial = {}): GenericIndexPatternColumn { - const result = { + return { dataType: 'string', isBucketed: true, label: 'a', @@ -33,13 +34,11 @@ describe('BucketNestingEditor', () => { }, sourceField: 'a', ...col, - }; - - return result as GenericIndexPatternColumn; + } as GenericIndexPatternColumn; } it('should display the top level grouping when at the root', () => { - const component = mount( + render( { setColumns={jest.fn()} /> ); - const nestingSwitch = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); - expect(nestingSwitch.prop('checked')).toBeTruthy(); + const nestingSwitch = screen.getByTestId('indexPattern-nesting-switch'); + expect(nestingSwitch).toBeChecked(); }); it('should display the bottom level grouping when appropriate', () => { - const component = mount( + render( { setColumns={jest.fn()} /> ); - const nestingSwitch = component.find('[data-test-subj="indexPattern-nesting-switch"]').first(); - expect(nestingSwitch.prop('checked')).toBeFalsy(); + const nestingSwitch = screen.getByTestId('indexPattern-nesting-switch'); + expect(nestingSwitch).not.toBeChecked(); }); - it('should reorder the columns when toggled', () => { + it('should reorder the columns when toggled', async () => { const setColumns = jest.fn(); - const component = mount( + const { rerender } = render( { /> ); - component - .find('[data-test-subj="indexPattern-nesting-switch"] button') - .first() - .simulate('click'); - + await userEvent.click(screen.getByTestId('indexPattern-nesting-switch')); expect(setColumns).toHaveBeenCalledTimes(1); expect(setColumns).toHaveBeenCalledWith(['a', 'b', 'c']); - component.setProps({ - layer: { - columnOrder: ['a', 'b', 'c'], - columns: { - a: mockCol(), - b: mockCol(), - c: mockCol({ operationType: 'min', isBucketed: false }), - }, - indexPatternId: 'foo', - }, - }); - - component - .find('[data-test-subj="indexPattern-nesting-switch"] button') - .first() - .simulate('click'); + rerender( + + ); + await userEvent.click(screen.getByTestId('indexPattern-nesting-switch')); expect(setColumns).toHaveBeenCalledTimes(2); expect(setColumns).toHaveBeenLastCalledWith(['b', 'a', 'c']); }); it('should display nothing if there are no buckets', () => { - const component = mount( + const { container } = render( { /> ); - expect(component.children().length).toBe(0); + expect(container.firstChild).toBeNull(); }); it('should display nothing if there is one bucket', () => { - const component = mount( + const { container } = render( { /> ); - expect(component.children().length).toBe(0); + expect(container.firstChild).toBeNull(); }); it('should display a dropdown with the parent column selected if 3+ buckets', () => { - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"]').first(); - - expect(control.prop('value')).toEqual('c'); + const control = screen.getByTestId('indexPattern-nesting-select'); + expect((control as HTMLSelectElement).value).toEqual('c'); }); - it('should reorder the columns when a column is selected in the dropdown', () => { + it('should reorder the columns when a column is selected in the dropdown', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { - target: { value: 'b' }, - }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, 'b'); expect(setColumns).toHaveBeenCalledWith(['c', 'b', 'a']); }); - it('should move to root if the first dropdown item is selected', () => { + it('should move to root if the first dropdown item is selected', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { target: { value: '' } }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, ''); expect(setColumns).toHaveBeenCalledWith(['a', 'c', 'b']); }); - it('should allow the last bucket to be moved', () => { + it('should allow the last bucket to be moved', async () => { const setColumns = jest.fn(); - const component = mount( + render( { /> ); - const control = component.find('[data-test-subj="indexPattern-nesting-select"] select').first(); - control.simulate('change', { - target: { value: '' }, - }); + const control = screen.getByTestId('indexPattern-nesting-select'); + await userEvent.selectOptions(control, ''); expect(setColumns).toHaveBeenCalledWith(['b', 'c', 'a']); }); From 94d508af8743edbadf2f339c94c8ca883a1e9f5b Mon Sep 17 00:00:00 2001 From: ruby Date: Tue, 19 Nov 2024 01:14:32 +0800 Subject: [PATCH 47/50] [Console]: incorrect position of console tour #198389 (#198636) --- src/plugins/console/common/constants/index.ts | 2 +- .../console/common/constants/plugin.ts | 2 ++ .../components/console_tour_step.tsx | 25 ++++++++++++++++-- .../embeddable/console_resize_button.tsx | 17 ++++++++++++ .../apps/console/_onboarding_tour.ts | 26 ++++++++++++++++--- test/functional/page_objects/console_page.ts | 6 ++++- 6 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/plugins/console/common/constants/index.ts b/src/plugins/console/common/constants/index.ts index a00bcebcf38cc..b4d6a594241ce 100644 --- a/src/plugins/console/common/constants/index.ts +++ b/src/plugins/console/common/constants/index.ts @@ -7,7 +7,7 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { MAJOR_VERSION } from './plugin'; +export { MAJOR_VERSION, WELCOME_TOUR_DELAY } from './plugin'; export { API_BASE_PATH, KIBANA_API_PREFIX } from './api'; export { DEFAULT_VARIABLES } from './variables'; export { diff --git a/src/plugins/console/common/constants/plugin.ts b/src/plugins/console/common/constants/plugin.ts index 27ddb7d5dff1d..bb87e300c138d 100644 --- a/src/plugins/console/common/constants/plugin.ts +++ b/src/plugins/console/common/constants/plugin.ts @@ -8,3 +8,5 @@ */ export const MAJOR_VERSION = '8.0.0'; + +export const WELCOME_TOUR_DELAY = 250; diff --git a/src/plugins/console/public/application/components/console_tour_step.tsx b/src/plugins/console/public/application/components/console_tour_step.tsx index 578d590bfff4a..97e999b0090aa 100644 --- a/src/plugins/console/public/application/components/console_tour_step.tsx +++ b/src/plugins/console/public/application/components/console_tour_step.tsx @@ -7,8 +7,9 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { ReactNode, ReactElement } from 'react'; +import React, { ReactNode, ReactElement, useState, useEffect } from 'react'; import { EuiTourStep, PopoverAnchorPosition } from '@elastic/eui'; +import { WELCOME_TOUR_DELAY } from '../../../common/constants'; export interface ConsoleTourStepProps { step: number; @@ -44,11 +45,31 @@ export const ConsoleTourStep = ({ tourStepProps, children }: Props) => { css, } = tourStepProps; + const [popoverVisible, setPopoverVisible] = useState(false); + + useEffect(() => { + let timeoutId: any; + + if (isStepOpen) { + timeoutId = setTimeout(() => { + setPopoverVisible(true); + }, WELCOME_TOUR_DELAY); + } else { + setPopoverVisible(false); + } + + return () => { + if (timeoutId) { + clearTimeout(timeoutId); + } + }; + }, [isStepOpen]); + return ( { + const debouncedResize = debounce(() => { + window.dispatchEvent(new Event('resize')); + }, WELCOME_TOUR_DELAY); + + debouncedResize(); + + // Cleanup the debounce instance on unmount or dependency change + return () => { + debouncedResize.cancel(); + }; + }, [consoleHeight]); + useEffect(() => { function handleResize() { const newMaxConsoleHeight = getCurrentConsoleMaxSize(euiTheme); diff --git a/test/functional/apps/console/_onboarding_tour.ts b/test/functional/apps/console/_onboarding_tour.ts index 330498cb7b5ec..1fc47a70d14b0 100644 --- a/test/functional/apps/console/_onboarding_tour.ts +++ b/test/functional/apps/console/_onboarding_tour.ts @@ -10,6 +10,9 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; +// The euiTour shows with a small delay, so with 1s we should be safe +const DELAY_FOR = 1000; + export default function ({ getService, getPageObjects }: FtrProviderContext) { const log = getService('log'); const browser = getService('browser'); @@ -40,22 +43,30 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await isTourStepOpen('filesTourStep')).to.be(false); }; + const waitUntilFinishedLoading = async () => { + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.common.sleep(DELAY_FOR); + }; + it('displays all five steps in the tour', async () => { + const andWaitFor = DELAY_FOR; + await waitUntilFinishedLoading(); + log.debug('on Shell tour step'); expect(await isTourStepOpen('shellTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Editor tour step'); expect(await isTourStepOpen('editorTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on History tour step'); expect(await isTourStepOpen('historyTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Config tour step'); expect(await isTourStepOpen('configTourStep')).to.be(true); - await PageObjects.console.clickNextTourStep(); + await PageObjects.console.clickNextTourStep(andWaitFor); log.debug('on Files tour step'); expect(await isTourStepOpen('filesTourStep')).to.be(true); @@ -73,10 +84,14 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // Tour should reset after clearing local storage await browser.clearLocalStorage(); await browser.refresh(); + + await waitUntilFinishedLoading(); expect(await isTourStepOpen('shellTourStep')).to.be(true); }); it('skipping the tour hides the tour steps', async () => { + await waitUntilFinishedLoading(); + expect(await isTourStepOpen('shellTourStep')).to.be(true); expect(await testSubjects.exists('consoleSkipTourButton')).to.be(true); await PageObjects.console.clickSkipTour(); @@ -90,6 +105,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('allows re-running the tour', async () => { + await waitUntilFinishedLoading(); + await PageObjects.console.skipTourIfExists(); // Verify that tour is hiddern @@ -100,6 +117,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.console.clickRerunTour(); // Verify that first tour step is visible + await waitUntilFinishedLoading(); expect(await isTourStepOpen('shellTourStep')).to.be(true); }); }); diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index 87308d24fd8c4..a80f3426e256e 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -276,8 +276,12 @@ export class ConsolePageObject extends FtrService { await this.testSubjects.click('consoleSkipTourButton'); } - public async clickNextTourStep() { + public async clickNextTourStep(andWaitFor: number = 0) { await this.testSubjects.click('consoleNextTourStepButton'); + + if (andWaitFor) { + await this.common.sleep(andWaitFor); + } } public async clickCompleteTour() { From 40afce5b9a42a70d340643996e28bc48bd3da0ef Mon Sep 17 00:00:00 2001 From: Carlos Crespo Date: Mon, 18 Nov 2024 18:28:49 +0100 Subject: [PATCH 48/50] [APM] Migrate time_range_metadata test to deployment agnostic (#200146) ## Summary Closes [#198994](https://github.com/elastic/kibana/issues/198994) Part of https://github.com/elastic/kibana/issues/193245 This PR contains the changes to migrate `time_range_metadata` test folder to Deployment-agnostic testing strategy. ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` It's recommended to be run against [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [ ] ~(OPTIONAL, only if a test has been unskipped) Run flaky test suite~ - [x] local run for serverless - [x] local run for stateful - [x] MKI run for serverless --------- Co-authored-by: Elastic Machine Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../apis/observability/apm/index.ts | 1 + .../apm/time_range_metadata/index.ts | 15 + .../many_apm_server_versions.spec.ts | 276 +++++++++++++++++ .../time_range_metadata.spec.ts | 68 ++--- .../many_apm_server_versions.spec.ts | 278 ------------------ 5 files changed, 324 insertions(+), 314 deletions(-) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/time_range_metadata/time_range_metadata.spec.ts (94%) delete mode 100644 x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index dcbf8edc4a755..640bb90abe711 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -32,6 +32,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./service_maps')); loadTestFile(require.resolve('./inspect')); loadTestFile(require.resolve('./service_groups')); + loadTestFile(require.resolve('./time_range_metadata')); loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); loadTestFile(require.resolve('./span_links')); diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts new file mode 100644 index 0000000000000..4e3c25936a2db --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('time_range_metadata', () => { + loadTestFile(require.resolve('./many_apm_server_versions.spec.ts')); + loadTestFile(require.resolve('./time_range_metadata.spec.ts')); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts new file mode 100644 index 0000000000000..31012e6dd6d63 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/many_apm_server_versions.spec.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import expect from '@kbn/expect'; +import { apm, timerange } from '@kbn/apm-synthtrace-client'; +import moment from 'moment'; +import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import { + TRANSACTION_DURATION_HISTOGRAM, + TRANSACTION_DURATION_SUMMARY, +} from '@kbn/apm-plugin/common/es_fields/apm'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; +import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; +import { Readable } from 'stream'; +import type { ApmApiClient } from '../../../../services/apm_api'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + const es = getService('es'); + + const baseTime = new Date('2023-10-01T00:00:00.000Z').getTime(); + const startLegacy = moment(baseTime).add(0, 'minutes'); + const start = moment(baseTime).add(5, 'minutes'); + const endLegacy = moment(baseTime).add(10, 'minutes'); + const end = moment(baseTime).add(15, 'minutes'); + + describe('Time range metadata when there are multiple APM Server versions', () => { + describe('when ingesting traces from APM Server with different versions', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + + before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateTraceDataForService({ + serviceName: 'synth-java-legacy', + start: startLegacy, + end: endLegacy, + isLegacy: true, + synthtrace: apmSynthtraceEsClient, + }); + + await generateTraceDataForService({ + serviceName: 'synth-java', + start, + end, + isLegacy: false, + synthtrace: apmSynthtraceEsClient, + }); + }); + + after(() => { + return apmSynthtraceEsClient.clean(); + }); + + it('ingests transaction metrics with transaction.duration.summary', async () => { + const res = await es.search({ + index: 'metrics-apm*', + body: { + query: { + bool: { + filter: [ + { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, + { exists: { field: TRANSACTION_DURATION_SUMMARY } }, + ], + }, + }, + }, + }); + + // @ts-expect-error + expect(res.hits.total.value).to.be(20); + }); + + it('ingests transaction metrics without transaction.duration.summary', async () => { + const res = await es.search({ + index: 'metrics-apm*', + body: { + query: { + bool: { + filter: [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }], + must_not: [{ exists: { field: TRANSACTION_DURATION_SUMMARY } }], + }, + }, + }, + }); + + // @ts-expect-error + expect(res.hits.total.value).to.be(10); + }); + + it('has transaction.duration.summary field for every document type', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: endLegacy.toISOString(), + end: end.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const allHasSummaryField = response.body.sources + .filter( + (source) => + source.documentType !== ApmDocumentType.TransactionEvent && + source.rollupInterval !== RollupInterval.SixtyMinutes // there is not enough data for 60 minutes + ) + .every((source) => { + return source.hasDurationSummaryField; + }); + + expect(allHasSummaryField).to.eql(true); + }); + + it('does not support transaction.duration.summary when the field is not supported by all APM server versions', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: startLegacy.toISOString(), + end: endLegacy.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const allHasSummaryField = response.body.sources.every((source) => { + return source.hasDurationSummaryField; + }); + + expect(allHasSummaryField).to.eql(false); + }); + + it('does not support transaction.duration.summary for transactionMetric 1m when not all documents within the range support it ', async () => { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/time_range_metadata', + params: { + query: { + start: startLegacy.toISOString(), + end: end.toISOString(), + enableContinuousRollups: true, + enableServiceTransactionMetrics: true, + useSpanName: false, + kuery: '', + }, + }, + }); + + const hasDurationSummaryField = response.body.sources.find( + (source) => + source.documentType === ApmDocumentType.TransactionMetric && + source.rollupInterval === RollupInterval.OneMinute // there is not enough data for 60 minutes in the timerange defined for the tests + )?.hasDurationSummaryField; + + expect(hasDurationSummaryField).to.eql(false); + }); + + it('does not have latency data for synth-java-legacy', async () => { + const res = await getLatencyChartForService({ + serviceName: 'synth-java-legacy', + start, + end: endLegacy, + apmApiClient, + useDurationSummary: true, + }); + + expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ + null, + null, + null, + null, + null, + null, + ]); + }); + + it('has latency data for synth-java service', async () => { + const res = await getLatencyChartForService({ + serviceName: 'synth-java', + start, + end: endLegacy, + apmApiClient, + useDurationSummary: true, + }); + + expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ + 1000000, 1000000, 1000000, 1000000, 1000000, 1000000, + ]); + }); + }); + }); +} + +// This will retrieve latency data expecting the `transaction.duration.summary` field to be present +function getLatencyChartForService({ + serviceName, + start, + end, + apmApiClient, + useDurationSummary, +}: { + serviceName: string; + start: moment.Moment; + end: moment.Moment; + apmApiClient: ApmApiClient; + useDurationSummary: boolean; +}) { + return apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, + params: { + path: { serviceName }, + query: { + start: start.toISOString(), + end: end.toISOString(), + environment: 'production', + latencyAggregationType: LatencyAggregationType.avg, + transactionType: 'request', + kuery: '', + documentType: ApmDocumentType.TransactionMetric, + rollupInterval: RollupInterval.OneMinute, + bucketSizeInSeconds: 60, + useDurationSummary, + }, + }, + }); +} + +function generateTraceDataForService({ + serviceName, + start, + end, + isLegacy, + synthtrace, +}: { + serviceName: string; + start: moment.Moment; + end: moment.Moment; + isLegacy?: boolean; + synthtrace: ApmSynthtraceEsClient; +}) { + const instance = apm + .service({ + name: serviceName, + environment: 'production', + agentName: 'java', + }) + .instance(`instance`); + + const events = timerange(start, end) + .ratePerMinute(6) + .generator((timestamp) => + instance + .transaction({ transactionName: 'GET /order/{id}' }) + .timestamp(timestamp) + .duration(1000) + .success() + ); + + const apmPipeline = (base: Readable) => { + return synthtrace.getDefaultPipeline({ versionOverride: '8.5.0' })(base); + }; + + return synthtrace.index(events, isLegacy ? apmPipeline : undefined); +} diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts index 6ea90a1b8b1d2..7ec73a692f988 100644 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/time_range_metadata.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/time_range_metadata/time_range_metadata.spec.ts @@ -11,15 +11,14 @@ import { omit, sortBy } from 'lodash'; import moment, { Moment } from 'moment'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; import { Readable } from 'stream'; import { ToolingLog } from '@kbn/tooling-log'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const es = getService('es'); const log = getService('log'); @@ -55,29 +54,28 @@ export default function ApiTest({ getService }: FtrProviderContext) { }; } - registry.when('Time range metadata without data', { config: 'basic', archives: [] }, () => { - it('handles empty state', async () => { - const response = await getTimeRangeMedata({ - start, - end, - }); + describe('Time range metadata', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + describe('without data', () => { + it('handles empty state', async () => { + const response = await getTimeRangeMedata({ + start, + end, + }); - expect(response.isUsingServiceDestinationMetrics).to.eql(false); - expect(response.sources.filter((source) => source.hasDocs)).to.eql([ - { - documentType: ApmDocumentType.TransactionEvent, - rollupInterval: RollupInterval.None, - hasDocs: true, - hasDurationSummaryField: false, - }, - ]); + expect(response.isUsingServiceDestinationMetrics).to.eql(false); + expect(response.sources.filter((source) => source.hasDocs)).to.eql([ + { + documentType: ApmDocumentType.TransactionEvent, + rollupInterval: RollupInterval.None, + hasDocs: true, + hasDurationSummaryField: false, + }, + ]); + }); }); - }); - registry.when( - 'Time range metadata when generating data with multiple APM server versions', - { config: 'basic', archives: [] }, - () => { + describe('when generating data with multiple APM server versions', () => { describe('data loaded with and without summary field', () => { const withoutSummaryFieldStart = moment('2023-04-28T00:00:00.000Z'); const withoutSummaryFieldEnd = moment(withoutSummaryFieldStart).add(2, 'hours'); @@ -86,6 +84,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const withSummaryFieldEnd = moment(withSummaryFieldStart).add(2, 'hours'); before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await getTransactionEvents({ start: withoutSummaryFieldStart, end: withoutSummaryFieldEnd, @@ -259,15 +258,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); }); - } - ); - - registry.when( - 'Time range metadata when generating data', - { config: 'basic', archives: [] }, - () => { - before(() => { + }); + + describe('when generating data', () => { + before(async () => { const instance = apm.service('my-service', 'production', 'java').instance('instance'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); return apmSynthtraceEsClient.index( timerange(moment(start).subtract(1, 'day'), end) @@ -620,8 +616,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { ]); }); }); - } - ); + }); + }); } function getTransactionEvents({ diff --git a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts b/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts deleted file mode 100644 index 6031b7dd8de5b..0000000000000 --- a/x-pack/test/apm_api_integration/tests/time_range_metadata/many_apm_server_versions.spec.ts +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { apm, timerange } from '@kbn/apm-synthtrace-client'; -import moment from 'moment'; -import { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; -import { - TRANSACTION_DURATION_HISTOGRAM, - TRANSACTION_DURATION_SUMMARY, -} from '@kbn/apm-plugin/common/es_fields/apm'; -import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; -import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; -import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregation_types'; -import { Readable } from 'stream'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { ApmApiClient } from '../../common/config'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const synthtrace = getService('apmSynthtraceEsClient'); - const es = getService('es'); - - const baseTime = new Date('2023-10-01T00:00:00.000Z').getTime(); - const startLegacy = moment(baseTime).add(0, 'minutes'); - const start = moment(baseTime).add(5, 'minutes'); - const endLegacy = moment(baseTime).add(10, 'minutes'); - const end = moment(baseTime).add(15, 'minutes'); - - registry.when( - 'Time range metadata when there are multiple APM Server versions', - { config: 'basic', archives: [] }, - () => { - describe('when ingesting traces from APM Server with different versions', () => { - before(async () => { - await generateTraceDataForService({ - serviceName: 'synth-java-legacy', - start: startLegacy, - end: endLegacy, - isLegacy: true, - synthtrace, - }); - - await generateTraceDataForService({ - serviceName: 'synth-java', - start, - end, - isLegacy: false, - synthtrace, - }); - }); - - after(() => { - return synthtrace.clean(); - }); - - it('ingests transaction metrics with transaction.duration.summary', async () => { - const res = await es.search({ - index: 'metrics-apm*', - body: { - query: { - bool: { - filter: [ - { exists: { field: TRANSACTION_DURATION_HISTOGRAM } }, - { exists: { field: TRANSACTION_DURATION_SUMMARY } }, - ], - }, - }, - }, - }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(20); - }); - - it('ingests transaction metrics without transaction.duration.summary', async () => { - const res = await es.search({ - index: 'metrics-apm*', - body: { - query: { - bool: { - filter: [{ exists: { field: TRANSACTION_DURATION_HISTOGRAM } }], - must_not: [{ exists: { field: TRANSACTION_DURATION_SUMMARY } }], - }, - }, - }, - }); - - // @ts-expect-error - expect(res.hits.total.value).to.be(10); - }); - - it('has transaction.duration.summary field for every document type', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: endLegacy.toISOString(), - end: end.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const allHasSummaryField = response.body.sources - .filter( - (source) => - source.documentType !== ApmDocumentType.TransactionEvent && - source.rollupInterval !== RollupInterval.SixtyMinutes // there is not enough data for 60 minutes - ) - .every((source) => { - return source.hasDurationSummaryField; - }); - - expect(allHasSummaryField).to.eql(true); - }); - - it('does not support transaction.duration.summary when the field is not supported by all APM server versions', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: startLegacy.toISOString(), - end: endLegacy.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const allHasSummaryField = response.body.sources.every((source) => { - return source.hasDurationSummaryField; - }); - - expect(allHasSummaryField).to.eql(false); - }); - - it('does not support transaction.duration.summary for transactionMetric 1m when not all documents within the range support it ', async () => { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/time_range_metadata', - params: { - query: { - start: startLegacy.toISOString(), - end: end.toISOString(), - enableContinuousRollups: true, - enableServiceTransactionMetrics: true, - useSpanName: false, - kuery: '', - }, - }, - }); - - const hasDurationSummaryField = response.body.sources.find( - (source) => - source.documentType === ApmDocumentType.TransactionMetric && - source.rollupInterval === RollupInterval.OneMinute // there is not enough data for 60 minutes in the timerange defined for the tests - )?.hasDurationSummaryField; - - expect(hasDurationSummaryField).to.eql(false); - }); - - it('does not have latency data for synth-java-legacy', async () => { - const res = await getLatencyChartForService({ - serviceName: 'synth-java-legacy', - start, - end: endLegacy, - apmApiClient, - useDurationSummary: true, - }); - - expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ - null, - null, - null, - null, - null, - null, - ]); - }); - - it('has latency data for synth-java service', async () => { - const res = await getLatencyChartForService({ - serviceName: 'synth-java', - start, - end: endLegacy, - apmApiClient, - useDurationSummary: true, - }); - - expect(res.body.currentPeriod.latencyTimeseries.map(({ y }) => y)).to.eql([ - 1000000, 1000000, 1000000, 1000000, 1000000, 1000000, - ]); - }); - }); - } - ); -} - -// This will retrieve latency data expecting the `transaction.duration.summary` field to be present -function getLatencyChartForService({ - serviceName, - start, - end, - apmApiClient, - useDurationSummary, -}: { - serviceName: string; - start: moment.Moment; - end: moment.Moment; - apmApiClient: ApmApiClient; - useDurationSummary: boolean; -}) { - return apmApiClient.readUser({ - endpoint: `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, - params: { - path: { serviceName }, - query: { - start: start.toISOString(), - end: end.toISOString(), - environment: 'production', - latencyAggregationType: LatencyAggregationType.avg, - transactionType: 'request', - kuery: '', - documentType: ApmDocumentType.TransactionMetric, - rollupInterval: RollupInterval.OneMinute, - bucketSizeInSeconds: 60, - useDurationSummary, - }, - }, - }); -} - -function generateTraceDataForService({ - serviceName, - start, - end, - isLegacy, - synthtrace, -}: { - serviceName: string; - start: moment.Moment; - end: moment.Moment; - isLegacy?: boolean; - synthtrace: ApmSynthtraceEsClient; -}) { - const instance = apm - .service({ - name: serviceName, - environment: 'production', - agentName: 'java', - }) - .instance(`instance`); - - const events = timerange(start, end) - .ratePerMinute(6) - .generator((timestamp) => - instance - .transaction({ transactionName: 'GET /order/{id}' }) - .timestamp(timestamp) - .duration(1000) - .success() - ); - - const apmPipeline = (base: Readable) => { - return synthtrace.getDefaultPipeline({ versionOverride: '8.5.0' })(base); - }; - - return synthtrace.index(events, isLegacy ? apmPipeline : undefined); -} From a101cc586c393b8b072044f68c200eb23638b777 Mon Sep 17 00:00:00 2001 From: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com> Date: Mon, 18 Nov 2024 18:45:52 +0100 Subject: [PATCH 49/50] Migrate `/test/apm_api_integration/tests/suggestions` to be deployment agnostic api tests (#200556) closes #198992 closes #198993 part of https://github.com/elastic/kibana/issues/193245 ### How to test - Serverless ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/serverless/oblt.serverless.config.ts --grep="APM" ``` - Stateful ``` node scripts/functional_tests_server --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts node scripts/functional_test_runner --config x-pack/test/api_integration/deployment_agnostic/configs/stateful/oblt.stateful.config.ts --grep="APM" ``` - [MKI](https://github.com/crespocarlos/kibana/blob/main/x-pack/test_serverless/README.md#run-tests-on-mki) ### Checklist - [x] (OPTIONAL, only if a test has been unskipped) Run flaky test suite - [x] serverless - [x] stateful - [x] MKI --------- Co-authored-by: Sergi Romeu --- .../apis/observability/apm/index.ts | 2 ++ .../apm}/suggestions/generate_data.ts | 0 .../observability/apm/suggestions/index.ts | 14 ++++++++++++++ .../apm}/suggestions/suggestions.spec.ts | 17 ++++++++++------- .../apm}/throughput/dependencies_apis.spec.ts | 18 ++++++++++-------- .../observability/apm/throughput/index.ts | 16 ++++++++++++++++ .../apm}/throughput/service_apis.spec.ts | 18 ++++++++++-------- .../apm}/throughput/service_maps.spec.ts | 19 ++++++++++--------- 8 files changed, 72 insertions(+), 32 deletions(-) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/generate_data.ts (100%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/suggestions/suggestions.spec.ts (94%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/dependencies_apis.spec.ts (94%) create mode 100644 x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_apis.spec.ts (92%) rename x-pack/test/{apm_api_integration/tests => api_integration/deployment_agnostic/apis/observability/apm}/throughput/service_maps.spec.ts (90%) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index 640bb90abe711..ab7f9e5736392 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -36,5 +36,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./diagnostics')); loadTestFile(require.resolve('./service_nodes')); loadTestFile(require.resolve('./span_links')); + loadTestFile(require.resolve('./suggestions')); + loadTestFile(require.resolve('./throughput')); }); } diff --git a/x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts similarity index 100% rename from x-pack/test/apm_api_integration/tests/suggestions/generate_data.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/generate_data.ts diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts new file mode 100644 index 0000000000000..9b2563c093a9d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Suggestions', () => { + loadTestFile(require.resolve('./suggestions.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts index d4d1c3b141700..a6e9342885571 100644 --- a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/suggestions/suggestions.spec.ts @@ -11,7 +11,8 @@ import { TRANSACTION_TYPE, } from '@kbn/apm-plugin/common/es_fields/apm'; import expect from '@kbn/expect'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; import { generateData } from './generate_data'; const startNumber = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -20,14 +21,16 @@ const endNumber = new Date('2021-01-01T00:05:00.000Z').getTime() - 1; const start = new Date(startNumber).toISOString(); const end = new Date(endNumber).toISOString(); -export default function suggestionsTests({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function suggestionsTests({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + describe('suggestions when data is loaded', () => { + let apmSynthtraceEsClient: ApmSynthtraceEsClient; - // FLAKY: https://github.com/elastic/kibana/issues/177538 - registry.when('suggestions when data is loaded', { config: 'basic', archives: [] }, async () => { before(async () => { + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); + await generateData({ apmSynthtraceEsClient, start: startNumber, diff --git a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts similarity index 94% rename from x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts index fe591631fafe7..84d293f287b2f 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/dependencies_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/dependencies_apis.spec.ts @@ -8,13 +8,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { meanBy, sumBy } from 'lodash'; import { DependencyNode, ServiceNode } from '@kbn/apm-plugin/common/connections'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const start = new Date('2021-01-01T00:00:00.000Z').getTime(); const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; @@ -93,11 +93,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177536 - registry.when.skip('Dependencies throughput value', { config: 'basic', archives: [] }, () => { + describe('Dependencies throughput value', () => { describe('when data is loaded', () => { const GO_PROD_RATE = 75; const JAVA_PROD_RATE = 25; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: 'synth-go', environment: 'production', agentName: 'go' }) @@ -105,6 +106,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceJavaInstance = apm .service({ name: 'synth-java', environment: 'development', agentName: 'java' }) .instance('instance-c'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts new file mode 100644 index 0000000000000..e0176b18be783 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('Throughput', () => { + loadTestFile(require.resolve('./dependencies_apis.spec.ts')); + loadTestFile(require.resolve('./service_apis.spec.ts')); + loadTestFile(require.resolve('./service_maps.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts similarity index 92% rename from x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts index 9d69ce74bf0ea..429d29090a1d2 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_apis.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_apis.spec.ts @@ -11,13 +11,13 @@ import { apm, timerange } from '@kbn/apm-synthtrace-client'; import expect from '@kbn/expect'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; import { meanBy, sumBy } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -141,11 +141,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - // FLAKY: https://github.com/elastic/kibana/issues/177535 - registry.when('Services APIs', { config: 'basic', archives: [] }, () => { + describe('Services APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -153,6 +154,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) diff --git a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts similarity index 90% rename from x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts index 5ee475344e286..883e81ea24524 100644 --- a/x-pack/test/apm_api_integration/tests/throughput/service_maps.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/throughput/service_maps.spec.ts @@ -9,13 +9,13 @@ import expect from '@kbn/expect'; import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { ProcessorEvent } from '@kbn/observability-plugin/common'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { roundNumber } from '../../utils'; +import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; +import { roundNumber } from '../utils/common'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const apmSynthtraceEsClient = getService('apmSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2021-01-01T00:00:00.000Z').getTime(); @@ -83,10 +83,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { let throughputMetricValues: Awaited>; let throughputTransactionValues: Awaited>; - registry.when('Service Maps APIs', { config: 'trial', archives: [] }, () => { + describe('Service Maps APIs', () => { describe('when data is loaded ', () => { const GO_PROD_RATE = 80; const GO_DEV_RATE = 20; + let apmSynthtraceEsClient: ApmSynthtraceEsClient; + before(async () => { const serviceGoProdInstance = apm .service({ name: serviceName, environment: 'production', agentName: 'go' }) @@ -94,6 +96,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { const serviceGoDevInstance = apm .service({ name: serviceName, environment: 'development', agentName: 'go' }) .instance('instance-b'); + apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient(); await apmSynthtraceEsClient.index([ timerange(start, end) @@ -119,7 +122,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { after(() => apmSynthtraceEsClient.clean()); - // FLAKY: https://github.com/elastic/kibana/issues/176984 describe('compare throughput value between service inventory and service maps', () => { before(async () => { [throughputTransactionValues, throughputMetricValues] = await Promise.all([ @@ -136,7 +138,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); - // FLAKY: https://github.com/elastic/kibana/issues/176987 describe('when calling service maps transactions stats api', () => { let serviceMapsNodeThroughput: number | null | undefined; before(async () => { From 5c485fa728713f6e8fe03193c0bb6aa08be49f14 Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 18 Nov 2024 17:50:21 +0000 Subject: [PATCH 50/50] [Ownership] Assign test files to presentation team (#200209) ## Summary Assign test files to presentation team Contributes to: #192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .github/CODEOWNERS | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e51fceb9e2e0f..760a4e0ba446e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1368,7 +1368,23 @@ x-pack/test_serverless/**/test_suites/observability/ai_assistant @elastic/obs-ai ### END Observability Plugins # Presentation -/x-pack/test/disable_ems @elastic/kibana-presentation +/test/interpreter_functional/snapshots @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/54342 +/test/functional/services/inspector.ts @elastic/kibana-presentation +/x-pack/test/functional/services/canvas_element.ts @elastic/kibana-presentation +/x-pack/test/functional/page_objects/canvas_page.ts @elastic/kibana-presentation +/x-pack/test/accessibility/apps/group3/canvas.ts @elastic/kibana-presentation +/x-pack/test/upgrade/apps/canvas @elastic/kibana-presentation +/x-pack/test/upgrade/apps/dashboard @elastic/kibana-presentation +/test/functional/screenshots/baseline/tsvb_dashboard.png @elastic/kibana-presentation +/test/functional/screenshots/baseline/dashboard_*.png @elastic/kibana-presentation +/test/functional/screenshots/baseline/area_chart.png @elastic/kibana-presentation +/x-pack/test/disable_ems @elastic/kibana-presentation # Assigned per https://github.com/elastic/kibana/pull/165986 +/x-pack/test/functional/fixtures/kbn_archiver/dashboard* @elastic/kibana-presentation +/test/functional/page_objects/dashboard_page* @elastic/kibana-presentation +/test/functional/firefox/dashboard.config.ts @elastic/kibana-presentation # Assigned per: https://github.com/elastic/kibana/issues/15023 +/test/functional/fixtures/es_archiver/dashboard @elastic/kibana-presentation # Assigned per: https://github.com/elastic/kibana/issues/15023 +/test/accessibility/apps/dashboard.ts @elastic/kibana-presentation +/test/accessibility/apps/filter_panel.ts @elastic/kibana-presentation /x-pack/test/functional/apps/dashboard @elastic/kibana-presentation /x-pack/test/accessibility/apps/group3/maps.ts @elastic/kibana-presentation /x-pack/test/accessibility/apps/group1/dashboard_panel_options.ts @elastic/kibana-presentation