From 41c813bac438530a579b7896f6848dd56089145a Mon Sep 17 00:00:00 2001 From: Uladzislau Lasitsa Date: Mon, 11 Oct 2021 11:56:36 +0300 Subject: [PATCH 001/287] [Visualization] Get rid of saved object loader and use savedObjectClient resolve (#113121) * First step: create saved_visualize_utils, starting use new get/save methods * Use new util methods in embeddable * move findListItem in utils * some clean up * clean up * Some fixes * Fix saved object tags * Some types fixes * Fix unit tests * Clean up code * Add unit tests for new utils * Fix lint * Fix tagging * Add unit tests * Some fixes * Clean up code * Fix lint * Fix types * put new methods in start contract * Fix imports * Fix lint * Fix comments * Fix lint * Fix CI * use local url instead of full path * Fix unit test * Some clean up * Fix nits * fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/visualizations/kibana.json | 2 +- .../create_vis_embeddable_from_object.ts | 20 +- .../public/embeddable/visualize_embeddable.ts | 15 +- .../visualize_embeddable_factory.tsx | 63 ++- src/plugins/visualizations/public/index.ts | 2 + src/plugins/visualizations/public/mocks.ts | 7 + src/plugins/visualizations/public/plugin.ts | 51 +- .../public/saved_visualizations/_saved_vis.ts | 42 +- .../saved_visualizations.ts | 1 + src/plugins/visualizations/public/types.ts | 35 +- .../controls_references.ts | 0 .../saved_visualization_references/index.ts | 0 .../saved_visualization_references.test.ts | 0 .../saved_visualization_references.ts | 0 .../timeseries_references.ts | 0 .../utils/saved_visualize_utils.test.ts | 507 ++++++++++++++++++ .../public/utils/saved_visualize_utils.ts | 403 ++++++++++++++ src/plugins/visualizations/tsconfig.json | 1 + src/plugins/visualize/kibana.json | 3 +- .../visualize_editor_common.test.tsx | 112 ++++ .../components/visualize_editor_common.tsx | 56 +- .../components/visualize_listing.tsx | 9 +- .../visualize/public/application/types.ts | 3 +- .../application/utils/get_top_nav_config.tsx | 31 +- .../utils/get_visualization_instance.test.ts | 27 +- .../utils/get_visualization_instance.ts | 9 +- .../public/application/utils/mocks.ts | 1 - .../utils/use/use_saved_vis_instance.test.ts | 5 - .../utils/use/use_saved_vis_instance.ts | 3 - src/plugins/visualize/public/plugin.ts | 4 +- src/plugins/visualize/tsconfig.json | 3 +- 31 files changed, 1291 insertions(+), 124 deletions(-) rename src/plugins/visualizations/public/{saved_visualizations => utils}/saved_visualization_references/controls_references.ts (100%) rename src/plugins/visualizations/public/{saved_visualizations => utils}/saved_visualization_references/index.ts (100%) rename src/plugins/visualizations/public/{saved_visualizations => utils}/saved_visualization_references/saved_visualization_references.test.ts (100%) rename src/plugins/visualizations/public/{saved_visualizations => utils}/saved_visualization_references/saved_visualization_references.ts (100%) rename src/plugins/visualizations/public/{saved_visualizations => utils}/saved_visualization_references/timeseries_references.ts (100%) create mode 100644 src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts create mode 100644 src/plugins/visualizations/public/utils/saved_visualize_utils.ts create mode 100644 src/plugins/visualize/public/application/components/visualize_editor_common.test.tsx diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index 0afbec24c7c3f..32430c9d4e4fd 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -11,7 +11,7 @@ "inspector", "savedObjects" ], - "optionalPlugins": ["usageCollection"], + "optionalPlugins": ["usageCollection", "spaces", "savedObjectsTaggingOss"], "requiredBundles": ["kibanaUtils", "discover"], "extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"], "owner": { diff --git a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts index cfa871f17b0e0..c72b8618dc199 100644 --- a/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts +++ b/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts @@ -20,16 +20,10 @@ import { AttributeService, } from '../../../../plugins/embeddable/public'; import { DisabledLabEmbeddable } from './disabled_lab_embeddable'; -import { - getSavedVisualizationsLoader, - getUISettings, - getHttp, - getTimeFilter, - getCapabilities, -} from '../services'; +import { getUISettings, getHttp, getTimeFilter, getCapabilities } from '../services'; +import { urlFor } from '../utils/saved_visualize_utils'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; -import { SavedVisualizationsLoader } from '../saved_visualizations'; import { IndexPattern } from '../../../data/public'; import { createVisualizeEmbeddableAsync } from './visualize_embeddable_async'; @@ -38,7 +32,6 @@ export const createVisEmbeddableFromObject = async ( vis: Vis, input: Partial & { id: string }, - savedVisualizationsLoader?: SavedVisualizationsLoader, attributeService?: AttributeService< VisualizeSavedObjectAttributes, VisualizeByValueInput, @@ -46,16 +39,12 @@ export const createVisEmbeddableFromObject = >, parent?: IContainer ): Promise => { - const savedVisualizations = getSavedVisualizationsLoader(); - try { const visId = vis.id as string; - const editPath = visId ? savedVisualizations.urlFor(visId) : '#/edit_by_value'; + const editPath = visId ? urlFor(visId) : '#/edit_by_value'; - const editUrl = visId - ? getHttp().basePath.prepend(`/app/visualize${savedVisualizations.urlFor(visId)}`) - : ''; + const editUrl = visId ? getHttp().basePath.prepend(`/app/visualize${urlFor(visId)}`) : ''; const isLabsEnabled = getUISettings().get(VISUALIZE_ENABLE_LABS_SETTING); if (!isLabsEnabled && vis.type.stage === 'experimental') { @@ -87,7 +76,6 @@ export const createVisEmbeddableFromObject = }, input, attributeService, - savedVisualizationsLoader, parent ); } catch (e) { diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts index f5a7349b633eb..0c7d58453db69 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts @@ -39,7 +39,7 @@ import { getExpressions, getUiActions } from '../services'; import { VIS_EVENT_TO_TRIGGER } from './events'; import { VisualizeEmbeddableFactoryDeps } from './visualize_embeddable_factory'; import { SavedObjectAttributes } from '../../../../core/types'; -import { SavedVisualizationsLoader } from '../saved_visualizations'; +import { getSavedVisualization } from '../utils/saved_visualize_utils'; import { VisSavedObject } from '../types'; import { toExpressionAst } from './to_ast'; @@ -108,7 +108,6 @@ export class VisualizeEmbeddable VisualizeByValueInput, VisualizeByReferenceInput >; - private savedVisualizationsLoader?: SavedVisualizationsLoader; constructor( timefilter: TimefilterContract, @@ -119,7 +118,6 @@ export class VisualizeEmbeddable VisualizeByValueInput, VisualizeByReferenceInput >, - savedVisualizationsLoader?: SavedVisualizationsLoader, parent?: IContainer ) { super( @@ -144,7 +142,6 @@ export class VisualizeEmbeddable this.vis.uiState.on('change', this.uiStateChangeHandler); this.vis.uiState.on('reload', this.reload); this.attributeService = attributeService; - this.savedVisualizationsLoader = savedVisualizationsLoader; if (this.attributeService) { const isByValue = !this.inputIsRefType(initialInput); @@ -455,7 +452,15 @@ export class VisualizeEmbeddable }; getInputAsRefType = async (): Promise => { - const savedVis = await this.savedVisualizationsLoader?.get({}); + const { savedObjectsClient, data, spaces, savedObjectsTaggingOss } = await this.deps.start() + .plugins; + const savedVis = await getSavedVisualization({ + savedObjectsClient, + search: data.search, + dataViews: data.dataViews, + spaces, + savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), + }); if (!savedVis) { throw new Error('Error creating a saved vis object'); } diff --git a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx index 9e22b33bdee9d..9b1af5bea3fce 100644 --- a/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx +++ b/src/plugins/visualizations/public/embeddable/visualize_embeddable_factory.tsx @@ -33,20 +33,20 @@ import type { import { VISUALIZE_EMBEDDABLE_TYPE } from './constants'; import type { SerializedVis, Vis } from '../vis'; import { createVisAsync } from '../vis_async'; -import { - getCapabilities, - getTypes, - getUISettings, - getSavedVisualizationsLoader, -} from '../services'; +import { getCapabilities, getTypes, getUISettings } from '../services'; import { showNewVisModal } from '../wizard'; -import { convertToSerializedVis } from '../saved_visualizations/_saved_vis'; +import { + convertToSerializedVis, + getSavedVisualization, + saveVisualization, + getFullPath, +} from '../utils/saved_visualize_utils'; import { extractControlsReferences, extractTimeSeriesReferences, injectTimeSeriesReferences, injectControlsReferences, -} from '../saved_visualizations/saved_visualization_references'; +} from '../utils/saved_visualization_references'; import { createVisEmbeddableFromObject } from './create_vis_embeddable_from_object'; import { VISUALIZE_ENABLE_LABS_SETTING } from '../../common/constants'; import { checkForDuplicateTitle } from '../../../saved_objects/public'; @@ -59,7 +59,15 @@ interface VisualizationAttributes extends SavedObjectAttributes { export interface VisualizeEmbeddableFactoryDeps { start: StartServicesGetter< - Pick + Pick< + VisualizationsStartDeps, + | 'inspector' + | 'embeddable' + | 'savedObjectsClient' + | 'data' + | 'savedObjectsTaggingOss' + | 'spaces' + > >; } @@ -147,17 +155,36 @@ export class VisualizeEmbeddableFactory input: Partial & { id: string }, parent?: IContainer ): Promise { - const savedVisualizations = getSavedVisualizationsLoader(); + const startDeps = await this.deps.start(); try { - const savedObject = await savedVisualizations.get(savedObjectId); + const savedObject = await getSavedVisualization( + { + savedObjectsClient: startDeps.core.savedObjects.client, + search: startDeps.plugins.data.search, + dataViews: startDeps.plugins.data.dataViews, + spaces: startDeps.plugins.spaces, + savedObjectsTagging: startDeps.plugins.savedObjectsTaggingOss?.getTaggingApi(), + }, + savedObjectId + ); + + if (savedObject.sharingSavedObjectProps?.outcome === 'conflict') { + return new ErrorEmbeddable( + i18n.translate('visualizations.embeddable.legacyURLConflict.errorMessage', { + defaultMessage: `This visualization has the same URL as a legacy alias. Disable the alias to resolve this error : {json}`, + values: { json: savedObject.sharingSavedObjectProps?.errorJSON }, + }), + input, + parent + ); + } const visState = convertToSerializedVis(savedObject); const vis = await createVisAsync(savedObject.visState.type, visState); return createVisEmbeddableFromObject(this.deps)( vis, input, - savedVisualizations, await this.getAttributeService(), parent ); @@ -173,11 +200,9 @@ export class VisualizeEmbeddableFactory if (input.savedVis) { const visState = input.savedVis; const vis = await createVisAsync(visState.type, visState); - const savedVisualizations = getSavedVisualizationsLoader(); return createVisEmbeddableFromObject(this.deps)( vis, input, - savedVisualizations, await this.getAttributeService(), parent ); @@ -201,9 +226,9 @@ export class VisualizeEmbeddableFactory confirmOverwrite: false, returnToOrigin: true, isTitleDuplicateConfirmed: true, + copyOnSave: false, }; savedVis.title = title; - savedVis.copyOnSave = false; savedVis.description = ''; savedVis.searchSourceFields = visObj?.data.searchSource?.getSerializedFields(); const serializedVis = (visObj as unknown as Vis).serialize(); @@ -217,7 +242,12 @@ export class VisualizeEmbeddableFactory if (visObj) { savedVis.uiStateJSON = visObj?.uiState.toString(); } - const id = await savedVis.save(saveOptions); + const { core, plugins } = await this.deps.start(); + const id = await saveVisualization(savedVis, saveOptions, { + savedObjectsClient: core.savedObjects.client, + overlays: core.overlays, + savedObjectsTagging: plugins.savedObjectsTaggingOss?.getTaggingApi(), + }); if (!id || id === '') { throw new Error( i18n.translate('visualizations.savingVisualizationFailed.errorMsg', { @@ -225,6 +255,7 @@ export class VisualizeEmbeddableFactory }) ); } + core.chrome.recentlyAccessed.add(getFullPath(id), savedVis.title, String(id)); return { id }; } catch (error) { throw error; diff --git a/src/plugins/visualizations/public/index.ts b/src/plugins/visualizations/public/index.ts index 0886f230d101f..e6ea3cd489556 100644 --- a/src/plugins/visualizations/public/index.ts +++ b/src/plugins/visualizations/public/index.ts @@ -38,6 +38,7 @@ export { VisToExpressionAst, VisToExpressionAstParams, VisEditorOptionsProps, + GetVisOptions, } from './types'; export { VisualizationListItem, VisualizationStage } from './vis_types/vis_type_alias_registry'; export { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; @@ -49,3 +50,4 @@ export { FakeParams, HistogramParams, } from '../common/expression_functions/xy_dimension'; +export { urlFor, getFullPath } from './utils/saved_visualize_utils'; diff --git a/src/plugins/visualizations/public/mocks.ts b/src/plugins/visualizations/public/mocks.ts index 901593626a945..9b2d6bfe25b32 100644 --- a/src/plugins/visualizations/public/mocks.ts +++ b/src/plugins/visualizations/public/mocks.ts @@ -10,6 +10,7 @@ import { PluginInitializerContext } from '../../../core/public'; import { Schema, VisualizationsSetup, VisualizationsStart } from './'; import { Schemas } from './vis_types'; import { VisualizationsPlugin } from './plugin'; +import { spacesPluginMock } from '../../../../x-pack/plugins/spaces/public/mocks'; import { coreMock, applicationServiceMock } from '../../../core/public/mocks'; import { embeddablePluginMock } from '../../../plugins/embeddable/public/mocks'; import { expressionsPluginMock } from '../../../plugins/expressions/public/mocks'; @@ -18,6 +19,7 @@ import { usageCollectionPluginMock } from '../../../plugins/usage_collection/pub import { uiActionsPluginMock } from '../../../plugins/ui_actions/public/mocks'; import { inspectorPluginMock } from '../../../plugins/inspector/public/mocks'; import { savedObjectsPluginMock } from '../../../plugins/saved_objects/public/mocks'; +import { savedObjectTaggingOssPluginMock } from '../../saved_objects_tagging_oss/public/mocks'; const createSetupContract = (): VisualizationsSetup => ({ createBaseVisualization: jest.fn(), @@ -34,6 +36,9 @@ const createStartContract = (): VisualizationsStart => ({ savedVisualizationsLoader: { get: jest.fn(), } as any, + getSavedVisualization: jest.fn(), + saveVisualization: jest.fn(), + findListItems: jest.fn(), showNewVisModal: jest.fn(), createVis: jest.fn(), convertFromSerializedVis: jest.fn(), @@ -61,9 +66,11 @@ const createInstance = async () => { uiActions: uiActionsPluginMock.createStartContract(), application: applicationServiceMock.createStartContract(), embeddable: embeddablePluginMock.createStartContract(), + spaces: spacesPluginMock.createStartContract(), getAttributeService: jest.fn(), savedObjectsClient: coreMock.createStart().savedObjects.client, savedObjects: savedObjectsPluginMock.createStartContract(), + savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(), }); return { diff --git a/src/plugins/visualizations/public/plugin.ts b/src/plugins/visualizations/public/plugin.ts index ee3e914aa4bc6..47f544ce2f5d3 100644 --- a/src/plugins/visualizations/public/plugin.ts +++ b/src/plugins/visualizations/public/plugin.ts @@ -5,6 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ + +import type { SavedObjectsFindOptionsReference } from 'kibana/public'; import { setUISettings, setTypes, @@ -30,6 +32,7 @@ import { VisualizeEmbeddableFactory, createVisEmbeddableFromObject, } from './embeddable'; +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { TypesService } from './vis_types/types_service'; import { range as rangeExpressionFunction } from '../common/expression_functions/range'; import { visDimension as visDimensionExpressionFunction } from '../common/expression_functions/vis_dimension'; @@ -43,7 +46,10 @@ import { showNewVisModal } from './wizard'; import { convertFromSerializedVis, convertToSerializedVis, -} from './saved_visualizations/_saved_vis'; + getSavedVisualization, + saveVisualization, + findListItems, +} from './utils/saved_visualize_utils'; import { createSavedSearchesLoader } from '../../discover/public'; @@ -66,7 +72,9 @@ import type { import type { DataPublicPluginSetup, DataPublicPluginStart } from '../../../plugins/data/public'; import type { ExpressionsSetup, ExpressionsStart } from '../../expressions/public'; import type { EmbeddableSetup, EmbeddableStart } from '../../embeddable/public'; +import type { SavedObjectTaggingOssPluginStart } from '../../saved_objects_tagging_oss/public'; import { createVisAsync } from './vis_async'; +import type { VisSavedObject, SaveVisOptions, GetVisOptions } from './types'; /** * Interface for this plugin's returned setup/start contracts. @@ -82,6 +90,13 @@ export interface VisualizationsStart extends TypesStart { convertToSerializedVis: typeof convertToSerializedVis; convertFromSerializedVis: typeof convertFromSerializedVis; showNewVisModal: typeof showNewVisModal; + getSavedVisualization: (opts?: GetVisOptions | string) => Promise; + saveVisualization: (savedVis: VisSavedObject, saveOptions: SaveVisOptions) => Promise; + findListItems: ( + searchTerm: string, + listingLimit: number, + references?: SavedObjectsFindOptionsReference[] + ) => Promise<{ hits: Array>; total: number }>; __LEGACY: { createVisEmbeddableFromObject: ReturnType }; } @@ -103,6 +118,8 @@ export interface VisualizationsStartDeps { getAttributeService: EmbeddableStart['getAttributeService']; savedObjects: SavedObjectsStart; savedObjectsClient: SavedObjectsClientContract; + spaces?: SpacesPluginStart; + savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; } /** @@ -149,7 +166,15 @@ export class VisualizationsPlugin public start( core: CoreStart, - { data, expressions, uiActions, embeddable, savedObjects }: VisualizationsStartDeps + { + data, + expressions, + uiActions, + embeddable, + savedObjects, + spaces, + savedObjectsTaggingOss, + }: VisualizationsStartDeps ): VisualizationsStart { const types = this.types.start(); setTypes(types); @@ -181,6 +206,28 @@ export class VisualizationsPlugin return { ...types, showNewVisModal, + getSavedVisualization: async (opts) => { + return getSavedVisualization( + { + search: data.search, + savedObjectsClient: core.savedObjects.client, + dataViews: data.dataViews, + spaces, + savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), + }, + opts + ); + }, + saveVisualization: async (savedVis, saveOptions) => { + return saveVisualization(savedVis, saveOptions, { + savedObjectsClient: core.savedObjects.client, + overlays: core.overlays, + savedObjectsTagging: savedObjectsTaggingOss?.getTaggingApi(), + }); + }, + findListItems: async (searchTerm, listingLimit, references) => { + return findListItems(core.savedObjects.client, types, searchTerm, listingLimit, references); + }, /** * creates new instance of Vis * @param {IndexPattern} indexPattern - index pattern to use diff --git a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts index fb6c99ac8ef02..fbd8e414c2738 100644 --- a/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts +++ b/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts @@ -16,11 +16,11 @@ import type { SavedObjectsStart, SavedObject } from '../../../../plugins/saved_objects/public'; // @ts-ignore import { updateOldState } from '../legacy/vis_update_state'; -import { extractReferences, injectReferences } from './saved_visualization_references'; +import { extractReferences, injectReferences } from '../utils/saved_visualization_references'; import { createSavedSearchesLoader } from '../../../discover/public'; import type { SavedObjectsClientContract } from '../../../../core/public'; import type { IndexPatternsContract } from '../../../../plugins/data/public'; -import type { ISavedVis, SerializedVis } from '../types'; +import type { ISavedVis } from '../types'; export interface SavedVisServices { savedObjectsClient: SavedObjectsClientContract; @@ -28,43 +28,7 @@ export interface SavedVisServices { indexPatterns: IndexPatternsContract; } -export const convertToSerializedVis = (savedVis: ISavedVis): SerializedVis => { - const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; - - const aggs = searchSourceFields && searchSourceFields.index ? visState.aggs || [] : visState.aggs; - - return { - id, - title, - type: visState.type, - description, - params: visState.params, - uiState: JSON.parse(uiStateJSON || '{}'), - data: { - aggs, - searchSource: searchSourceFields!, - savedSearchId: savedVis.savedSearchId, - }, - }; -}; - -export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { - return { - id: vis.id, - title: vis.title, - description: vis.description, - visState: { - title: vis.title, - type: vis.type, - aggs: vis.data.aggs, - params: vis.params, - }, - uiStateJSON: JSON.stringify(vis.uiState), - searchSourceFields: vis.data.searchSource, - savedSearchId: vis.data.savedSearchId, - }; -}; - +/** @deprecated **/ export function createSavedVisClass(services: SavedVisServices) { const savedSearch = createSavedSearchesLoader(services); diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts index d07d28b393dcc..cec65b8f988b3 100644 --- a/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts +++ b/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts @@ -22,6 +22,7 @@ export interface FindListItemsOptions { references?: SavedObjectsFindOptionsReference[]; } +/** @deprecated **/ export function createSavedVisLoader(services: SavedVisServicesWithVisualizations) { const { savedObjectsClient, visualizationTypes } = services; diff --git a/src/plugins/visualizations/public/types.ts b/src/plugins/visualizations/public/types.ts index d68599c0724f6..5be8f49e9cdc7 100644 --- a/src/plugins/visualizations/public/types.ts +++ b/src/plugins/visualizations/public/types.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import { SavedObject } from '../../../plugins/saved_objects/public'; +import type { SavedObjectsMigrationVersion } from 'kibana/public'; import { IAggConfigs, SearchSourceFields, TimefilterContract, AggConfigSerialized, } from '../../../plugins/data/public'; +import type { ISearchSource } from '../../data/common'; import { ExpressionAstExpression } from '../../expressions/public'; import type { SerializedVis, Vis } from './vis'; @@ -36,9 +37,39 @@ export interface ISavedVis { uiStateJSON?: string; savedSearchRefName?: string; savedSearchId?: string; + sharingSavedObjectProps?: { + outcome?: 'aliasMatch' | 'exactMatch' | 'conflict'; + aliasTargetId?: string; + errorJSON?: string; + }; } -export interface VisSavedObject extends SavedObject, ISavedVis {} +export interface VisSavedObject extends ISavedVis { + lastSavedTitle: string; + getEsType: () => string; + getDisplayName?: () => string; + displayName: string; + migrationVersion?: SavedObjectsMigrationVersion; + searchSource?: ISearchSource; + version?: string; + tags?: string[]; +} + +export interface SaveVisOptions { + confirmOverwrite?: boolean; + isTitleDuplicateConfirmed?: boolean; + onTitleDuplicate?: () => void; + copyOnSave?: boolean; +} + +export interface GetVisOptions { + id?: string; + searchSource?: boolean; + migrationVersion?: SavedObjectsMigrationVersion; + savedSearchId?: string; + type?: string; + indexPattern?: string; +} export interface VisToExpressionAstParams { timefilter: TimefilterContract; diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts similarity index 100% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts rename to src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/index.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/index.ts similarity index 100% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/index.ts rename to src/plugins/visualizations/public/utils/saved_visualization_references/index.ts diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.test.ts similarity index 100% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.test.ts rename to src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.test.ts diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts similarity index 100% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/saved_visualization_references.ts rename to src/plugins/visualizations/public/utils/saved_visualization_references/saved_visualization_references.ts diff --git a/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts b/src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts similarity index 100% rename from src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts rename to src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts new file mode 100644 index 0000000000000..83b16026de391 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.test.ts @@ -0,0 +1,507 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { ISearchSource } from '../../../data/common'; +import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; +import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; +import { coreMock } from '../../../../core/public/mocks'; +import { dataPluginMock } from '../../../data/public/mocks'; +import { SavedObjectsClientContract } from '../../../../core/public'; +import { + findListItems, + getSavedVisualization, + saveVisualization, + SAVED_VIS_TYPE, +} from './saved_visualize_utils'; +import { VisTypeAlias, TypesStart } from '../vis_types'; +import type { VisSavedObject } from '../types'; + +let visTypes = [] as VisTypeAlias[]; +const mockGetAliases = jest.fn(() => visTypes); +const mockGetTypes = jest.fn((type: string) => type) as unknown as TypesStart['get']; +jest.mock('../services', () => ({ + getSpaces: jest.fn(() => ({ + getActiveSpace: () => ({ + id: 'test', + }), + })), +})); + +const mockParseSearchSourceJSON = jest.fn(); +const mockInjectSearchSourceReferences = jest.fn(); +const mockExtractSearchSourceReferences = jest.fn((...args) => [{}, []]); + +jest.mock('../../../../plugins/data/public', () => ({ + extractSearchSourceReferences: jest.fn((...args) => mockExtractSearchSourceReferences(...args)), + injectSearchSourceReferences: jest.fn((...args) => mockInjectSearchSourceReferences(...args)), + parseSearchSourceJSON: jest.fn((...args) => mockParseSearchSourceJSON(...args)), +})); + +const mockInjectReferences = jest.fn(); +const mockExtractReferences = jest.fn(() => ({ references: [], attributes: {} })); +jest.mock('./saved_visualization_references', () => ({ + injectReferences: jest.fn((...args) => mockInjectReferences(...args)), + extractReferences: jest.fn(() => mockExtractReferences()), +})); + +let isTitleDuplicateConfirmed = true; +const mockCheckForDuplicateTitle = jest.fn(() => { + if (!isTitleDuplicateConfirmed) { + throw new Error(); + } +}); +const mockSaveWithConfirmation = jest.fn(() => ({ id: 'test-after-confirm' })); +jest.mock('../../../../plugins/saved_objects/public', () => ({ + checkForDuplicateTitle: jest.fn(() => mockCheckForDuplicateTitle()), + saveWithConfirmation: jest.fn(() => mockSaveWithConfirmation()), + isErrorNonFatal: jest.fn(() => true), +})); + +describe('saved_visualize_utils', () => { + const { overlays, savedObjects } = coreMock.createStart(); + const savedObjectsClient = savedObjects.client as jest.Mocked; + (savedObjectsClient.resolve as jest.Mock).mockImplementation(() => ({ + saved_object: { + references: [ + { + id: 'test', + type: 'index-pattern', + }, + ], + attributes: { + visState: JSON.stringify({ type: 'area' }), + kibanaSavedObjectMeta: { + searchSourceJSON: '{filter: []}', + }, + }, + _version: '1', + }, + outcome: 'exact', + alias_target_id: null, + })); + (savedObjectsClient.create as jest.Mock).mockImplementation(() => ({ id: 'test' })); + const { dataViews, search } = dataPluginMock.createStartContract(); + + describe('getSavedVisualization', () => { + beforeEach(() => { + mockParseSearchSourceJSON.mockClear(); + mockInjectSearchSourceReferences.mockClear(); + mockInjectReferences.mockClear(); + }); + it('should return object with defaults if was not provided id', async () => { + const savedVis = await getSavedVisualization({ + savedObjectsClient, + search, + dataViews, + spaces: Promise.resolve({ + getActiveSpace: () => ({ + id: 'test', + }), + }) as unknown as SpacesPluginStart, + }); + expect(savedVis).toBeDefined(); + expect(savedVis.title).toBe(''); + expect(savedVis.displayName).toBe(SAVED_VIS_TYPE); + }); + + it('should create search source if saved object has searchSourceJSON', async () => { + await getSavedVisualization( + { + savedObjectsClient, + search, + dataViews, + spaces: Promise.resolve({ + getActiveSpace: () => ({ + id: 'test', + }), + }) as unknown as SpacesPluginStart, + }, + { id: 'test', searchSource: true } + ); + expect(mockParseSearchSourceJSON).toHaveBeenCalledWith('{filter: []}'); + expect(mockInjectSearchSourceReferences).toHaveBeenCalled(); + expect(search.searchSource.create).toHaveBeenCalled(); + }); + + it('should inject references if saved object has references', async () => { + await getSavedVisualization( + { + savedObjectsClient, + search, + dataViews, + spaces: Promise.resolve({ + getActiveSpace: () => ({ + id: 'test', + }), + }) as unknown as SpacesPluginStart, + }, + { id: 'test', searchSource: true } + ); + expect(mockInjectReferences.mock.calls[0][1]).toEqual([ + { + id: 'test', + type: 'index-pattern', + }, + ]); + }); + + it('should call getTagIdsFromReferences if we provide savedObjectsTagging service', async () => { + const mockGetTagIdsFromReferences = jest.fn(() => ['test']); + await getSavedVisualization( + { + savedObjectsClient, + search, + dataViews, + spaces: Promise.resolve({ + getActiveSpace: () => ({ + id: 'test', + }), + }) as unknown as SpacesPluginStart, + savedObjectsTagging: { + ui: { + getTagIdsFromReferences: mockGetTagIdsFromReferences, + }, + } as unknown as SavedObjectsTaggingApi, + }, + { id: 'test', searchSource: true } + ); + expect(mockGetTagIdsFromReferences).toHaveBeenCalled(); + }); + }); + + describe('saveVisualization', () => { + let vis: VisSavedObject; + beforeEach(() => { + mockExtractSearchSourceReferences.mockClear(); + mockExtractReferences.mockClear(); + mockSaveWithConfirmation.mockClear(); + savedObjectsClient.create.mockClear(); + vis = { + visState: { + type: 'area', + }, + title: 'test', + uiStateJSON: '{}', + version: '1', + __tags: [], + lastSavedTitle: 'test', + displayName: 'test', + getEsType: () => 'vis', + } as unknown as VisSavedObject; + }); + + it('should return id after save', async () => { + const savedVisId = await saveVisualization(vis, {}, { savedObjectsClient, overlays }); + expect(savedObjectsClient.create).toHaveBeenCalled(); + expect(mockExtractReferences).toHaveBeenCalled(); + expect(savedVisId).toBe('test'); + }); + + it('should call extractSearchSourceReferences if we new vis has searchSourceFields', async () => { + vis.searchSourceFields = { fields: [] }; + await saveVisualization(vis, {}, { savedObjectsClient, overlays }); + expect(mockExtractSearchSourceReferences).toHaveBeenCalledWith(vis.searchSourceFields); + }); + + it('should serialize searchSource', async () => { + vis.searchSource = { + serialize: jest.fn(() => ({ searchSourceJSON: '{}', references: [] })), + } as unknown as ISearchSource; + await saveVisualization(vis, {}, { savedObjectsClient, overlays }); + expect(vis.searchSource?.serialize).toHaveBeenCalled(); + }); + + it('should call updateTagsReferences if we provide savedObjectsTagging service', async () => { + const mockUpdateTagsReferences = jest.fn(() => []); + await saveVisualization( + vis, + {}, + { + savedObjectsClient, + overlays, + savedObjectsTagging: { + ui: { + updateTagsReferences: mockUpdateTagsReferences, + }, + } as unknown as SavedObjectsTaggingApi, + } + ); + expect(mockUpdateTagsReferences).toHaveBeenCalled(); + }); + + describe('confirmOverwrite', () => { + it('as false we should not call saveWithConfirmation and just do create', async () => { + const savedVisId = await saveVisualization( + vis, + { confirmOverwrite: false }, + { savedObjectsClient, overlays } + ); + expect(savedObjectsClient.create).toHaveBeenCalled(); + expect(mockExtractReferences).toHaveBeenCalled(); + expect(mockSaveWithConfirmation).not.toHaveBeenCalled(); + expect(savedVisId).toBe('test'); + }); + + it('as true we should call saveWithConfirmation', async () => { + const savedVisId = await saveVisualization( + vis, + { confirmOverwrite: true }, + { savedObjectsClient, overlays } + ); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(mockSaveWithConfirmation).toHaveBeenCalled(); + expect(savedVisId).toBe('test-after-confirm'); + }); + }); + + describe('isTitleDuplicateConfirmed', () => { + it('as false we should not save vis with duplicated title', async () => { + isTitleDuplicateConfirmed = false; + const savedVisId = await saveVisualization( + vis, + { isTitleDuplicateConfirmed }, + { savedObjectsClient, overlays } + ); + expect(savedObjectsClient.create).not.toHaveBeenCalled(); + expect(mockSaveWithConfirmation).not.toHaveBeenCalled(); + expect(mockCheckForDuplicateTitle).toHaveBeenCalled(); + expect(savedVisId).toBe(''); + expect(vis.id).toBeUndefined(); + }); + + it('as true we should save vis with duplicated title', async () => { + isTitleDuplicateConfirmed = true; + const savedVisId = await saveVisualization( + vis, + { isTitleDuplicateConfirmed }, + { savedObjectsClient, overlays } + ); + expect(mockCheckForDuplicateTitle).toHaveBeenCalled(); + expect(savedObjectsClient.create).toHaveBeenCalled(); + expect(savedVisId).toBe('test'); + expect(vis.id).toBe('test'); + }); + }); + }); + + describe('findListItems', () => { + function testProps() { + (savedObjectsClient.find as jest.Mock).mockImplementation(() => ({ + total: 0, + savedObjects: [], + })); + return { + savedObjectsClient, + search: '', + size: 10, + }; + } + + beforeEach(() => { + savedObjectsClient.find.mockClear(); + }); + + it('searches visualization title and description', async () => { + const props = testProps(); + const { find } = props.savedObjectsClient; + await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size + ); + expect(find.mock.calls).toMatchObject([ + [ + { + type: ['visualization'], + searchFields: ['title^3', 'description'], + }, + ], + ]); + }); + + it('searches searchFields and types specified by app extensions', async () => { + const props = testProps(); + visTypes = [ + { + appExtensions: { + visualizations: { + docTypes: ['bazdoc', 'etc'], + searchFields: ['baz', 'bing'], + }, + }, + } as VisTypeAlias, + ]; + const { find } = props.savedObjectsClient; + await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size + ); + expect(find.mock.calls).toMatchObject([ + [ + { + type: ['bazdoc', 'etc', 'visualization'], + searchFields: ['baz', 'bing', 'title^3', 'description'], + }, + ], + ]); + }); + + it('deduplicates types and search fields', async () => { + const props = testProps(); + visTypes = [ + { + appExtensions: { + visualizations: { + docTypes: ['bazdoc', 'bar'], + searchFields: ['baz', 'bing', 'barfield'], + }, + }, + } as VisTypeAlias, + { + appExtensions: { + visualizations: { + docTypes: ['visualization', 'foo', 'bazdoc'], + searchFields: ['baz', 'bing', 'foofield'], + }, + }, + } as VisTypeAlias, + ]; + const { find } = props.savedObjectsClient; + await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size + ); + expect(find.mock.calls).toMatchObject([ + [ + { + type: ['bazdoc', 'bar', 'visualization', 'foo'], + searchFields: ['baz', 'bing', 'barfield', 'foofield', 'title^3', 'description'], + }, + ], + ]); + }); + + it('searches the search term prefix', async () => { + const props = { + ...testProps(), + search: 'ahoythere', + }; + const { find } = props.savedObjectsClient; + await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size + ); + expect(find.mock.calls).toMatchObject([ + [ + { + search: 'ahoythere*', + }, + ], + ]); + }); + + it('searches with references', async () => { + const props = { + ...testProps(), + references: [ + { type: 'foo', id: 'hello' }, + { type: 'bar', id: 'dolly' }, + ], + }; + const { find } = props.savedObjectsClient; + await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size, + props.references + ); + expect(find.mock.calls).toMatchObject([ + [ + { + hasReference: [ + { type: 'foo', id: 'hello' }, + { type: 'bar', id: 'dolly' }, + ], + }, + ], + ]); + }); + + it('uses type-specific toListItem function, if available', async () => { + const props = testProps(); + + visTypes = [ + { + appExtensions: { + visualizations: { + docTypes: ['wizard'], + toListItem(savedObject) { + return { + id: savedObject.id, + title: `${(savedObject.attributes as { label: string }).label} THE GRAY`, + }; + }, + }, + }, + } as VisTypeAlias, + ]; + (props.savedObjectsClient.find as jest.Mock).mockImplementationOnce(async () => ({ + total: 2, + savedObjects: [ + { + id: 'lotr', + type: 'wizard', + attributes: { label: 'Gandalf' }, + }, + { + id: 'wat', + type: 'visualization', + attributes: { title: 'WATEVER', typeName: 'test' }, + }, + ], + })); + + const items = await findListItems( + props.savedObjectsClient, + { get: mockGetTypes, getAliases: mockGetAliases }, + props.search, + props.size + ); + expect(items).toEqual({ + total: 2, + hits: [ + { + id: 'lotr', + references: undefined, + title: 'Gandalf THE GRAY', + }, + { + id: 'wat', + references: undefined, + icon: undefined, + savedObjectType: 'visualization', + editUrl: '/edit/wat', + type: 'test', + typeName: 'test', + typeTitle: undefined, + title: 'WATEVER', + url: '#/edit/wat', + }, + ], + }); + }); + }); +}); diff --git a/src/plugins/visualizations/public/utils/saved_visualize_utils.ts b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts new file mode 100644 index 0000000000000..a28ee9486c4d2 --- /dev/null +++ b/src/plugins/visualizations/public/utils/saved_visualize_utils.ts @@ -0,0 +1,403 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import _ from 'lodash'; +import type { + SavedObjectsFindOptionsReference, + SavedObjectsFindOptions, + SavedObjectsClientContract, + SavedObjectAttributes, + SavedObjectReference, +} from 'kibana/public'; +import type { OverlayStart } from '../../../../core/public'; +import { SavedObjectNotFound } from '../../../kibana_utils/public'; +import { + extractSearchSourceReferences, + injectSearchSourceReferences, + parseSearchSourceJSON, + DataPublicPluginStart, +} from '../../../../plugins/data/public'; +import { + checkForDuplicateTitle, + saveWithConfirmation, + isErrorNonFatal, +} from '../../../../plugins/saved_objects/public'; +import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; +import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; +import { VisualizationsAppExtension } from '../vis_types/vis_type_alias_registry'; +import type { + VisSavedObject, + SerializedVis, + ISavedVis, + SaveVisOptions, + GetVisOptions, +} from '../types'; +import type { TypesStart, BaseVisType } from '../vis_types'; +// @ts-ignore +import { updateOldState } from '../legacy/vis_update_state'; +import { injectReferences, extractReferences } from './saved_visualization_references'; + +export const SAVED_VIS_TYPE = 'visualization'; + +const getDefaults = (opts: GetVisOptions) => ({ + title: '', + visState: !opts.type ? null : { type: opts.type }, + uiStateJSON: '{}', + description: '', + savedSearchId: opts.savedSearchId, + version: 1, +}); + +export function getFullPath(id: string) { + return `/app/visualize#/edit/${id}`; +} + +export function urlFor(id: string) { + return `#/edit/${encodeURIComponent(id)}`; +} + +export function mapHitSource( + visTypes: Pick, + { + attributes, + id, + references, + }: { + attributes: SavedObjectAttributes; + id: string; + references: SavedObjectReference[]; + } +) { + const newAttributes: { + id: string; + references: SavedObjectReference[]; + url: string; + savedObjectType?: string; + editUrl?: string; + type?: BaseVisType; + icon?: BaseVisType['icon']; + image?: BaseVisType['image']; + typeTitle?: BaseVisType['title']; + error?: string; + } = { + id, + references, + url: urlFor(id), + ...attributes, + }; + + let typeName = attributes.typeName; + if (attributes.visState) { + try { + typeName = JSON.parse(String(attributes.visState)).type; + } catch (e) { + /* missing typename handled below */ + } + } + + if (!typeName || !visTypes.get(typeName as string)) { + newAttributes.error = 'Unknown visualization type'; + return newAttributes; + } + + newAttributes.type = visTypes.get(typeName as string); + newAttributes.savedObjectType = 'visualization'; + newAttributes.icon = newAttributes.type?.icon; + newAttributes.image = newAttributes.type?.image; + newAttributes.typeTitle = newAttributes.type?.title; + newAttributes.editUrl = `/edit/${id}`; + + return newAttributes; +} + +export const convertToSerializedVis = (savedVis: VisSavedObject): SerializedVis => { + const { id, title, description, visState, uiStateJSON, searchSourceFields } = savedVis; + + const aggs = searchSourceFields && searchSourceFields.index ? visState.aggs || [] : visState.aggs; + + return { + id, + title, + type: visState.type, + description, + params: visState.params, + uiState: JSON.parse(uiStateJSON || '{}'), + data: { + aggs, + searchSource: searchSourceFields!, + savedSearchId: savedVis.savedSearchId, + }, + }; +}; + +export const convertFromSerializedVis = (vis: SerializedVis): ISavedVis => { + return { + id: vis.id, + title: vis.title, + description: vis.description, + visState: { + title: vis.title, + type: vis.type, + aggs: vis.data.aggs, + params: vis.params, + }, + uiStateJSON: JSON.stringify(vis.uiState), + searchSourceFields: vis.data.searchSource, + savedSearchId: vis.data.savedSearchId, + }; +}; + +export async function findListItems( + savedObjectsClient: SavedObjectsClientContract, + visTypes: Pick, + search: string, + size: number, + references?: SavedObjectsFindOptionsReference[] +) { + const visAliases = visTypes.getAliases(); + const extensions = visAliases + .map((v) => v.appExtensions?.visualizations) + .filter(Boolean) as VisualizationsAppExtension[]; + const extensionByType = extensions.reduce((acc, m) => { + return m!.docTypes.reduce((_acc, type) => { + acc[type] = m; + return acc; + }, acc); + }, {} as { [visType: string]: VisualizationsAppExtension }); + const searchOption = (field: string, ...defaults: string[]) => + _(extensions).map(field).concat(defaults).compact().flatten().uniq().value() as string[]; + const searchOptions: SavedObjectsFindOptions = { + type: searchOption('docTypes', 'visualization'), + searchFields: searchOption('searchFields', 'title^3', 'description'), + search: search ? `${search}*` : undefined, + perPage: size, + page: 1, + defaultSearchOperator: 'AND' as 'AND', + hasReference: references, + }; + + const { total, savedObjects } = await savedObjectsClient.find( + searchOptions + ); + + return { + total, + hits: savedObjects.map((savedObject) => { + const config = extensionByType[savedObject.type]; + + if (config) { + return { + ...config.toListItem(savedObject), + references: savedObject.references, + }; + } else { + return mapHitSource(visTypes, savedObject); + } + }), + }; +} + +export async function getSavedVisualization( + services: { + savedObjectsClient: SavedObjectsClientContract; + search: DataPublicPluginStart['search']; + dataViews: DataPublicPluginStart['dataViews']; + spaces?: SpacesPluginStart; + savedObjectsTagging?: SavedObjectsTaggingApi; + }, + opts?: GetVisOptions | string +): Promise { + if (typeof opts !== 'object') { + opts = { id: opts } as GetVisOptions; + } + + const id = (opts.id as string) || ''; + const savedObject = { + id, + migrationVersion: opts.migrationVersion, + displayName: SAVED_VIS_TYPE, + getEsType: () => SAVED_VIS_TYPE, + getDisplayName: () => SAVED_VIS_TYPE, + searchSource: opts.searchSource ? services.search.searchSource.createEmpty() : undefined, + } as VisSavedObject; + const defaultsProps = getDefaults(opts); + + if (!id) { + Object.assign(savedObject, defaultsProps); + return savedObject; + } + + const { + saved_object: resp, + outcome, + alias_target_id: aliasTargetId, + } = await services.savedObjectsClient.resolve(SAVED_VIS_TYPE, id); + + if (!resp._version) { + throw new SavedObjectNotFound(SAVED_VIS_TYPE, id || ''); + } + + const attributes = _.cloneDeep(resp.attributes); + + if (attributes.visState && typeof attributes.visState === 'string') { + attributes.visState = JSON.parse(attributes.visState); + } + + // assign the defaults to the response + _.defaults(attributes, defaultsProps); + + Object.assign(savedObject, attributes); + savedObject.lastSavedTitle = savedObject.title; + + savedObject.sharingSavedObjectProps = { + aliasTargetId, + outcome, + errorJSON: + outcome === 'conflict' && services.spaces + ? JSON.stringify({ + targetType: SAVED_VIS_TYPE, + sourceId: id, + targetSpace: (await services.spaces.getActiveSpace()).id, + }) + : undefined, + }; + + const meta = (attributes.kibanaSavedObjectMeta || {}) as SavedObjectAttributes; + + if (meta.searchSourceJSON) { + try { + let searchSourceValues = parseSearchSourceJSON(meta.searchSourceJSON as string); + + if (opts.searchSource) { + searchSourceValues = injectSearchSourceReferences( + searchSourceValues as any, + resp.references + ); + savedObject.searchSource = await services.search.searchSource.create(searchSourceValues); + } else { + savedObject.searchSourceFields = searchSourceValues; + } + } catch (error: any) { + throw error; + } + } + + if (resp.references && resp.references.length > 0) { + injectReferences(savedObject, resp.references); + } + + if (services.savedObjectsTagging) { + savedObject.tags = services.savedObjectsTagging.ui.getTagIdsFromReferences(resp.references); + } + + savedObject.visState = await updateOldState(savedObject.visState); + if (savedObject.searchSourceFields?.index) { + await services.dataViews.get(savedObject.searchSourceFields.index as any); + } + + return savedObject; +} + +export async function saveVisualization( + savedObject: VisSavedObject, + { + confirmOverwrite = false, + isTitleDuplicateConfirmed = false, + onTitleDuplicate, + copyOnSave = false, + }: SaveVisOptions, + services: { + savedObjectsClient: SavedObjectsClientContract; + overlays: OverlayStart; + savedObjectsTagging?: SavedObjectsTaggingApi; + } +) { + // 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 (copyOnSave) { + delete savedObject.id; + } + + const attributes: SavedObjectAttributes = { + visState: JSON.stringify(savedObject.visState), + title: savedObject.title, + uiStateJSON: savedObject.uiStateJSON, + description: savedObject.description, + savedSearchId: savedObject.savedSearchId, + version: savedObject.version, + }; + let references: SavedObjectReference[] = []; + + 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 (services.savedObjectsTagging) { + references = services.savedObjectsTagging.ui.updateTagsReferences( + references, + savedObject.tags || [] + ); + } + + const extractedRefs = extractReferences({ attributes, references }); + + if (!extractedRefs.references) { + throw new Error('References not returned from extractReferences'); + } + + try { + await checkForDuplicateTitle( + { + ...savedObject, + copyOnSave, + } as any, + isTitleDuplicateConfirmed, + onTitleDuplicate, + services as any + ); + const createOpt = { + id: savedObject.id, + migrationVersion: savedObject.migrationVersion, + references: extractedRefs.references, + }; + const resp = confirmOverwrite + ? await saveWithConfirmation(attributes, savedObject, createOpt, services) + : await services.savedObjectsClient.create(SAVED_VIS_TYPE, extractedRefs.attributes, { + ...createOpt, + overwrite: true, + }); + + savedObject.id = resp.id; + savedObject.lastSavedTitle = savedObject.title; + return savedObject.id; + } catch (err: any) { + savedObject.id = originalId; + if (isErrorNonFatal(err)) { + return ''; + } + return Promise.reject(err); + } +} diff --git a/src/plugins/visualizations/tsconfig.json b/src/plugins/visualizations/tsconfig.json index 65ab83d5e0bae..eeaed655c3e73 100644 --- a/src/plugins/visualizations/tsconfig.json +++ b/src/plugins/visualizations/tsconfig.json @@ -23,5 +23,6 @@ { "path": "../usage_collection/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../discover/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index afa9e3ce055b2..bfb23bec2111c 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -17,7 +17,8 @@ "home", "share", "savedObjectsTaggingOss", - "usageCollection" + "usageCollection", + "spaces" ], "requiredBundles": [ "kibanaUtils", diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.test.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.test.tsx new file mode 100644 index 0000000000000..2c8478492005f --- /dev/null +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.test.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import React from 'react'; +import { shallow, mount } from 'enzyme'; +import { VisualizeEditorCommon } from './visualize_editor_common'; +import { VisualizeEditorVisInstance } from '../types'; + +const mockGetLegacyUrlConflict = jest.fn(); +const mockRedirectLegacyUrl = jest.fn(() => Promise.resolve()); +jest.mock('../../../../kibana_react/public', () => ({ + useKibana: jest.fn(() => ({ + services: { + spaces: { + ui: { + redirectLegacyUrl: mockRedirectLegacyUrl, + components: { + getLegacyUrlConflict: mockGetLegacyUrlConflict, + }, + }, + }, + history: { + location: { + search: '?_g=test', + }, + }, + http: { + basePath: { + prepend: (url: string) => url, + }, + }, + }, + })), + withKibana: jest.fn((comp) => comp), +})); + +describe('VisualizeEditorCommon', () => { + it('should display a conflict callout if saved object conflicts', async () => { + shallow( + {}} + hasUnappliedChanges={false} + isEmbeddableRendered={false} + onAppLeave={() => {}} + visEditorRef={React.createRef()} + visInstance={ + { + savedVis: { + id: 'test', + sharingSavedObjectProps: { + outcome: 'conflict', + aliasTargetId: 'alias_id', + }, + }, + vis: { + type: { + title: 'TSVB', + }, + }, + } as VisualizeEditorVisInstance + } + /> + ); + expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: 'test', + objectNoun: 'TSVB visualization', + otherObjectId: 'alias_id', + otherObjectPath: '#/edit/alias_id?_g=test', + }); + }); + + it('should redirect to new id if saved object aliasMatch', async () => { + mount( + {}} + hasUnappliedChanges={false} + isEmbeddableRendered={false} + onAppLeave={() => {}} + visEditorRef={React.createRef()} + visInstance={ + { + savedVis: { + id: 'test', + sharingSavedObjectProps: { + outcome: 'aliasMatch', + aliasTargetId: 'alias_id', + }, + }, + vis: { + type: { + title: 'TSVB', + }, + }, + } as VisualizeEditorVisInstance + } + /> + ); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith( + '#/edit/alias_id?_g=test', + 'TSVB visualization' + ); + }); +}); diff --git a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx index a03073e61f59c..6268ba5c936ef 100644 --- a/src/plugins/visualize/public/application/components/visualize_editor_common.tsx +++ b/src/plugins/visualize/public/application/components/visualize_editor_common.tsx @@ -7,15 +7,19 @@ */ import './visualize_editor.scss'; -import React, { RefObject } from 'react'; +import React, { RefObject, useCallback, useEffect } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; +import { i18n } from '@kbn/i18n'; import { EuiScreenReaderOnly } from '@elastic/eui'; import { AppMountParameters } from 'kibana/public'; import { VisualizeTopNav } from './visualize_top_nav'; import { ExperimentalVisInfo } from './experimental_vis_info'; +import { useKibana } from '../../../../kibana_react/public'; +import { urlFor } from '../../../../visualizations/public'; import { SavedVisInstance, VisualizeAppState, + VisualizeServices, VisualizeAppStateContainer, VisualizeEditorVisInstance, } from '../types'; @@ -53,6 +57,55 @@ export const VisualizeEditorCommon = ({ embeddableId, visEditorRef, }: VisualizeEditorCommonProps) => { + const { services } = useKibana(); + + useEffect(() => { + async function aliasMatchRedirect() { + const sharingSavedObjectProps = visInstance?.savedVis.sharingSavedObjectProps; + if (services.spaces && sharingSavedObjectProps?.outcome === 'aliasMatch') { + // We found this object by a legacy URL alias from its old ID; redirect the user to the page with its new ID, preserving any URL hash + const newObjectId = sharingSavedObjectProps?.aliasTargetId; // This is always defined if outcome === 'aliasMatch' + const newPath = `${urlFor(newObjectId!)}${services.history.location.search}`; + await services.spaces.ui.redirectLegacyUrl( + newPath, + i18n.translate('visualize.legacyUrlConflict.objectNoun', { + defaultMessage: '{visName} visualization', + values: { + visName: visInstance?.vis?.type.title, + }, + }) + ); + return; + } + } + + aliasMatchRedirect(); + }, [visInstance?.savedVis.sharingSavedObjectProps, visInstance?.vis?.type.title, services]); + + const getLegacyUrlConflictCallout = useCallback(() => { + // This function returns a callout component *if* we have encountered a "legacy URL conflict" scenario + const currentObjectId = visInstance?.savedVis.id; + const sharingSavedObjectProps = visInstance?.savedVis.sharingSavedObjectProps; + if (services.spaces && sharingSavedObjectProps?.outcome === 'conflict' && currentObjectId) { + // We have resolved to one object, but another object has a legacy URL alias associated with this ID/page. We should display a + // callout with a warning for the user, and provide a way for them to navigate to the other object. + const otherObjectId = sharingSavedObjectProps?.aliasTargetId!; // This is always defined if outcome === 'conflict' + const otherObjectPath = `${urlFor(otherObjectId)}${services.history.location.search}`; + return services.spaces.ui.components.getLegacyUrlConflict({ + objectNoun: i18n.translate('visualize.legacyUrlConflict.objectNoun', { + defaultMessage: '{visName} visualization', + values: { + visName: visInstance?.vis?.type.title, + }, + }), + currentObjectId, + otherObjectId, + otherObjectPath, + }); + } + return null; + }, [visInstance?.savedVis, services, visInstance?.vis?.type.title]); + return (
{visInstance && appState && currentAppState && ( @@ -74,6 +127,7 @@ export const VisualizeEditorCommon = ({ )} {visInstance?.vis?.type?.stage === 'experimental' && } {visInstance?.vis?.type?.getInfoMessage?.(visInstance.vis)} + {getLegacyUrlConflictCallout()} {visInstance && (

diff --git a/src/plugins/visualize/public/application/components/visualize_listing.tsx b/src/plugins/visualize/public/application/components/visualize_listing.tsx index 07c3d18b54b0d..7319a9b5e52f8 100644 --- a/src/plugins/visualize/public/application/components/visualize_listing.tsx +++ b/src/plugins/visualize/public/application/components/visualize_listing.tsx @@ -31,7 +31,6 @@ export const VisualizeListing = () => { chrome, dashboard, history, - savedVisualizations, toastNotifications, visualizations, stateTransferService, @@ -113,16 +112,16 @@ export const VisualizeListing = () => { } const isLabsEnabled = uiSettings.get(VISUALIZE_ENABLE_LABS_SETTING); - return savedVisualizations - .findListItems(searchTerm, { size: listingLimit, references }) - .then(({ total, hits }: { total: number; hits: object[] }) => ({ + return visualizations + .findListItems(searchTerm, listingLimit, references) + .then(({ total, hits }: { total: number; hits: Array> }) => ({ total, hits: hits.filter( (result: any) => isLabsEnabled || result.type?.stage !== 'experimental' ), })); }, - [listingLimit, savedVisualizations, uiSettings, savedObjectsTagging] + [listingLimit, uiSettings, savedObjectsTagging, visualizations] ); const deleteItems = useCallback( diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 7e9f69163f5a6..4debd9a4a7b7d 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -42,6 +42,7 @@ import type { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/p import type { EmbeddableStart, EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; import type { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import type { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; +import type { SpacesPluginStart } from '../../../../../x-pack/plugins/spaces/public'; import type { DashboardStart } from '../../../dashboard/public'; import type { SavedObjectsTaggingApi } from '../../../saved_objects_tagging_oss/public'; import type { UsageCollectionStart } from '../../../usage_collection/public'; @@ -94,7 +95,6 @@ export interface VisualizeServices extends CoreStart { dashboardCapabilities: Record>; visualizations: VisualizationsStart; savedObjectsPublic: SavedObjectsStart; - savedVisualizations: VisualizationsStart['savedVisualizationsLoader']; setActiveUrl: (newUrl: string) => void; createVisEmbeddableFromObject: VisualizationsStart['__LEGACY']['createVisEmbeddableFromObject']; restorePreviousUrl: () => void; @@ -105,6 +105,7 @@ export interface VisualizeServices extends CoreStart { presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; getKibanaVersion: () => string; + spaces?: SpacesPluginStart; } export interface SavedVisInstance { diff --git a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx index 0dc37ca00a6aa..9d1c93f25645c 100644 --- a/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx +++ b/src/plugins/visualize/public/application/utils/get_top_nav_config.tsx @@ -14,7 +14,11 @@ import { parse } from 'query-string'; import { Capabilities } from 'src/core/public'; import { TopNavMenuData } from 'src/plugins/navigation/public'; -import { VISUALIZE_EMBEDDABLE_TYPE, VisualizeInput } from '../../../../visualizations/public'; +import { + VISUALIZE_EMBEDDABLE_TYPE, + VisualizeInput, + getFullPath, +} from '../../../../visualizations/public'; import { showSaveModal, SavedObjectSaveModalOrigin, @@ -87,6 +91,7 @@ export const getTopNavConfig = ( data, application, chrome, + overlays, history, share, setActiveUrl, @@ -99,6 +104,8 @@ export const getTopNavConfig = ( presentationUtil, usageCollection, getKibanaVersion, + savedObjects, + visualizations, }: VisualizeServices ) => { const { vis, embeddableHandler } = visInstance; @@ -117,8 +124,10 @@ export const getTopNavConfig = ( /** * Called when the user clicks "Save" button. */ - async function doSave(saveOptions: SavedObjectSaveOpts & { dashboardId?: string }) { - const newlyCreated = !Boolean(savedVis.id) || savedVis.copyOnSave; + async function doSave( + saveOptions: SavedObjectSaveOpts & { dashboardId?: string; copyOnSave?: boolean } + ) { + const newlyCreated = !Boolean(savedVis.id) || saveOptions.copyOnSave; // vis.title was not bound and it's needed to reflect title into visState stateContainer.transitions.setVis({ title: savedVis.title, @@ -129,7 +138,7 @@ export const getTopNavConfig = ( setHasUnsavedChanges(false); try { - const id = await savedVis.save(saveOptions); + const id = await visualizations.saveVisualization(savedVis, saveOptions); if (id) { toastNotifications.addSuccess({ @@ -142,6 +151,8 @@ export const getTopNavConfig = ( 'data-test-subj': 'saveVisualizationSuccess', }); + chrome.recentlyAccessed.add(getFullPath(id), savedVis.title, String(id)); + if ((originatingApp && saveOptions.returnToOrigin) || saveOptions.dashboardId) { if (!embeddableId) { const appPath = `${VisualizeConstants.EDIT_PATH}/${encodeURIComponent(id)}`; @@ -164,7 +175,7 @@ export const getTopNavConfig = ( state: { type: VISUALIZE_EMBEDDABLE_TYPE, input: { savedObjectId: id }, - embeddableId: savedVis.copyOnSave ? undefined : embeddableId, + embeddableId: saveOptions.copyOnSave ? undefined : embeddableId, searchSessionId: data.search.session.getSessionId(), }, path, @@ -392,11 +403,10 @@ export const getTopNavConfig = ( const currentTitle = savedVis.title; savedVis.title = newTitle; embeddableHandler.updateInput({ title: newTitle }); - savedVis.copyOnSave = newCopyOnSave; savedVis.description = newDescription; - if (savedObjectsTagging && savedObjectsTagging.ui.hasTagDecoration(savedVis)) { - savedVis.setTags(selectedTags); + if (savedObjectsTagging) { + savedVis.tags = selectedTags; } const saveOptions = { @@ -405,6 +415,7 @@ export const getTopNavConfig = ( onTitleDuplicate, returnToOrigin, dashboardId: !!dashboardId ? dashboardId : undefined, + copyOnSave: newCopyOnSave, }; // If we're adding to a dashboard and not saving to library, @@ -457,9 +468,7 @@ export const getTopNavConfig = ( let tagOptions: React.ReactNode | undefined; if (savedObjectsTagging) { - if (savedVis && savedObjectsTagging.ui.hasTagDecoration(savedVis)) { - selectedTags = savedVis.getTags(); - } + selectedTags = savedVis.tags || []; tagOptions = ( ({ })), })); +let savedVisMock: VisSavedObject; + describe('getVisualizationInstance', () => { const serializedVisMock = { type: 'area', }; - let savedVisMock: VisSavedObject; let visMock: Vis; let mockServices: jest.Mocked; let subj: BehaviorSubject; @@ -47,13 +48,16 @@ describe('getVisualizationInstance', () => { data: {}, } as Vis; savedVisMock = {} as VisSavedObject; + // @ts-expect-error mockServices.data.search.showError.mockImplementation(() => {}); // @ts-expect-error - mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); - // @ts-expect-error mockServices.visualizations.convertToSerializedVis.mockImplementation(() => serializedVisMock); // @ts-expect-error + mockServices.visualizations.getSavedVisualization.mockImplementation( + (opts: unknown) => savedVisMock + ); + // @ts-expect-error mockServices.visualizations.createVis.mockImplementation(() => visMock); // @ts-expect-error mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({ @@ -71,7 +75,9 @@ describe('getVisualizationInstance', () => { opts ); - expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith(opts); + expect((mockServices.visualizations.getSavedVisualization as jest.Mock).mock.calls[0][0]).toBe( + opts + ); expect(savedVisMock.searchSourceFields).toEqual({ index: opts.indexPattern, }); @@ -98,7 +104,9 @@ describe('getVisualizationInstance', () => { visMock.type.setup = jest.fn(() => newVisObj); const { vis } = await getVisualizationInstance(mockServices, 'saved_vis_id'); - expect(mockServices.savedVisualizations.get).toHaveBeenCalledWith('saved_vis_id'); + expect((mockServices.visualizations.getSavedVisualization as jest.Mock).mock.calls[0][0]).toBe( + 'saved_vis_id' + ); expect(savedVisMock.searchSourceFields).toBeUndefined(); expect(visMock.type.setup).toHaveBeenCalledWith(visMock); expect(vis).toBe(newVisObj); @@ -128,7 +136,6 @@ describe('getVisualizationInstanceInput', () => { const serializedVisMock = { type: 'pie', }; - let savedVisMock: VisSavedObject; let visMock: Vis; let mockServices: jest.Mocked; let subj: BehaviorSubject; @@ -142,10 +149,12 @@ describe('getVisualizationInstanceInput', () => { } as Vis; savedVisMock = {} as VisSavedObject; // @ts-expect-error - mockServices.savedVisualizations.get.mockImplementation(() => savedVisMock); - // @ts-expect-error mockServices.visualizations.createVis.mockImplementation(() => visMock); // @ts-expect-error + mockServices.visualizations.getSavedVisualization.mockImplementation( + (opts: unknown) => savedVisMock + ); + // @ts-expect-error mockServices.createVisEmbeddableFromObject.mockImplementation(() => ({ getOutput$: jest.fn(() => subj.asObservable()), })); @@ -183,7 +192,7 @@ describe('getVisualizationInstanceInput', () => { const { savedVis, savedSearch, vis, embeddableHandler } = await getVisualizationInstanceFromInput(mockServices, input); - expect(mockServices.savedVisualizations.get).toHaveBeenCalled(); + expect(mockServices.visualizations.getSavedVisualization).toHaveBeenCalled(); expect(mockServices.visualizations.createVis).toHaveBeenCalledWith( serializedVisMock.type, input.savedVis diff --git a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts index 88797ce264e25..faf25ff28cec0 100644 --- a/src/plugins/visualize/public/application/utils/get_visualization_instance.ts +++ b/src/plugins/visualize/public/application/utils/get_visualization_instance.ts @@ -66,14 +66,15 @@ export const getVisualizationInstanceFromInput = async ( visualizeServices: VisualizeServices, input: VisualizeInput ) => { - const { visualizations, savedVisualizations } = visualizeServices; + const { visualizations } = visualizeServices; const visState = input.savedVis as SerializedVis; /** * A saved vis is needed even in by value mode to support 'save to library' which converts the 'by value' * state of the visualization, into a new saved object. */ - const savedVis: VisSavedObject = await savedVisualizations.get(); + const savedVis: VisSavedObject = await visualizations.getSavedVisualization(); + if (visState.uiState && Object.keys(visState.uiState).length !== 0) { savedVis.uiStateJSON = JSON.stringify(visState.uiState); } @@ -107,8 +108,8 @@ export const getVisualizationInstance = async ( */ opts?: Record | string ) => { - const { visualizations, savedVisualizations } = visualizeServices; - const savedVis: VisSavedObject = await savedVisualizations.get(opts); + const { visualizations } = visualizeServices; + const savedVis: VisSavedObject = await visualizations.getSavedVisualization(opts); if (typeof opts !== 'string') { savedVis.searchSourceFields = { index: opts?.indexPattern } as SearchSourceFields; diff --git a/src/plugins/visualize/public/application/utils/mocks.ts b/src/plugins/visualize/public/application/utils/mocks.ts index a7029071851ca..f26c81ed99a89 100644 --- a/src/plugins/visualize/public/application/utils/mocks.ts +++ b/src/plugins/visualize/public/application/utils/mocks.ts @@ -26,7 +26,6 @@ export const createVisualizeServicesMock = () => { location: { pathname: '' }, }, visualizations, - savedVisualizations: visualizations.savedVisualizationsLoader, createVisEmbeddableFromObject: visualizations.__LEGACY.createVisEmbeddableFromObject, } as unknown as jest.Mocked; }; diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts index b142f3fcd4061..f81744326365a 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.test.ts @@ -22,7 +22,6 @@ import { createEmbeddableStateTransferMock } from '../../../../../embeddable/pub const mockDefaultEditorControllerDestroy = jest.fn(); const mockEmbeddableHandlerDestroy = jest.fn(); const mockEmbeddableHandlerRender = jest.fn(); -const mockSavedVisDestroy = jest.fn(); const savedVisId = '9ca7aa90-b892-11e8-a6d9-e546fe2bba5f'; const mockSavedVisInstance = { embeddableHandler: { @@ -32,7 +31,6 @@ const mockSavedVisInstance = { savedVis: { id: savedVisId, title: 'Test Vis', - destroy: mockSavedVisDestroy, }, vis: { type: {}, @@ -103,7 +101,6 @@ describe('useSavedVisInstance', () => { mockDefaultEditorControllerDestroy.mockClear(); mockEmbeddableHandlerDestroy.mockClear(); mockEmbeddableHandlerRender.mockClear(); - mockSavedVisDestroy.mockClear(); toastNotifications.addWarning.mockClear(); mockGetVisualizationInstance.mockClear(); }); @@ -153,7 +150,6 @@ describe('useSavedVisInstance', () => { expect(mockDefaultEditorControllerDestroy.mock.calls.length).toBe(1); expect(mockEmbeddableHandlerDestroy).not.toHaveBeenCalled(); - expect(mockSavedVisDestroy.mock.calls.length).toBe(1); }); }); @@ -236,7 +232,6 @@ describe('useSavedVisInstance', () => { unmount(); expect(mockDefaultEditorControllerDestroy).not.toHaveBeenCalled(); expect(mockEmbeddableHandlerDestroy.mock.calls.length).toBe(1); - expect(mockSavedVisDestroy.mock.calls.length).toBe(1); }); }); }); diff --git a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts index 965951bfbd88d..b5919ec074966 100644 --- a/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts +++ b/src/plugins/visualize/public/application/utils/use/use_saved_vis_instance.ts @@ -176,9 +176,6 @@ export const useSavedVisInstance = ( } else if (state.savedVisInstance?.embeddableHandler) { state.savedVisInstance.embeddableHandler.destroy(); } - if (state.savedVisInstance?.savedVis) { - state.savedVisInstance.savedVis.destroy(); - } }; }, [state]); diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index b128c09209743..c9df6a6ec57d8 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -28,6 +28,7 @@ import { createKbnUrlStateStorage, withNotifyOnErrors, } from '../../kibana_utils/public'; +import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { VisualizeConstants } from './application/visualize_constants'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; @@ -61,6 +62,7 @@ export interface VisualizePluginStartDependencies { savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; presentationUtil: PresentationUtilPluginStart; usageCollection?: UsageCollectionStart; + spaces: SpacesPluginStart; } export interface VisualizePluginSetupDependencies { @@ -192,7 +194,6 @@ export class VisualizePlugin data: pluginsStart.data, localStorage: new Storage(localStorage), navigation: pluginsStart.navigation, - savedVisualizations: pluginsStart.visualizations.savedVisualizationsLoader, share: pluginsStart.share, toastNotifications: coreStart.notifications.toasts, visualizeCapabilities: coreStart.application.capabilities.visualize, @@ -212,6 +213,7 @@ export class VisualizePlugin presentationUtil: pluginsStart.presentationUtil, usageCollection: pluginsStart.usageCollection, getKibanaVersion: () => this.initializerContext.env.packageInfo.version, + spaces: pluginsStart.spaces, }; params.element.classList.add('visAppWrapper'); diff --git a/src/plugins/visualize/tsconfig.json b/src/plugins/visualize/tsconfig.json index 3f1f7487085bf..9c1e3fd72ff8b 100644 --- a/src/plugins/visualize/tsconfig.json +++ b/src/plugins/visualize/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../kibana_react/tsconfig.json" }, { "path": "../home/tsconfig.json" }, { "path": "../presentation_util/tsconfig.json" }, - { "path": "../discover/tsconfig.json" } + { "path": "../discover/tsconfig.json" }, + { "path": "../../../x-pack/plugins/spaces/tsconfig.json" }, ] } From 930fe96260d08830d64cd6e7f52b553341ceaa48 Mon Sep 17 00:00:00 2001 From: juliaElastic <90178898+juliaElastic@users.noreply.github.com> Date: Mon, 11 Oct 2021 11:00:53 +0200 Subject: [PATCH 002/287] [Fleet] added support for installing tag saved objects (#114110) * added tag saved objects to assets * fixed review comments * added translation to constants * added missing icon type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../package_to_package_policy.test.ts | 1 + .../plugins/fleet/common/types/models/epm.ts | 2 + .../components/assets_facet_group.stories.tsx | 1 + .../integrations/sections/epm/constants.tsx | 65 ++++++++++++++----- .../services/epm/kibana/assets/install.ts | 2 + ...kage_policies_to_agent_permissions.test.ts | 3 + .../context/fixtures/integration.nginx.ts | 1 + .../context/fixtures/integration.okta.ts | 1 + .../apis/epm/install_remove_assets.ts | 11 ++++ .../apis/epm/update_assets.ts | 5 ++ .../kibana/dashboard/sample_dashboard.json | 3 +- .../0.1.0/kibana/tag/sample_tag.json | 17 +++++ .../kibana/dashboard/sample_dashboard.json | 3 +- .../0.2.0/kibana/tag/sample_tag.json | 17 +++++ .../error_handling/0.2.0/manifest.yml | 1 + 15 files changed, 116 insertions(+), 17 deletions(-) create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/tag/sample_tag.json create mode 100644 x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/tag/sample_tag.json diff --git a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts index 275cf237a9621..e554eb925c38a 100644 --- a/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts +++ b/x-pack/plugins/fleet/common/services/package_to_package_policy.test.ts @@ -33,6 +33,7 @@ describe('Fleet - packageToPackagePolicy', () => { lens: [], ml_module: [], security_rule: [], + tag: [], }, elasticsearch: { ingest_pipeline: [], diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index 1325d74f82b68..a487fd0a37e70 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -69,6 +69,7 @@ export enum KibanaAssetType { lens = 'lens', securityRule = 'security_rule', mlModule = 'ml_module', + tag = 'tag', } /* @@ -83,6 +84,7 @@ export enum KibanaSavedObjectType { lens = 'lens', mlModule = 'ml-module', securityRule = 'security-rule', + tag = 'tag', } export enum ElasticsearchAssetType { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/assets_facet_group.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/assets_facet_group.stories.tsx index d98f2b2408d56..a7fa069e77a69 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/assets_facet_group.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/assets_facet_group.stories.tsx @@ -36,6 +36,7 @@ export const AssetsFacetGroup = ({ width }: Args) => { lens: [], security_rule: [], ml_module: [], + tag: [], }, elasticsearch: { component_template: [], diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx index 8e900e625215f..25604bb6b984d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/constants.tsx @@ -6,6 +6,7 @@ */ import type { IconType } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import type { ServiceName } from '../../types'; import { ElasticsearchAssetType, KibanaAssetType } from '../../types'; @@ -22,21 +23,54 @@ export const DisplayedAssets: ServiceNameToAssetTypes = { export type DisplayedAssetType = ElasticsearchAssetType | KibanaAssetType | 'view'; export const AssetTitleMap: Record = { - dashboard: 'Dashboards', - ilm_policy: 'ILM policies', - ingest_pipeline: 'Ingest pipelines', - transform: 'Transforms', - index_pattern: 'Index patterns', - index_template: 'Index templates', - component_template: 'Component templates', - search: 'Saved searches', - visualization: 'Visualizations', - map: 'Maps', - data_stream_ilm_policy: 'Data stream ILM policies', - lens: 'Lens', - security_rule: 'Security rules', - ml_module: 'ML modules', - view: 'Views', + dashboard: i18n.translate('xpack.fleet.epm.assetTitles.dashboards', { + defaultMessage: 'Dashboards', + }), + ilm_policy: i18n.translate('xpack.fleet.epm.assetTitles.ilmPolicies', { + defaultMessage: 'ILM policies', + }), + ingest_pipeline: i18n.translate('xpack.fleet.epm.assetTitles.ingestPipelines', { + defaultMessage: 'Ingest pipelines', + }), + transform: i18n.translate('xpack.fleet.epm.assetTitles.transforms', { + defaultMessage: 'Transforms', + }), + index_pattern: i18n.translate('xpack.fleet.epm.assetTitles.indexPatterns', { + defaultMessage: 'Index patterns', + }), + index_template: i18n.translate('xpack.fleet.epm.assetTitles.indexTemplates', { + defaultMessage: 'Index templates', + }), + component_template: i18n.translate('xpack.fleet.epm.assetTitles.componentTemplates', { + defaultMessage: 'Component templates', + }), + search: i18n.translate('xpack.fleet.epm.assetTitles.savedSearches', { + defaultMessage: 'Saved searches', + }), + visualization: i18n.translate('xpack.fleet.epm.assetTitles.visualizations', { + defaultMessage: 'Visualizations', + }), + map: i18n.translate('xpack.fleet.epm.assetTitles.maps', { + defaultMessage: 'Maps', + }), + data_stream_ilm_policy: i18n.translate('xpack.fleet.epm.assetTitles.dataStreamILM', { + defaultMessage: 'Data stream ILM policies', + }), + lens: i18n.translate('xpack.fleet.epm.assetTitles.lens', { + defaultMessage: 'Lens', + }), + security_rule: i18n.translate('xpack.fleet.epm.assetTitles.securityRules', { + defaultMessage: 'Security rules', + }), + ml_module: i18n.translate('xpack.fleet.epm.assetTitles.mlModules', { + defaultMessage: 'ML modules', + }), + view: i18n.translate('xpack.fleet.epm.assetTitles.views', { + defaultMessage: 'Views', + }), + tag: i18n.translate('xpack.fleet.epm.assetTitles.tag', { + defaultMessage: 'Tag', + }), }; export const ServiceTitleMap: Record = { @@ -53,6 +87,7 @@ export const AssetIcons: Record = { lens: 'lensApp', security_rule: 'securityApp', ml_module: 'mlApp', + tag: 'tagApp', }; export const ServiceIcons: Record = { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts index 0f2d7b6679bf9..50c0239cd8c56 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/assets/install.ts @@ -39,6 +39,7 @@ const KibanaSavedObjectTypeMapping: Record { diff --git a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts index 9f8ac01afe6c9..845e4f1d2670e 100644 --- a/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies_to_agent_permissions.test.ts @@ -97,6 +97,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { lens: [], security_rule: [], ml_module: [], + tag: [], }, elasticsearch: { component_template: [], @@ -207,6 +208,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { lens: [], security_rule: [], ml_module: [], + tag: [], }, elasticsearch: { component_template: [], @@ -323,6 +325,7 @@ describe('storedPackagePoliciesToAgentPermissions()', () => { lens: [], security_rule: [], ml_module: [], + tag: [], }, elasticsearch: { component_template: [], diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts b/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts index 50262b73a6a41..e0179897a59c7 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/integration.nginx.ts @@ -252,6 +252,7 @@ export const response: GetInfoResponse['response'] = { lens: [], map: [], security_rule: [], + tag: [], }, elasticsearch: { ingest_pipeline: [ diff --git a/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts b/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts index efef00579f4bd..387161171485b 100644 --- a/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts +++ b/x-pack/plugins/fleet/storybook/context/fixtures/integration.okta.ts @@ -105,6 +105,7 @@ export const response: GetInfoResponse['response'] = { lens: [], ml_module: [], security_rule: [], + tag: [], }, elasticsearch: { ingest_pipeline: [ diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index e57899531e939..79f3d52821f75 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -414,6 +414,7 @@ const expectAssetsInstalled = ({ id: 'sample_dashboard', }); expect(resDashboard.id).equal('sample_dashboard'); + expect(resDashboard.references.map((ref: any) => ref.id).includes('sample_tag')).equal(true); const resDashboard2 = await kibanaServer.savedObjects.get({ type: 'dashboard', id: 'sample_dashboard2', @@ -444,6 +445,11 @@ const expectAssetsInstalled = ({ id: 'sample_security_rule', }); expect(resSecurityRule.id).equal('sample_security_rule'); + const resTag = await kibanaServer.savedObjects.get({ + type: 'tag', + id: 'sample_tag', + }); + expect(resTag.id).equal('sample_tag'); const resIndexPattern = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'test-*', @@ -521,6 +527,10 @@ const expectAssetsInstalled = ({ id: 'sample_security_rule', type: 'security-rule', }, + { + id: 'sample_tag', + type: 'tag', + }, { id: 'sample_visualization', type: 'visualization', @@ -607,6 +617,7 @@ const expectAssetsInstalled = ({ { id: '4c758d70-ecf1-56b3-b704-6d8374841b34', type: 'epm-packages-assets' }, { id: 'e786cbd9-0f3b-5a0b-82a6-db25145ebf58', type: 'epm-packages-assets' }, { id: 'd8b175c3-0d42-5ec7-90c1-d1e4b307a4c2', type: 'epm-packages-assets' }, + { id: 'b265a5e0-c00b-5eda-ac44-2ddbd36d9ad0', type: 'epm-packages-assets' }, { id: '53c94591-aa33-591d-8200-cd524c2a0561', type: 'epm-packages-assets' }, { id: 'b658d2d4-752e-54b8-afc2-4c76155c1466', type: 'epm-packages-assets' }, ], diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index f46dcdb761e6d..5282312164148 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -339,6 +339,10 @@ export default function (providerContext: FtrProviderContext) { id: 'sample_ml_module', type: 'ml-module', }, + { + id: 'sample_tag', + type: 'tag', + }, ], installed_es: [ { @@ -418,6 +422,7 @@ export default function (providerContext: FtrProviderContext) { { id: '4281a436-45a8-54ab-9724-fda6849f789d', type: 'epm-packages-assets' }, { id: '2e56f08b-1d06-55ed-abee-4708e1ccf0aa', type: 'epm-packages-assets' }, { id: '4035007b-9c33-5227-9803-2de8a17523b5', type: 'epm-packages-assets' }, + { id: 'e6ae7d31-6920-5408-9219-91ef1662044b', type: 'epm-packages-assets' }, { id: 'c7bf1a39-e057-58a0-afde-fb4b48751d8c', type: 'epm-packages-assets' }, { id: '8c665f28-a439-5f43-b5fd-8fda7b576735', type: 'epm-packages-assets' }, ], diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json index 7f416c26cc9aa..c75dd7673dc38 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/dashboard/sample_dashboard.json @@ -15,7 +15,8 @@ { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, { "id": "sample_search", "name": "panel_1", "type": "search" }, { "id": "sample_search", "name": "panel_2", "type": "search" }, - { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" }, + { "id": "sample_tag", "type": "tag", "name": "tag-ref-sample_tag" } ], "id": "sample_dashboard", "type": "dashboard" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/tag/sample_tag.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/tag/sample_tag.json new file mode 100644 index 0000000000000..c6494d42679b9 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/kibana/tag/sample_tag.json @@ -0,0 +1,17 @@ +{ + "id": "sample_tag", + "type": "tag", + "namespaces": [ + "default" + ], + "attributes": { + "name": "my tag", + "description": "", + "color": "#a80853" + }, + "references": [], + "migrationVersion": { + "tag": "8.0.0" + }, + "coreMigrationVersion": "8.0.0" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json index 4513c07f27786..1215a934c6368 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/dashboard/sample_dashboard.json @@ -15,7 +15,8 @@ { "id": "sample_visualization", "name": "panel_0", "type": "visualization" }, { "id": "sample_search2", "name": "panel_1", "type": "search" }, { "id": "sample_search2", "name": "panel_2", "type": "search" }, - { "id": "sample_visualization", "name": "panel_3", "type": "visualization" } + { "id": "sample_visualization", "name": "panel_3", "type": "visualization" }, + { "id": "sample_tag", "type": "tag", "name": "tag-ref-sample_tag" } ], "id": "sample_dashboard", "type": "dashboard" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/tag/sample_tag.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/tag/sample_tag.json new file mode 100644 index 0000000000000..c6494d42679b9 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.2.0/kibana/tag/sample_tag.json @@ -0,0 +1,17 @@ +{ + "id": "sample_tag", + "type": "tag", + "namespaces": [ + "default" + ], + "attributes": { + "name": "my tag", + "description": "", + "color": "#a80853" + }, + "references": [], + "migrationVersion": { + "tag": "8.0.0" + }, + "coreMigrationVersion": "8.0.0" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml index c92f0ab5ae7f3..c473ce29b87d5 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/error_handling/0.2.0/manifest.yml @@ -17,3 +17,4 @@ requirement: icons: - src: '/img/logo_overrides_64_color.svg' size: '16x16' + type: 'image/svg+xml' From 25fef38f123dc6c7b5e6c94f7268d42a248b5db7 Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 11 Oct 2021 04:19:00 -0500 Subject: [PATCH 003/287] [fleet][ui] Fix offset image; scrollbar flashing; missing assets in Stories (#114406) --- packages/kbn-storybook/src/lib/run_storybook_cli.ts | 6 +++++- .../plugins/fleet/public/applications/integrations/app.tsx | 6 ++---- .../public/applications/integrations/layouts/default.tsx | 2 ++ 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/packages/kbn-storybook/src/lib/run_storybook_cli.ts b/packages/kbn-storybook/src/lib/run_storybook_cli.ts index 24a3e4511f7be..93197a1f2b318 100644 --- a/packages/kbn-storybook/src/lib/run_storybook_cli.ts +++ b/packages/kbn-storybook/src/lib/run_storybook_cli.ts @@ -36,7 +36,11 @@ export function runStorybookCli({ configDir, name }: { configDir: string; name: async ({ flags, log }) => { log.debug('Global config:\n', constants); - const staticDir = [UiSharedDepsNpm.distDir, UiSharedDepsSrc.distDir]; + const staticDir = [ + UiSharedDepsNpm.distDir, + UiSharedDepsSrc.distDir, + 'src/plugins/kibana_react/public/assets:plugins/kibanaReact/assets', + ]; const config: Record = { configDir, mode: flags.site ? 'static' : 'dev', diff --git a/x-pack/plugins/fleet/public/applications/integrations/app.tsx b/x-pack/plugins/fleet/public/applications/integrations/app.tsx index c5cc1e1892eda..b10cef9d3ffe4 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/app.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/app.tsx @@ -39,15 +39,13 @@ import { Error, Loading, SettingFlyout } from './components'; import type { UIExtensionsStorage } from './types'; import { EPMApp } from './sections/epm'; -import { DefaultLayout, WithoutHeaderLayout } from './layouts'; +import { DefaultLayout } from './layouts'; import { PackageInstallProvider } from './hooks'; import { useBreadcrumbs, UIExtensionsContext } from './hooks'; const ErrorLayout = ({ children }: { children: JSX.Element }) => ( - - {children} - + {children} ); diff --git a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx index e4de48a85c35a..70e55c9bd56b0 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/layouts/default.tsx @@ -28,6 +28,8 @@ interface Props { const Illustration = styled(EuiImage)` margin-bottom: -68px; + position: relative; + top: -20px; width: 80%; `; From cba91fdaab4153c4a7257c506f187c30755b3031 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Mon, 11 Oct 2021 11:48:10 +0200 Subject: [PATCH 004/287] Status service: improve overall status summary (#114228) * improve getSummaryStatus * fix unit tests --- .../server/status/get_summary_status.test.ts | 153 +++++++++--------- src/core/server/status/get_summary_status.ts | 40 +++-- src/core/server/status/plugins_status.test.ts | 32 ++-- src/core/server/status/status_service.test.ts | 26 +-- 4 files changed, 127 insertions(+), 124 deletions(-) diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index 33b2e6f7913a1..2c91aa8c7b16a 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -81,93 +81,86 @@ describe('getSummaryStatus', () => { }); describe('summary', () => { - describe('when a single service is at highest level', () => { - it('returns all information about that single service', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - meta: { - custom: { data: 'here' }, - }, + it('returns correct summary when a single service is affected', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[s2]: Lorem ipsum', - detail: 'See the status page for more information', - meta: { - affectedServices: ['s2'], - }, - }); + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '1 service is unavailable: s2', + detail: 'See the status page for more information', + meta: { + affectedServices: ['s2'], + }, }); + }); - it('allows the single service to override the detail and documentationUrl fields', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, + it('returns correct summary when multiple services are affected', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + s3: { + level: ServiceStatusLevels.unavailable, + summary: 'Proin mattis', + detail: 'Nunc quis nulla at mi lobortis pretium.', + documentationUrl: 'http://helpmenow.com/problem2', + meta: { + other: { data: 'over there' }, }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[s2]: Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - affectedServices: ['s2'], - }, - }); + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '2 services are unavailable: s2, s3', + detail: 'See the status page for more information', + meta: { + affectedServices: ['s2', 's3'], + }, }); }); - describe('when multiple services is at highest level', () => { - it('returns aggregated information about the affected services', () => { - expect( - getSummaryStatus( - Object.entries({ - s1: degraded, - s2: { - level: ServiceStatusLevels.unavailable, - summary: 'Lorem ipsum', - detail: 'Vivamus pulvinar sem ac luctus ultrices.', - documentationUrl: 'http://helpmenow.com/problem1', - meta: { - custom: { data: 'here' }, - }, - }, - s3: { - level: ServiceStatusLevels.unavailable, - summary: 'Proin mattis', - detail: 'Nunc quis nulla at mi lobortis pretium.', - documentationUrl: 'http://helpmenow.com/problem2', - meta: { - other: { data: 'over there' }, - }, - }, - }) - ) - ).toEqual({ - level: ServiceStatusLevels.unavailable, - summary: '[2] services are unavailable', - detail: 'See the status page for more information', - meta: { - affectedServices: ['s2', 's3'], - }, - }); + it('returns correct summary more than `maxServices` services are affected', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: available, + s3: degraded, + s4: degraded, + s5: degraded, + s6: available, + s7: degraded, + }), + { maxServices: 3 } + ) + ).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '5 services are degraded: s1, s3, s4 and 2 other(s)', + detail: 'See the status page for more information', + meta: { + affectedServices: ['s1', 's3', 's4', 's5', 's7'], + }, }); }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 9124023148dd1..1dc939ce3f80c 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -10,11 +10,13 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' /** * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. - * @param statuses */ export const getSummaryStatus = ( statuses: Array<[string, ServiceStatus]>, - { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} + { + allAvailableSummary = `All services are available`, + maxServices = 3, + }: { allAvailableSummary?: string; maxServices?: number } = {} ): ServiceStatus => { const { highestLevel, highestStatuses } = highestLevelSummary(statuses); @@ -23,30 +25,38 @@ export const getSummaryStatus = ( level: ServiceStatusLevels.available, summary: allAvailableSummary, }; - } else if (highestStatuses.length === 1) { - const [serviceName, status] = highestStatuses[0]! as [string, ServiceStatus]; - return { - ...status, - summary: `[${serviceName}]: ${status.summary!}`, - // TODO: include URL to status page - detail: status.detail ?? `See the status page for more information`, - meta: { - affectedServices: [serviceName], - }, - }; } else { + const affectedServices = highestStatuses.map(([serviceName]) => serviceName); return { level: highestLevel, - summary: `[${highestStatuses.length}] services are ${highestLevel.toString()}`, + summary: getSummaryContent(affectedServices, highestLevel, maxServices), // TODO: include URL to status page detail: `See the status page for more information`, meta: { - affectedServices: highestStatuses.map(([serviceName]) => serviceName), + affectedServices, }, }; } }; +const getSummaryContent = ( + affectedServices: string[], + statusLevel: ServiceStatusLevel, + maxServices: number +): string => { + const serviceCount = affectedServices.length; + if (serviceCount === 1) { + return `1 service is ${statusLevel.toString()}: ${affectedServices[0]}`; + } else if (serviceCount > maxServices) { + const exceedingCount = serviceCount - maxServices; + return `${serviceCount} services are ${statusLevel.toString()}: ${affectedServices + .slice(0, maxServices) + .join(', ')} and ${exceedingCount} other(s)`; + } else { + return `${serviceCount} services are ${statusLevel.toString()}: ${affectedServices.join(', ')}`; + } +}; + type StatusPair = [string, ServiceStatus]; const highestLevelSummary = ( diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts index b7c0733de728e..0befbf63bd186 100644 --- a/src/core/server/status/plugins_status.test.ts +++ b/src/core/server/status/plugins_status.test.ts @@ -73,7 +73,7 @@ describe('PluginStatusService', () => { }); expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -84,7 +84,7 @@ describe('PluginStatusService', () => { }); expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', + summary: '1 service is critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -95,7 +95,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: savedObjects, a', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -106,7 +106,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.unavailable, - summary: '[a]: a is not working', + summary: '1 service is unavailable: a', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -120,7 +120,7 @@ describe('PluginStatusService', () => { service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', + summary: '1 service is critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -132,7 +132,7 @@ describe('PluginStatusService', () => { service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ level: ServiceStatusLevels.unavailable, - summary: '[b]: b is not working', + summary: '1 service is unavailable: b', detail: 'See the status page for more information', meta: expect.any(Object), }); @@ -166,19 +166,19 @@ describe('PluginStatusService', () => { expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ a: { level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, b: { level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -191,19 +191,19 @@ describe('PluginStatusService', () => { expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ a: { level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', + summary: '1 service is critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, b: { level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', + summary: '1 service is critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.critical, - summary: '[elasticsearch]: elasticsearch critical', + summary: '1 service is critical: elasticsearch', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -218,13 +218,13 @@ describe('PluginStatusService', () => { a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded b: { level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, c: { level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: savedObjects, b', detail: 'See the status page for more information', meta: expect.any(Object), }, @@ -298,7 +298,7 @@ describe('PluginStatusService', () => { a: { level: ServiceStatusLevels.unavailable, summary: 'Status check timed out after 30s' }, b: { level: ServiceStatusLevels.unavailable, - summary: '[a]: Status check timed out after 30s', + summary: '1 service is unavailable: a', detail: 'See the status page for more information', meta: { affectedServices: ['a'], @@ -341,7 +341,7 @@ describe('PluginStatusService', () => { a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded b: { level: ServiceStatusLevels.degraded, - summary: '[savedObjects]: savedObjects degraded', + summary: '1 service is degraded: savedObjects', detail: 'See the status page for more information', meta: expect.any(Object), }, diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 255ed821bc2fe..dfd0ff9a7e103 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -188,7 +188,7 @@ describe('StatusService', () => { ); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); }); @@ -208,15 +208,15 @@ describe('StatusService', () => { const subResult3 = await setup.overall$.pipe(first()).toPromise(); expect(subResult1).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); expect(subResult2).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); expect(subResult3).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); }); @@ -265,7 +265,7 @@ describe('StatusService', () => { "savedObjects", ], }, - "summary": "[savedObjects]: This is degraded!", + "summary": "1 service is degraded: savedObjects", }, Object { "level": available, @@ -315,7 +315,7 @@ describe('StatusService', () => { "savedObjects", ], }, - "summary": "[savedObjects]: This is degraded!", + "summary": "1 service is degraded: savedObjects", }, Object { "level": available, @@ -340,7 +340,7 @@ describe('StatusService', () => { ); expect(await setup.coreOverall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); }); @@ -357,7 +357,7 @@ describe('StatusService', () => { ); expect(await setup.coreOverall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.critical, - summary: '[savedObjects]: This is critical!', + summary: '1 service is critical: savedObjects', }); }); @@ -379,15 +379,15 @@ describe('StatusService', () => { expect(subResult1).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); expect(subResult2).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); expect(subResult3).toMatchObject({ level: ServiceStatusLevels.degraded, - summary: '[2] services are degraded', + summary: '2 services are degraded: elasticsearch, savedObjects', }); }); @@ -436,7 +436,7 @@ describe('StatusService', () => { "savedObjects", ], }, - "summary": "[savedObjects]: This is degraded!", + "summary": "1 service is degraded: savedObjects", }, Object { "level": available, @@ -486,7 +486,7 @@ describe('StatusService', () => { "savedObjects", ], }, - "summary": "[savedObjects]: This is degraded!", + "summary": "1 service is degraded: savedObjects", }, Object { "level": available, From 9d2c536ccb717bc439c957f6cea1b7c16f217529 Mon Sep 17 00:00:00 2001 From: Maja Grubic Date: Mon, 11 Oct 2021 11:49:20 +0200 Subject: [PATCH 005/287] [Discover] Unskip Painless date functional test (#114224) * [Discover] Unskip functional test * Remove comment Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- test/functional/apps/management/_scripted_fields.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/functional/apps/management/_scripted_fields.js b/test/functional/apps/management/_scripted_fields.js index 2e965c275d6dd..72f45e1fedb4d 100644 --- a/test/functional/apps/management/_scripted_fields.js +++ b/test/functional/apps/management/_scripted_fields.js @@ -367,8 +367,7 @@ export default function ({ getService, getPageObjects }) { }); }); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/113745 - describe.skip('creating and using Painless date scripted fields', function describeIndexTests() { + describe('creating and using Painless date scripted fields', function describeIndexTests() { const scriptedPainlessFieldName2 = 'painDate'; it('should create scripted field', async function () { @@ -384,7 +383,7 @@ export default function ({ getService, getPageObjects }) { 'date', { format: 'date', datePattern: 'YYYY-MM-DD HH:00' }, '1', - "doc['utc_time'].value.getMillis() + (1000) * 60 * 60" + "doc['utc_time'].value.toEpochMilli() + (1000) * 60 * 60" ); await retry.try(async function () { expect(parseInt(await PageObjects.settings.getScriptedFieldsTabCount())).to.be( From 1bf09e6930daa027b54f33a1477caa4d3af8310f Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 11 Oct 2021 12:32:19 +0200 Subject: [PATCH 006/287] [Lens] Thresholds: Add text to markers body (#113629) * :bug: Add padding to the tick label to fit threshold markers * :bug: Better icon detection * :bug: Fix edge cases with no title or labels * :camera_flash: Update snapshots * :sparkles: Add icon placement flag * :sparkles: Sync padding computation with marker positioning * :ok_hand: Make disabled when no icon is selected * :sparkles: First text on marker implementation * :bug: Fix some edge cases with auto positioning * Update x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx Co-authored-by: Michael Marcialis * :bug: Fix minor details * :lipstick: Small tweak * :sparkles: Reduce the padding if no icon is shown on the axis * :white_check_mark: Fix broken unit tests * :lipstick: Fix vertical text centering * :rotating_light: Fix linting issue * :bug: Fix issue * :lipstick: Reorder panel inputs * :lipstick: Move styling to sass * :ok_hand: Address feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Michael Marcialis --- .../expressions/xy_chart/axis_config.ts | 5 + .../dimension_panel/dimension_editor.tsx | 32 ++-- .../dimension_panel/dimension_panel.test.tsx | 20 ++- .../public/xy_visualization/expression.tsx | 1 + .../expression_thresholds.scss | 18 +++ .../expression_thresholds.tsx | 134 +++++++++++++---- .../public/xy_visualization/to_expression.ts | 10 +- .../xy_config_panel/threshold_panel.tsx | 137 +++++++++++------- 8 files changed, 248 insertions(+), 109 deletions(-) create mode 100644 x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts index 47bb1f91b4ab2..9ff1b5a4dc3f7 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts @@ -41,6 +41,7 @@ export interface YConfig { lineStyle?: LineStyle; fill?: FillStyle; iconPosition?: IconPosition; + textVisibility?: boolean; } export type AxisTitlesVisibilityConfigResult = AxesSettingsConfig & { @@ -187,6 +188,10 @@ export const yAxisConfig: ExpressionFunctionDefinition< options: ['auto', 'above', 'below', 'left', 'right'], help: 'The placement of the icon for the threshold line', }, + textVisibility: { + types: ['boolean'], + help: 'Visibility of the label on the threshold line', + }, fill: { types: ['string'], options: ['none', 'above', 'below'], diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 29bbe6a96b9e1..2f1c00bc5cca0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -107,16 +107,19 @@ export function DimensionEditor(props: DimensionEditorProps) { ); const setStateWrapper = ( - setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer) + setter: IndexPatternLayer | ((prevLayer: IndexPatternLayer) => IndexPatternLayer), + options: { forceRender?: boolean } = {} ) => { const hypotheticalLayer = typeof setter === 'function' ? setter(state.layers[layerId]) : setter; + const isDimensionComplete = Boolean(hypotheticalLayer.columns[columnId]); setState( (prevState) => { const layer = typeof setter === 'function' ? setter(prevState.layers[layerId]) : setter; return mergeLayer({ state: prevState, layerId, newLayer: layer }); }, { - isDimensionComplete: Boolean(hypotheticalLayer.columns[columnId]), + isDimensionComplete, + ...options, } ); }; @@ -169,20 +172,8 @@ export function DimensionEditor(props: DimensionEditorProps) { ) => { if (temporaryStaticValue) { setTemporaryState('none'); - if (typeof setter === 'function') { - return setState( - (prevState) => { - const layer = setter(addStaticValueColumn(prevState.layers[layerId])); - return mergeLayer({ state: prevState, layerId, newLayer: layer }); - }, - { - isDimensionComplete: true, - forceRender: true, - } - ); - } } - return setStateWrapper(setter); + return setStateWrapper(setter, { forceRender: true }); }; const ParamEditor = getParamEditor( @@ -314,7 +305,7 @@ export function DimensionEditor(props: DimensionEditorProps) { temporaryQuickFunction && isQuickFunction(newLayer.columns[columnId].operationType) ) { - // Only switch the tab once the formula is fully removed + // Only switch the tab once the "non quick function" is fully removed setTemporaryState('none'); } setStateWrapper(newLayer); @@ -344,13 +335,12 @@ export function DimensionEditor(props: DimensionEditorProps) { visualizationGroups: dimensionGroups, targetGroup: props.groupId, }); - // ); } if ( temporaryQuickFunction && isQuickFunction(newLayer.columns[columnId].operationType) ) { - // Only switch the tab once the formula is fully removed + // Only switch the tab once the "non quick function" is fully removed setTemporaryState('none'); } setStateWrapper(newLayer); @@ -508,6 +498,9 @@ export function DimensionEditor(props: DimensionEditorProps) { } incompleteOperation={incompleteOperation} onChoose={(choice) => { + if (temporaryQuickFunction) { + setTemporaryState('none'); + } setStateWrapper( insertOrReplaceColumn({ layer: state.layers[layerId], @@ -518,7 +511,8 @@ export function DimensionEditor(props: DimensionEditorProps) { visualizationGroups: dimensionGroups, targetGroup: props.groupId, incompleteParams, - }) + }), + { forceRender: temporaryQuickFunction } ); }} /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx index 6df4360aeac4c..6d9e1ae3fe81b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_panel.test.tsx @@ -513,7 +513,10 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { isDimensionComplete: true, forceRender: false }, + ]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...initialState, layers: { @@ -545,7 +548,10 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { isDimensionComplete: true, forceRender: false }, + ]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, layers: { @@ -1037,7 +1043,10 @@ describe('IndexPatternDimensionEditorPanel', () => { }); expect(setState.mock.calls.length).toEqual(2); - expect(setState.mock.calls[1]).toEqual([expect.any(Function), { isDimensionComplete: true }]); + expect(setState.mock.calls[1]).toEqual([ + expect.any(Function), + { isDimensionComplete: true, forceRender: false }, + ]); expect(setState.mock.calls[1][0](state)).toEqual({ ...state, layers: { @@ -1921,7 +1930,10 @@ describe('IndexPatternDimensionEditorPanel', () => { comboBox.prop('onChange')!([option]); }); - expect(setState.mock.calls[0]).toEqual([expect.any(Function), { isDimensionComplete: true }]); + expect(setState.mock.calls[0]).toEqual([ + expect.any(Function), + { isDimensionComplete: true, forceRender: false }, + ]); expect(setState.mock.calls[0][0](defaultProps.state)).toEqual({ ...state, layers: { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 484032e5ffbd9..5dfad58f50018 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -924,6 +924,7 @@ export function XYChart({ right: Boolean(yAxesMap.right), }} isHorizontal={shouldRotate} + thresholdPaddingMap={thresholdPaddings} /> ) : null} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss new file mode 100644 index 0000000000000..41b30ce40676b --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.scss @@ -0,0 +1,18 @@ +.lnsXyDecorationRotatedWrapper { + display: inline-block; + overflow: hidden; + line-height: $euiLineHeight; + + .lnsXyDecorationRotatedWrapper__label { + display: inline-block; + white-space: nowrap; + transform: translate(0, 100%) rotate(-90deg); + transform-origin: 0 0; + + &::after { + content: ''; + float: left; + margin-top: 100%; + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx index 7532d41f091d1..67e994b734b84 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx @@ -5,6 +5,7 @@ * 2.0. */ +import './expression_thresholds.scss'; import React from 'react'; import { groupBy } from 'lodash'; import { EuiIcon } from '@elastic/eui'; @@ -14,8 +15,9 @@ import type { FieldFormat } from 'src/plugins/field_formats/common'; import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import type { LayerArgs, YConfig } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; +import { hasIcon } from './xy_config_panel/threshold_panel'; -const THRESHOLD_ICON_SIZE = 20; +const THRESHOLD_MARKER_SIZE = 20; export const computeChartMargins = ( thresholdPaddings: Partial>, @@ -51,27 +53,35 @@ export const computeChartMargins = ( return result; }; -function hasIcon(icon: string | undefined): icon is string { - return icon != null && icon !== 'none'; -} - // Note: it does not take into consideration whether the threshold is in view or not export const getThresholdRequiredPaddings = ( thresholdLayers: LayerArgs[], axesMap: Record<'left' | 'right', unknown> ) => { - const positions = Object.keys(Position); - return thresholdLayers.reduce((memo, layer) => { - if (positions.some((pos) => !(pos in memo))) { - layer.yConfig?.forEach(({ axisMode, icon, iconPosition }) => { - if (axisMode && hasIcon(icon)) { - const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); - memo[placement] = THRESHOLD_ICON_SIZE; - } - }); + // collect all paddings for the 4 axis: if any text is detected double it. + const paddings: Partial> = {}; + const icons: Partial> = {}; + thresholdLayers.forEach((layer) => { + layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => { + if (axisMode && (hasIcon(icon) || textVisibility)) { + const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); + paddings[placement] = Math.max( + paddings[placement] || 0, + THRESHOLD_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text + ); + icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); + } + }); + }); + // post-process the padding based on the icon presence: + // if no icon is present for the placement, just reduce the padding + (Object.keys(paddings) as Position[]).forEach((placement) => { + if (!icons[placement]) { + paddings[placement] = THRESHOLD_MARKER_SIZE; } - return memo; - }, {} as Partial>); + }); + + return paddings; }; function mapVerticalToHorizontalPlacement(placement: Position) { @@ -117,17 +127,57 @@ function getBaseIconPlacement( return Position.Top; } -function getIconPlacement( - iconPosition: YConfig['iconPosition'], - axisMode: YConfig['axisMode'], - axesMap: Record, - isHorizontal: boolean -) { - const vPosition = getBaseIconPlacement(iconPosition, axisMode, axesMap); +function getMarkerBody(label: string | undefined, isHorizontal: boolean) { + if (!label) { + return; + } if (isHorizontal) { - return mapVerticalToHorizontalPlacement(vPosition); + return ( +
+ {label} +
+ ); + } + return ( +
+
+ {label} +
+
+ ); +} + +function getMarkerToShow( + yConfig: YConfig, + label: string | undefined, + isHorizontal: boolean, + hasReducedPadding: boolean +) { + // show an icon if present + if (hasIcon(yConfig.icon)) { + return ; + } + // if there's some text, check whether to show it as marker, or just show some padding for the icon + if (yConfig.textVisibility) { + if (hasReducedPadding) { + return getMarkerBody( + label, + (!isHorizontal && yConfig.axisMode === 'bottom') || + (isHorizontal && yConfig.axisMode !== 'bottom') + ); + } + return ; } - return vPosition; } export const ThresholdAnnotations = ({ @@ -138,6 +188,7 @@ export const ThresholdAnnotations = ({ syncColors, axesMap, isHorizontal, + thresholdPaddingMap, }: { thresholdLayers: LayerArgs[]; data: LensMultiTable; @@ -146,6 +197,7 @@ export const ThresholdAnnotations = ({ syncColors: boolean; axesMap: Record<'left' | 'right', boolean>; isHorizontal: boolean; + thresholdPaddingMap: Partial>; }) => { return ( <> @@ -180,15 +232,35 @@ export const ThresholdAnnotations = ({ const defaultColor = euiLightVars.euiColorDarkShade; + // get the position for vertical chart + const markerPositionVertical = getBaseIconPlacement( + yConfig.iconPosition, + yConfig.axisMode, + axesMap + ); + // the padding map is built for vertical chart + const hasReducedPadding = + thresholdPaddingMap[markerPositionVertical] === THRESHOLD_MARKER_SIZE; + const props = { groupId, - marker: hasIcon(yConfig.icon) ? : undefined, - markerPosition: getIconPlacement( - yConfig.iconPosition, - yConfig.axisMode, - axesMap, - isHorizontal + marker: getMarkerToShow( + yConfig, + columnToLabelMap[yConfig.forAccessor], + isHorizontal, + hasReducedPadding + ), + markerBody: getMarkerBody( + yConfig.textVisibility && !hasReducedPadding + ? columnToLabelMap[yConfig.forAccessor] + : undefined, + (!isHorizontal && yConfig.axisMode === 'bottom') || + (isHorizontal && yConfig.axisMode !== 'bottom') ), + // rotate the position if required + markerPosition: isHorizontal + ? mapVerticalToHorizontalPlacement(markerPositionVertical) + : markerPositionVertical, }; const annotations = []; diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index bb65b69a8d121..96ea9b84dd983 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -13,6 +13,7 @@ import { OperationMetadata, DatasourcePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import type { ValidLayer, XYLayerConfig } from '../../common/expressions'; import { layerTypes } from '../../common'; +import { hasIcon } from './xy_config_panel/threshold_panel'; import { defaultThresholdColor } from './color_assignment'; export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: XYLayerConfig) => { @@ -66,6 +67,7 @@ export function toPreviewExpression( ...config, lineWidth: 1, icon: undefined, + textVisibility: false, })), } ), @@ -344,8 +346,12 @@ export const buildExpression = ( lineStyle: [yConfig.lineStyle || 'solid'], lineWidth: [yConfig.lineWidth || 1], fill: [yConfig.fill || 'none'], - icon: yConfig.icon ? [yConfig.icon] : [], - iconPosition: [yConfig.iconPosition || 'auto'], + icon: hasIcon(yConfig.icon) ? [yConfig.icon] : [], + iconPosition: + hasIcon(yConfig.icon) || yConfig.textVisibility + ? [yConfig.iconPosition || 'auto'] + : ['auto'], + textVisibility: [yConfig.textVisibility || false], }, }, ], diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx index cdf5bb2cc2ef1..7c31d72e6cbde 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx @@ -8,7 +8,14 @@ import './xy_config_panel.scss'; import React, { useCallback, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiButtonGroup, EuiComboBox, EuiFormRow, EuiIcon, EuiRange } from '@elastic/eui'; +import { + EuiButtonGroup, + EuiComboBox, + EuiFormRow, + EuiIcon, + EuiRange, + EuiSwitch, +} from '@elastic/eui'; import type { PaletteRegistry } from 'src/plugins/charts/public'; import type { VisualizationDimensionEditorProps } from '../../types'; import { State, XYState } from '../types'; @@ -177,6 +184,10 @@ function getIconPositionOptions({ return [...options, ...yOptions]; } +export function hasIcon(icon: string | undefined): icon is string { + return icon != null && icon !== 'none'; +} + export const ThresholdPanel = ( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; @@ -220,6 +231,78 @@ export const ThresholdPanel = ( return ( <> + + { + setYConfig({ forAccessor: accessor, textVisibility: !currentYConfig?.textVisibility }); + }} + /> + + + { + setYConfig({ forAccessor: accessor, icon: newIcon }); + }} + /> + + + + { + const newMode = id.replace(idPrefix, '') as IconPosition; + setYConfig({ forAccessor: accessor, iconPosition: newMode }); + }} + /> + + - - { - setYConfig({ forAccessor: accessor, icon: newIcon }); - }} - /> - - - - { - const newMode = id.replace(idPrefix, '') as IconPosition; - setYConfig({ forAccessor: accessor, iconPosition: newMode }); - }} - /> - - ); }; From 9a1779d364044dff27672c110f6aa344e61fce15 Mon Sep 17 00:00:00 2001 From: Stratoula Kalafateli Date: Mon, 11 Oct 2021 13:37:01 +0300 Subject: [PATCH 007/287] [Visualize] unskip the reporting funtional test (#114094) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/test/functional/apps/visualize/reporting.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/x-pack/test/functional/apps/visualize/reporting.ts b/x-pack/test/functional/apps/visualize/reporting.ts index efffa0b6a692b..07ce3d9b23128 100644 --- a/x-pack/test/functional/apps/visualize/reporting.ts +++ b/x-pack/test/functional/apps/visualize/reporting.ts @@ -25,13 +25,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'visEditor', ]); - // Failing: See https://github.com/elastic/kibana/issues/113496 - describe.skip('Visualize Reporting Screenshots', () => { + describe('Visualize Reporting Screenshots', () => { before('initialize tests', async () => { log.debug('ReportingPage:initTests'); await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/reporting/ecommerce'); await kibanaServer.importExport.load(ecommerceSOPath); await browser.setWindowSize(1600, 850); + await kibanaServer.uiSettings.replace({ + 'timepicker:timeDefaults': + '{ "from": "2019-04-27T23:56:51.374Z", "to": "2019-08-23T16:18:51.821Z"}', + }); }); after('clean up archives', async () => { await esArchiver.unload('x-pack/test/functional/es_archives/reporting/ecommerce'); @@ -41,6 +44,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { refresh: true, body: { query: { match_all: {} } }, }); + await kibanaServer.uiSettings.unset('timepicker:timeDefaults'); }); describe('Print PDF button', () => { @@ -54,11 +58,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); it('becomes available when saved', async () => { - await PageObjects.timePicker.timePickerExists(); - const fromTime = 'Apr 27, 2019 @ 23:56:51.374'; - const toTime = 'Aug 23, 2019 @ 16:18:51.821'; - await PageObjects.timePicker.setAbsoluteRange(fromTime, toTime); - await PageObjects.visEditor.clickBucket('X-axis'); await PageObjects.visEditor.selectAggregation('Date Histogram'); await PageObjects.visEditor.clickGo(); From b2108f4c2c939f7f9ea8f0cf272c6f856d260b0e Mon Sep 17 00:00:00 2001 From: Giorgos Bamparopoulos Date: Mon, 11 Oct 2021 11:53:49 +0100 Subject: [PATCH 008/287] Add all APM configuration settings to the documentation (#114139) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add APM configuration settings to the documentation * Rename the deprecated apm_oss.* configurations to xpack.apm.* * Remove new lines * Add ess icon to config settings * Add link to the APM configuration settings docs Co-authored-by: Søren Louv-Jansen Co-authored-by: Søren Louv-Jansen Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/settings/apm-settings.asciidoc | 48 +++++++++++++++++++---------- x-pack/plugins/apm/server/index.ts | 2 ++ 2 files changed, 33 insertions(+), 17 deletions(-) diff --git a/docs/settings/apm-settings.asciidoc b/docs/settings/apm-settings.asciidoc index e565bda0dff47..fb96f68355330 100644 --- a/docs/settings/apm-settings.asciidoc +++ b/docs/settings/apm-settings.asciidoc @@ -40,57 +40,71 @@ Changing these settings may disable features of the APM App. [cols="2*<"] |=== -| `xpack.apm.enabled` +| `xpack.apm.enabled` {ess-icon} | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] Set to `false` to disable the APM app. Defaults to `true`. -| `xpack.apm.maxServiceEnvironments` +| `xpack.apm.maxServiceEnvironments` {ess-icon} | Maximum number of unique service environments recognized by the UI. Defaults to `100`. -| `xpack.apm.serviceMapFingerprintBucketSize` +| `xpack.apm.serviceMapFingerprintBucketSize` {ess-icon} | Maximum number of unique transaction combinations sampled for generating service map focused on a specific service. Defaults to `100`. -| `xpack.apm.serviceMapFingerprintGlobalBucketSize` +| `xpack.apm.serviceMapFingerprintGlobalBucketSize` {ess-icon} | Maximum number of unique transaction combinations sampled for generating the global service map. Defaults to `100`. +| `xpack.apm.serviceMapEnabled` {ess-icon} + | Set to `false` to disable service maps. Defaults to `true`. + +| `xpack.apm.serviceMapTraceIdBucketSize` {ess-icon} + | Maximum number of trace IDs sampled for generating service map focused on a specific service. Defaults to `65`. + +| `xpack.apm.serviceMapTraceIdGlobalBucketSize` {ess-icon} + | Maximum number of trace IDs sampled for generating the global service map. Defaults to `6`. + +| `xpack.apm.serviceMapMaxTracesPerRequest` {ess-icon} + | Maximum number of traces per request for generating the global service map. Defaults to `50`. + | `xpack.apm.ui.enabled` {ess-icon} | Set to `false` to hide the APM app from the main menu. Defaults to `true`. -| `xpack.apm.ui.transactionGroupBucketSize` +| `xpack.apm.ui.transactionGroupBucketSize` {ess-icon} | Number of top transaction groups displayed in the APM app. Defaults to `1000`. | `xpack.apm.ui.maxTraceItems` {ess-icon} | Maximum number of child items displayed when viewing trace details. Defaults to `1000`. -| `xpack.observability.annotations.index` +| `xpack.observability.annotations.index` {ess-icon} | Index name where Observability annotations are stored. Defaults to `observability-annotations`. -| `xpack.apm.searchAggregatedTransactions` +| `xpack.apm.searchAggregatedTransactions` {ess-icon} | experimental[] Enables Transaction histogram metrics. Defaults to `never` and aggregated transactions are not used. When set to `auto`, the UI will use metric indices over transaction indices for transactions if aggregated transactions are found. When set to `always`, additional configuration in APM Server is required. See {apm-server-ref-v}/transaction-metrics.html[Configure transaction metrics] for more information. -| `apm_oss.indexPattern` {ess-icon} - | The index pattern used for integrations with Machine Learning and Query Bar. - It must match all apm indices. Defaults to `apm-*`. +| `xpack.apm.metricsInterval` {ess-icon} + | Sets a `fixed_interval` for date histograms in metrics aggregations. Defaults to `30`. + +| `xpack.apm.agent.migrations.enabled` {ess-icon} + | Set to `false` to disable cloud APM migrations. Defaults to `true`. -| `apm_oss.errorIndices` {ess-icon} +| `xpack.apm.errorIndices` {ess-icon} | Matcher for all {apm-server-ref}/error-indices.html[error indices]. Defaults to `apm-*`. -| `apm_oss.onboardingIndices` +| `xpack.apm.onboardingIndices` {ess-icon} | Matcher for all onboarding indices. Defaults to `apm-*`. -| `apm_oss.spanIndices` {ess-icon} +| `xpack.apm.spanIndices` {ess-icon} | Matcher for all {apm-server-ref}/span-indices.html[span indices]. Defaults to `apm-*`. -| `apm_oss.transactionIndices` {ess-icon} +| `xpack.apm.transactionIndices` {ess-icon} | Matcher for all {apm-server-ref}/transaction-indices.html[transaction indices]. Defaults to `apm-*`. -| `apm_oss.metricsIndices` +| `xpack.apm.metricsIndices` {ess-icon} | Matcher for all {apm-server-ref}/metricset-indices.html[metrics indices]. Defaults to `apm-*`. -| `apm_oss.sourcemapIndices` +| `xpack.apm.sourcemapIndices` {ess-icon} | Matcher for all {apm-server-ref}/sourcemap-indices.html[source map indices]. Defaults to `apm-*`. |=== -// end::general-apm-settings[] +// end::general-apm-settings[] \ No newline at end of file diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b7002ff7cbe79..22787b0301ce0 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -74,6 +74,8 @@ export type APMXPackConfig = TypeOf; export type APMConfig = ReturnType; // plugin config and ui indices settings +// All options should be documented in the APM configuration settings: https://github.com/elastic/kibana/blob/master/docs/settings/apm-settings.asciidoc +// and be included on cloud allow list unless there are specific reasons not to export function mergeConfigs( apmOssConfig: APMOSSConfig, apmConfig: APMXPackConfig From 75983cf45065fc498feb150f4be8e95b6b85886f Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Mon, 11 Oct 2021 12:58:59 +0200 Subject: [PATCH 009/287] [Reporting] Update chromium exit behaviour (#113544) * move uncaught exception out of exit$ * reintroduce original error, but as a log instead * change log level: error -> warning. also update copy to make it explicit that the error will be ignored Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../browsers/chromium/driver_factory/index.ts | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 2170b50f195b4..a0487421a9a0d 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -220,6 +220,17 @@ export class HeadlessChromiumDriverFactory { }) ); + const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( + map((err) => { + logger.warning( + i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { + defaultMessage: `Reporting encountered an uncaught error on the page that will be ignored: {err}`, + values: { err: err.toString() }, + }) + ); + }) + ); + const pageRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( map((req) => { const failure = req.failure && req.failure(); @@ -231,7 +242,7 @@ export class HeadlessChromiumDriverFactory { }) ); - return Rx.merge(consoleMessages$, pageRequestFailed$); + return Rx.merge(consoleMessages$, uncaughtExceptionPageError$, pageRequestFailed$); } getProcessLogger(browser: puppeteer.Browser, logger: LevelLogger): Rx.Observable { @@ -266,21 +277,10 @@ export class HeadlessChromiumDriverFactory { }) ); - const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( - mergeMap((err) => { - return Rx.throwError( - i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { - defaultMessage: `Reporting encountered an error on the page: {err}`, - values: { err: err.toString() }, - }) - ); - }) - ); - const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) ); - return Rx.merge(pageError$, uncaughtExceptionPageError$, browserDisconnect$); + return Rx.merge(pageError$, browserDisconnect$); } } From ce7b1ea6530653ddb910ca57af00d3503f8e3362 Mon Sep 17 00:00:00 2001 From: Dmitry Shevchenko Date: Mon, 11 Oct 2021 13:05:52 +0200 Subject: [PATCH 010/287] Implement writing rule execution events to event_log (#112286) --- .../plugins/event_log/generated/mappings.json | 43 +++ x-pack/plugins/event_log/generated/schemas.ts | 23 ++ x-pack/plugins/event_log/scripts/mappings.js | 48 +++- x-pack/plugins/security_solution/kibana.json | 5 +- .../security_solution/server/config.ts | 14 + .../routes/__mocks__/index.ts | 4 + .../__mocks__/rule_execution_log_client.ts | 2 +- .../event_log_adapter/constants.ts | 15 + .../event_log_adapter/event_log_adapter.ts | 87 ++++++ .../event_log_adapter/event_log_client.ts | 157 ++++++++++ .../register_event_log_provider.ts | 16 ++ .../rule_execution_log_client.ts | 34 ++- .../rule_registry_adapter.ts | 106 ------- .../rule_registry_log_client/constants.ts | 41 --- .../parse_rule_execution_log.ts | 40 --- .../rule_execution_field_map.ts | 32 --- .../rule_registry_log_client.ts | 270 ------------------ .../rule_registry_log_client/utils.ts | 76 ----- .../saved_objects_adapter.ts | 35 ++- .../rule_execution_log/types.ts | 55 ++-- .../rule_types/__mocks__/rule_type.ts | 4 +- .../create_security_rule_type_factory.ts | 49 ++-- .../eql/create_eql_alert_type.test.ts | 6 +- .../rule_types/eql/create_eql_alert_type.ts | 17 +- .../build_alert_group_from_sequence.test.ts | 1 + .../create_indicator_match_alert_type.test.ts | 16 +- .../create_indicator_match_alert_type.ts | 17 +- .../ml/create_ml_alert_type.test.ts | 6 +- .../rule_types/ml/create_ml_alert_type.ts | 8 +- .../query/create_query_alert_type.test.ts | 11 +- .../query/create_query_alert_type.ts | 17 +- .../create_threshold_alert_type.test.ts | 6 +- .../threshold/create_threshold_alert_type.ts | 17 +- .../lib/detection_engine/rule_types/types.ts | 12 +- .../lib/detection_engine/rules/enable_rule.ts | 2 + .../signals/__mocks__/es_results.ts | 1 + .../signals/executors/eql.test.ts | 1 + .../signals/executors/threshold.test.ts | 1 + .../signals/signal_rule_alert_type.test.ts | 23 +- .../signals/signal_rule_alert_type.ts | 63 ++-- .../lib/detection_engine/signals/types.ts | 1 + .../detection_engine/signals/utils.test.ts | 4 + .../lib/detection_engine/signals/utils.ts | 23 +- .../security_solution/server/plugin.ts | 26 +- .../event_log/service_api_integration.ts | 15 + 45 files changed, 653 insertions(+), 797 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts create mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts diff --git a/x-pack/plugins/event_log/generated/mappings.json b/x-pack/plugins/event_log/generated/mappings.json index cbb59cc3204c0..aba23eef79e3f 100644 --- a/x-pack/plugins/event_log/generated/mappings.json +++ b/x-pack/plugins/event_log/generated/mappings.json @@ -267,6 +267,42 @@ } } }, + "alert": { + "properties": { + "rule": { + "properties": { + "execution": { + "properties": { + "uuid": { + "type": "keyword", + "ignore_above": 1024 + }, + "status": { + "type": "keyword", + "ignore_above": 1024 + }, + "status_order": { + "type": "long" + }, + "metrics": { + "properties": { + "total_indexing_duration_ms": { + "type": "long" + }, + "total_search_duration_ms": { + "type": "long" + }, + "execution_gap_duration_s": { + "type": "long" + } + } + } + } + } + } + } + } + }, "saved_objects": { "type": "nested", "properties": { @@ -292,6 +328,13 @@ } } }, + "space_ids": { + "type": "keyword", + "ignore_above": 1024, + "meta": { + "isArray": "true" + } + }, "version": { "type": "version" } diff --git a/x-pack/plugins/event_log/generated/schemas.ts b/x-pack/plugins/event_log/generated/schemas.ts index 15dc182dbe653..e73bafd9cb81e 100644 --- a/x-pack/plugins/event_log/generated/schemas.ts +++ b/x-pack/plugins/event_log/generated/schemas.ts @@ -116,6 +116,28 @@ export const EventSchema = schema.maybe( status: ecsString(), }) ), + alert: schema.maybe( + schema.object({ + rule: schema.maybe( + schema.object({ + execution: schema.maybe( + schema.object({ + uuid: ecsString(), + status: ecsString(), + status_order: ecsNumber(), + metrics: schema.maybe( + schema.object({ + total_indexing_duration_ms: ecsNumber(), + total_search_duration_ms: ecsNumber(), + execution_gap_duration_s: ecsNumber(), + }) + ), + }) + ), + }) + ), + }) + ), saved_objects: schema.maybe( schema.arrayOf( schema.object({ @@ -127,6 +149,7 @@ export const EventSchema = schema.maybe( }) ) ), + space_ids: ecsStringMulti(), version: ecsVersion(), }) ), diff --git a/x-pack/plugins/event_log/scripts/mappings.js b/x-pack/plugins/event_log/scripts/mappings.js index d114603052491..231cc225f7c47 100644 --- a/x-pack/plugins/event_log/scripts/mappings.js +++ b/x-pack/plugins/event_log/scripts/mappings.js @@ -49,6 +49,42 @@ exports.EcsCustomPropertyMappings = { }, }, }, + alert: { + properties: { + rule: { + properties: { + execution: { + properties: { + uuid: { + type: 'keyword', + ignore_above: 1024, + }, + status: { + type: 'keyword', + ignore_above: 1024, + }, + status_order: { + type: 'long', + }, + metrics: { + properties: { + total_indexing_duration_ms: { + type: 'long', + }, + total_search_duration_ms: { + type: 'long', + }, + execution_gap_duration_s: { + type: 'long', + }, + }, + }, + }, + }, + }, + }, + }, + }, // array of saved object references, for "linking" via search saved_objects: { type: 'nested', @@ -77,6 +113,10 @@ exports.EcsCustomPropertyMappings = { }, }, }, + space_ids: { + type: 'keyword', + ignore_above: 1024, + }, version: { type: 'version', }, @@ -105,4 +145,10 @@ exports.EcsPropertiesToGenerate = [ /** * These properties can have multiple values (are arrays in the generated event schema). */ -exports.EcsEventLogMultiValuedProperties = ['tags', 'event.category', 'event.type', 'rule.author']; +exports.EcsEventLogMultiValuedProperties = [ + 'tags', + 'event.category', + 'event.type', + 'rule.author', + 'kibana.space_ids', +]; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index 8bb1f4d75e6bc..a76b942e555bc 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -12,15 +12,16 @@ "actions", "alerting", "cases", - "ruleRegistry", "data", "dataEnhanced", "embeddable", + "eventLog", "features", - "taskManager", "inspector", "licensing", "maps", + "ruleRegistry", + "taskManager", "timelines", "triggersActionsUi", "uiActions" diff --git a/x-pack/plugins/security_solution/server/config.ts b/x-pack/plugins/security_solution/server/config.ts index 0850e43b21eda..bc5b43c6d25fd 100644 --- a/x-pack/plugins/security_solution/server/config.ts +++ b/x-pack/plugins/security_solution/server/config.ts @@ -12,6 +12,7 @@ import { getExperimentalAllowedValues, isValidExperimentalValue, } from '../common/experimental_features'; +import { UnderlyingLogClient } from './lib/detection_engine/rule_execution_log/types'; const allowedExperimentalValues = getExperimentalAllowedValues(); @@ -103,6 +104,19 @@ export const configSchema = schema.object({ }, }), + /** + * Rule Execution Log Configuration + */ + ruleExecutionLog: schema.object({ + underlyingClient: schema.oneOf( + [ + schema.literal(UnderlyingLogClient.eventLog), + schema.literal(UnderlyingLogClient.savedObjects), + ], + { defaultValue: UnderlyingLogClient.savedObjects } + ), + }), + /** * Host Endpoint Configuration */ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts index 1ac85f9a27969..2f401d27813ac 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/index.ts @@ -11,6 +11,7 @@ import { serverMock } from './server'; import { requestMock } from './request'; import { responseMock } from './response_factory'; import { ConfigType } from '../../../../config'; +import { UnderlyingLogClient } from '../../rule_execution_log/types'; export { requestMock, requestContextMock, responseMock, serverMock }; @@ -29,6 +30,9 @@ export const createMockConfig = (): ConfigType => ({ alertIgnoreFields: [], prebuiltRulesFromFileSystem: true, prebuiltRulesFromSavedObjects: false, + ruleExecutionLog: { + underlyingClient: UnderlyingLogClient.savedObjects, + }, }); export const mockGetCurrentUser = { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts index bc9723e47a9d0..910e1ecaa508f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/__mocks__/rule_execution_log_client.ts @@ -14,7 +14,7 @@ export const ruleExecutionLogClientMock = { update: jest.fn(), delete: jest.fn(), logStatusChange: jest.fn(), - logExecutionMetric: jest.fn(), + logExecutionMetrics: jest.fn(), }), }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.ts new file mode 100644 index 0000000000000..f09eb43bf15f1 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/constants.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. + */ + +export const RULE_EXECUTION_LOG_PROVIDER = 'rule-execution.security'; + +export const ALERT_SAVED_OBJECT_TYPE = 'alert'; + +export enum RuleExecutionLogAction { + 'status-change' = 'status-change', + 'execution-metrics' = 'execution-metrics', +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts new file mode 100644 index 0000000000000..6b1a0cd5b18d0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_adapter.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IEventLogService } from '../../../../../../event_log/server'; +import { + FindBulkExecutionLogArgs, + FindExecutionLogArgs, + IRuleExecutionLogClient, + LogExecutionMetricsArgs, + LogStatusChangeArgs, + UpdateExecutionLogArgs, +} from '../types'; +import { EventLogClient } from './event_log_client'; + +export class EventLogAdapter implements IRuleExecutionLogClient { + private eventLogClient: EventLogClient; + + constructor(eventLogService: IEventLogService) { + this.eventLogClient = new EventLogClient(eventLogService); + } + + public async find({ ruleId, logsCount = 1, spaceId }: FindExecutionLogArgs) { + return []; // TODO Implement + } + + public async findBulk({ ruleIds, logsCount = 1, spaceId }: FindBulkExecutionLogArgs) { + return {}; // TODO Implement + } + + public async update({ attributes, spaceId, ruleName, ruleType }: UpdateExecutionLogArgs) { + // execution events are immutable, so we just log a status change istead of updating previous + if (attributes.status) { + this.eventLogClient.logStatusChange({ + ruleName, + ruleType, + ruleId: attributes.alertId, + newStatus: attributes.status, + spaceId, + }); + } + } + + public async delete(id: string) { + // execution events are immutable, nothing to do here + } + + public async logExecutionMetrics({ + ruleId, + spaceId, + ruleType, + ruleName, + metrics, + }: LogExecutionMetricsArgs) { + this.eventLogClient.logExecutionMetrics({ + ruleId, + ruleName, + ruleType, + spaceId, + metrics: { + executionGapDuration: metrics.executionGap?.asSeconds(), + totalIndexingDuration: metrics.indexingDurations?.reduce( + (acc, cur) => acc + Number(cur), + 0 + ), + totalSearchDuration: metrics.searchDurations?.reduce((acc, cur) => acc + Number(cur), 0), + }, + }); + } + + public async logStatusChange(args: LogStatusChangeArgs) { + if (args.metrics) { + this.logExecutionMetrics({ + ruleId: args.ruleId, + ruleName: args.ruleName, + ruleType: args.ruleType, + spaceId: args.spaceId, + metrics: args.metrics, + }); + } + + this.eventLogClient.logStatusChange(args); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts new file mode 100644 index 0000000000000..d85c67e422035 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/event_log_client.ts @@ -0,0 +1,157 @@ +/* + * 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 { SavedObjectsUtils } from '../../../../../../../../src/core/server'; +import { + IEventLogger, + IEventLogService, + SAVED_OBJECT_REL_PRIMARY, +} from '../../../../../../event_log/server'; +import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; +import { LogStatusChangeArgs } from '../types'; +import { + RuleExecutionLogAction, + RULE_EXECUTION_LOG_PROVIDER, + ALERT_SAVED_OBJECT_TYPE, +} from './constants'; + +const spaceIdToNamespace = SavedObjectsUtils.namespaceStringToId; + +const statusSeverityDict: Record = { + [RuleExecutionStatus.succeeded]: 0, + [RuleExecutionStatus['going to run']]: 10, + [RuleExecutionStatus.warning]: 20, + [RuleExecutionStatus['partial failure']]: 20, + [RuleExecutionStatus.failed]: 30, +}; + +interface FindExecutionLogArgs { + ruleIds: string[]; + spaceId: string; + logsCount?: number; + statuses?: RuleExecutionStatus[]; +} + +interface LogExecutionMetricsArgs { + ruleId: string; + ruleName: string; + ruleType: string; + spaceId: string; + metrics: EventLogExecutionMetrics; +} + +interface EventLogExecutionMetrics { + totalSearchDuration?: number; + totalIndexingDuration?: number; + executionGapDuration?: number; +} + +interface IExecLogEventLogClient { + find: (args: FindExecutionLogArgs) => Promise<{}>; + logStatusChange: (args: LogStatusChangeArgs) => void; + logExecutionMetrics: (args: LogExecutionMetricsArgs) => void; +} + +export class EventLogClient implements IExecLogEventLogClient { + private sequence = 0; + private eventLogger: IEventLogger; + + constructor(eventLogService: IEventLogService) { + this.eventLogger = eventLogService.getLogger({ + event: { provider: RULE_EXECUTION_LOG_PROVIDER }, + }); + } + + public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) { + return {}; // TODO implement + } + + public logExecutionMetrics({ + ruleId, + ruleName, + ruleType, + metrics, + spaceId, + }: LogExecutionMetricsArgs) { + this.eventLogger.logEvent({ + rule: { + id: ruleId, + name: ruleName, + category: ruleType, + }, + event: { + kind: 'metric', + action: RuleExecutionLogAction['execution-metrics'], + sequence: this.sequence++, + }, + kibana: { + alert: { + rule: { + execution: { + metrics: { + execution_gap_duration_s: metrics.executionGapDuration, + total_search_duration_ms: metrics.totalSearchDuration, + total_indexing_duration_ms: metrics.totalIndexingDuration, + }, + }, + }, + }, + space_ids: [spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: ALERT_SAVED_OBJECT_TYPE, + id: ruleId, + namespace: spaceIdToNamespace(spaceId), + }, + ], + }, + }); + } + + public logStatusChange({ + ruleId, + ruleName, + ruleType, + newStatus, + message, + spaceId, + }: LogStatusChangeArgs) { + this.eventLogger.logEvent({ + rule: { + id: ruleId, + name: ruleName, + category: ruleType, + }, + event: { + kind: 'event', + action: RuleExecutionLogAction['status-change'], + sequence: this.sequence++, + }, + message, + kibana: { + alert: { + rule: { + execution: { + status: newStatus, + status_order: statusSeverityDict[newStatus], + }, + }, + }, + space_ids: [spaceId], + saved_objects: [ + { + rel: SAVED_OBJECT_REL_PRIMARY, + type: ALERT_SAVED_OBJECT_TYPE, + id: ruleId, + namespace: spaceIdToNamespace(spaceId), + }, + ], + }, + }); + } +} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.ts new file mode 100644 index 0000000000000..7f28715198da6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider.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 { IEventLogService } from '../../../../../../event_log/server'; +import { RuleExecutionLogAction, RULE_EXECUTION_LOG_PROVIDER } from './constants'; + +export const registerEventLogProvider = (eventLogService: IEventLogService) => { + eventLogService.registerProviderActions( + RULE_EXECUTION_LOG_PROVIDER, + Object.keys(RuleExecutionLogAction) + ); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts index 135cefe2243b2..87a3b00cf4ed3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_execution_log_client.ts @@ -6,34 +6,40 @@ */ import { SavedObjectsClientContract } from '../../../../../../../src/core/server'; -import { RuleRegistryAdapter } from './rule_registry_adapter/rule_registry_adapter'; +import { IEventLogService } from '../../../../../event_log/server'; +import { EventLogAdapter } from './event_log_adapter/event_log_adapter'; import { SavedObjectsAdapter } from './saved_objects_adapter/saved_objects_adapter'; import { - ExecutionMetric, - ExecutionMetricArgs, + LogExecutionMetricsArgs, FindBulkExecutionLogArgs, FindExecutionLogArgs, - IRuleDataPluginService, IRuleExecutionLogClient, LogStatusChangeArgs, UpdateExecutionLogArgs, + UnderlyingLogClient, } from './types'; export interface RuleExecutionLogClientArgs { - ruleDataService: IRuleDataPluginService; savedObjectsClient: SavedObjectsClientContract; + eventLogService: IEventLogService; + underlyingClient: UnderlyingLogClient; } -const RULE_REGISTRY_LOG_ENABLED = false; - export class RuleExecutionLogClient implements IRuleExecutionLogClient { private client: IRuleExecutionLogClient; - constructor({ ruleDataService, savedObjectsClient }: RuleExecutionLogClientArgs) { - if (RULE_REGISTRY_LOG_ENABLED) { - this.client = new RuleRegistryAdapter(ruleDataService); - } else { - this.client = new SavedObjectsAdapter(savedObjectsClient); + constructor({ + savedObjectsClient, + eventLogService, + underlyingClient, + }: RuleExecutionLogClientArgs) { + switch (underlyingClient) { + case UnderlyingLogClient.savedObjects: + this.client = new SavedObjectsAdapter(savedObjectsClient); + break; + case UnderlyingLogClient.eventLog: + this.client = new EventLogAdapter(eventLogService); + break; } } @@ -53,8 +59,8 @@ export class RuleExecutionLogClient implements IRuleExecutionLogClient { return this.client.delete(id); } - public async logExecutionMetric(args: ExecutionMetricArgs) { - return this.client.logExecutionMetric(args); + public async logExecutionMetrics(args: LogExecutionMetricsArgs) { + return this.client.logExecutionMetrics(args); } public async logStatusChange(args: LogStatusChangeArgs) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts deleted file mode 100644 index ab8664ae995bf..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_adapter.ts +++ /dev/null @@ -1,106 +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 { merge } from 'lodash'; -import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; -import { RuleRegistryLogClient } from './rule_registry_log_client/rule_registry_log_client'; -import { - CreateExecutionLogArgs, - ExecutionMetric, - ExecutionMetricArgs, - FindBulkExecutionLogArgs, - FindExecutionLogArgs, - IRuleDataPluginService, - IRuleExecutionLogClient, - LogStatusChangeArgs, - UpdateExecutionLogArgs, -} from '../types'; - -/** - * @deprecated RuleRegistryAdapter is kept here only as a reference. It will be superseded with EventLog implementation - */ -export class RuleRegistryAdapter implements IRuleExecutionLogClient { - private ruleRegistryClient: RuleRegistryLogClient; - - constructor(ruleDataService: IRuleDataPluginService) { - this.ruleRegistryClient = new RuleRegistryLogClient(ruleDataService); - } - - public async find({ ruleId, logsCount = 1, spaceId }: FindExecutionLogArgs) { - const logs = await this.ruleRegistryClient.find({ - ruleIds: [ruleId], - logsCount, - spaceId, - }); - - return logs[ruleId].map((log) => ({ - id: '', - type: '', - score: 0, - attributes: log, - references: [], - })); - } - - public async findBulk({ ruleIds, logsCount = 1, spaceId }: FindBulkExecutionLogArgs) { - const [statusesById, lastErrorsById] = await Promise.all([ - this.ruleRegistryClient.find({ ruleIds, spaceId }), - this.ruleRegistryClient.find({ - ruleIds, - statuses: [RuleExecutionStatus.failed], - logsCount, - spaceId, - }), - ]); - return merge(statusesById, lastErrorsById); - } - - private async create({ attributes, spaceId }: CreateExecutionLogArgs) { - if (attributes.status) { - await this.ruleRegistryClient.logStatusChange({ - ruleId: attributes.alertId, - newStatus: attributes.status, - spaceId, - }); - } - - if (attributes.bulkCreateTimeDurations) { - await this.ruleRegistryClient.logExecutionMetric({ - ruleId: attributes.alertId, - metric: ExecutionMetric.indexingDurationMax, - value: Math.max(...attributes.bulkCreateTimeDurations.map(Number)), - spaceId, - }); - } - - if (attributes.gap) { - await this.ruleRegistryClient.logExecutionMetric({ - ruleId: attributes.alertId, - metric: ExecutionMetric.executionGap, - value: Number(attributes.gap), - spaceId, - }); - } - } - - public async update({ attributes, spaceId }: UpdateExecutionLogArgs) { - // execution events are immutable, so we just use 'create' here instead of 'update' - await this.create({ attributes, spaceId }); - } - - public async delete(id: string) { - // execution events are immutable, nothing to do here - } - - public async logExecutionMetric(args: ExecutionMetricArgs) { - return this.ruleRegistryClient.logExecutionMetric(args); - } - - public async logStatusChange(args: LogStatusChangeArgs) { - return this.ruleRegistryClient.logStatusChange(args); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts deleted file mode 100644 index 8d74c71bf447d..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/constants.ts +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/** - * @deprecated EVENTS_INDEX_PREFIX is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const EVENTS_INDEX_PREFIX = '.kibana_alerts-security.events'; - -/** - * @deprecated MESSAGE is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const MESSAGE = 'message' as const; - -/** - * @deprecated EVENT_SEQUENCE is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const EVENT_SEQUENCE = 'event.sequence' as const; - -/** - * @deprecated EVENT_DURATION is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const EVENT_DURATION = 'event.duration' as const; - -/** - * @deprecated EVENT_END is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const EVENT_END = 'event.end' as const; - -/** - * @deprecated RULE_STATUS is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const RULE_STATUS = 'kibana.rac.detection_engine.rule_status' as const; - -/** - * @deprecated RULE_STATUS_SEVERITY is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const RULE_STATUS_SEVERITY = 'kibana.rac.detection_engine.rule_status_severity' as const; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.ts deleted file mode 100644 index cbc6e570e936f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/parse_rule_execution_log.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { isLeft } from 'fp-ts/lib/Either'; -import { PathReporter } from 'io-ts/lib/PathReporter'; -import { technicalRuleFieldMap } from '../../../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map'; -import { - mergeFieldMaps, - runtimeTypeFromFieldMap, -} from '../../../../../../../rule_registry/common/field_map'; -import { ruleExecutionFieldMap } from './rule_execution_field_map'; - -const ruleExecutionLogRuntimeType = runtimeTypeFromFieldMap( - mergeFieldMaps(technicalRuleFieldMap, ruleExecutionFieldMap) -); - -/** - * @deprecated parseRuleExecutionLog is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const parseRuleExecutionLog = (input: unknown) => { - const validate = ruleExecutionLogRuntimeType.decode(input); - - if (isLeft(validate)) { - throw new Error(PathReporter.report(validate).join('\n')); - } - - return ruleExecutionLogRuntimeType.encode(validate.right); -}; - -/** - * @deprecated RuleExecutionEvent is kept here only as a reference. It will be superseded with EventLog implementation - * - * It's marked as `Partial` because the field map is not yet appropriate for - * execution log events. - */ -export type RuleExecutionEvent = Partial>; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts deleted file mode 100644 index b3c70cd56d9e6..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_execution_field_map.ts +++ /dev/null @@ -1,32 +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 { - EVENT_DURATION, - EVENT_END, - EVENT_SEQUENCE, - MESSAGE, - RULE_STATUS, - RULE_STATUS_SEVERITY, -} from './constants'; - -/** - * @deprecated ruleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const ruleExecutionFieldMap = { - [MESSAGE]: { type: 'keyword' }, - [EVENT_SEQUENCE]: { type: 'long' }, - [EVENT_END]: { type: 'date' }, - [EVENT_DURATION]: { type: 'long' }, - [RULE_STATUS]: { type: 'keyword' }, - [RULE_STATUS_SEVERITY]: { type: 'integer' }, -} as const; - -/** - * @deprecated RuleExecutionFieldMap is kept here only as a reference. It will be superseded with EventLog implementation - */ -export type RuleExecutionFieldMap = typeof ruleExecutionFieldMap; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts deleted file mode 100644 index 3cd6171b5bbeb..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/rule_registry_log_client.ts +++ /dev/null @@ -1,270 +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 { estypes } from '@elastic/elasticsearch'; -import { - ALERT_RULE_CONSUMER, - ALERT_RULE_TYPE_ID, - EVENT_ACTION, - EVENT_KIND, - SPACE_IDS, - TIMESTAMP, - ALERT_RULE_UUID, -} from '@kbn/rule-data-utils'; -import moment from 'moment'; - -import { mappingFromFieldMap } from '../../../../../../../rule_registry/common/mapping_from_field_map'; -import { Dataset, IRuleDataClient } from '../../../../../../../rule_registry/server'; -import { SERVER_APP_ID } from '../../../../../../common/constants'; -import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; -import { invariant } from '../../../../../../common/utils/invariant'; -import { IRuleStatusSOAttributes } from '../../../rules/types'; -import { makeFloatString } from '../../../signals/utils'; -import { - ExecutionMetric, - ExecutionMetricArgs, - IRuleDataPluginService, - LogStatusChangeArgs, -} from '../../types'; -import { EVENT_SEQUENCE, MESSAGE, RULE_STATUS, RULE_STATUS_SEVERITY } from './constants'; -import { parseRuleExecutionLog, RuleExecutionEvent } from './parse_rule_execution_log'; -import { ruleExecutionFieldMap } from './rule_execution_field_map'; -import { - getLastEntryAggregation, - getMetricAggregation, - getMetricField, - sortByTimeDesc, -} from './utils'; - -const statusSeverityDict: Record = { - [RuleExecutionStatus.succeeded]: 0, - [RuleExecutionStatus['going to run']]: 10, - [RuleExecutionStatus.warning]: 20, - [RuleExecutionStatus['partial failure']]: 20, - [RuleExecutionStatus.failed]: 30, -}; - -interface FindExecutionLogArgs { - ruleIds: string[]; - spaceId: string; - logsCount?: number; - statuses?: RuleExecutionStatus[]; -} - -interface IRuleRegistryLogClient { - find: (args: FindExecutionLogArgs) => Promise<{ - [ruleId: string]: IRuleStatusSOAttributes[] | undefined; - }>; - create: (event: RuleExecutionEvent) => Promise; - logStatusChange: (args: LogStatusChangeArgs) => Promise; - logExecutionMetric: (args: ExecutionMetricArgs) => Promise; -} - -/** - * @deprecated RuleRegistryLogClient is kept here only as a reference. It will be superseded with EventLog implementation - */ -export class RuleRegistryLogClient implements IRuleRegistryLogClient { - private sequence = 0; - private ruleDataClient: IRuleDataClient; - - constructor(ruleDataService: IRuleDataPluginService) { - this.ruleDataClient = ruleDataService.initializeIndex({ - feature: SERVER_APP_ID, - registrationContext: 'security', - dataset: Dataset.events, - componentTemplateRefs: [], - componentTemplates: [ - { - name: 'mappings', - mappings: mappingFromFieldMap(ruleExecutionFieldMap, 'strict'), - }, - ], - }); - } - - public async find({ ruleIds, spaceId, statuses, logsCount = 1 }: FindExecutionLogArgs) { - if (ruleIds.length === 0) { - return {}; - } - - const filter: estypes.QueryDslQueryContainer[] = [ - { terms: { [ALERT_RULE_UUID]: ruleIds } }, - { terms: { [SPACE_IDS]: [spaceId] } }, - ]; - - if (statuses) { - filter.push({ terms: { [RULE_STATUS]: statuses } }); - } - - const result = await this.ruleDataClient.getReader().search({ - size: 0, - body: { - query: { - bool: { - filter, - }, - }, - aggs: { - rules: { - terms: { - field: ALERT_RULE_UUID, - size: ruleIds.length, - }, - aggs: { - most_recent_logs: { - top_hits: { - sort: sortByTimeDesc, - size: logsCount, - }, - }, - last_failure: getLastEntryAggregation(RuleExecutionStatus.failed), - last_success: getLastEntryAggregation(RuleExecutionStatus.succeeded), - execution_gap: getMetricAggregation(ExecutionMetric.executionGap), - search_duration_max: getMetricAggregation(ExecutionMetric.searchDurationMax), - indexing_duration_max: getMetricAggregation(ExecutionMetric.indexingDurationMax), - indexing_lookback: getMetricAggregation(ExecutionMetric.indexingLookback), - }, - }, - }, - }, - }); - - if (result.hits.total.value === 0) { - return {}; - } - - invariant(result.aggregations, 'Search response should contain aggregations'); - - return Object.fromEntries( - result.aggregations.rules.buckets.map<[ruleId: string, logs: IRuleStatusSOAttributes[]]>( - (bucket) => [ - bucket.key as string, - bucket.most_recent_logs.hits.hits.map((event) => { - const logEntry = parseRuleExecutionLog(event._source); - invariant( - logEntry[ALERT_RULE_UUID] ?? '', - 'Malformed execution log entry: rule.id field not found' - ); - - const lastFailure = bucket.last_failure.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_failure.event.hits.hits[0]._source) - : undefined; - - const lastSuccess = bucket.last_success.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.last_success.event.hits.hits[0]._source) - : undefined; - - const lookBack = bucket.indexing_lookback.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_lookback.event.hits.hits[0]._source) - : undefined; - - const executionGap = bucket.execution_gap.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.execution_gap.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.executionGap) - ] - : undefined; - - const searchDuration = bucket.search_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.search_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.searchDurationMax) - ] - : undefined; - - const indexingDuration = bucket.indexing_duration_max.event.hits.hits[0] - ? parseRuleExecutionLog(bucket.indexing_duration_max.event.hits.hits[0]._source)[ - getMetricField(ExecutionMetric.indexingDurationMax) - ] - : undefined; - - const alertId = logEntry[ALERT_RULE_UUID] ?? ''; - const statusDate = logEntry[TIMESTAMP]; - const lastFailureAt = lastFailure?.[TIMESTAMP]; - const lastFailureMessage = lastFailure?.[MESSAGE]; - const lastSuccessAt = lastSuccess?.[TIMESTAMP]; - const lastSuccessMessage = lastSuccess?.[MESSAGE]; - const status = (logEntry[RULE_STATUS] as RuleExecutionStatus) || null; - const lastLookBackDate = lookBack?.[getMetricField(ExecutionMetric.indexingLookback)]; - const gap = executionGap ? moment.duration(executionGap).humanize() : null; - const bulkCreateTimeDurations = indexingDuration - ? [makeFloatString(indexingDuration)] - : null; - const searchAfterTimeDurations = searchDuration - ? [makeFloatString(searchDuration)] - : null; - - return { - alertId, - statusDate, - lastFailureAt, - lastFailureMessage, - lastSuccessAt, - lastSuccessMessage, - status, - lastLookBackDate, - gap, - bulkCreateTimeDurations, - searchAfterTimeDurations, - }; - }), - ] - ) - ); - } - - public async logExecutionMetric({ - ruleId, - namespace, - metric, - value, - spaceId, - }: ExecutionMetricArgs) { - await this.create( - { - [SPACE_IDS]: [spaceId], - [EVENT_ACTION]: metric, - [EVENT_KIND]: 'metric', - [getMetricField(metric)]: value, - [ALERT_RULE_UUID]: ruleId ?? '', - [TIMESTAMP]: new Date().toISOString(), - [ALERT_RULE_CONSUMER]: SERVER_APP_ID, - [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, - }, - namespace - ); - } - - public async logStatusChange({ - ruleId, - newStatus, - namespace, - message, - spaceId, - }: LogStatusChangeArgs) { - await this.create( - { - [SPACE_IDS]: [spaceId], - [EVENT_ACTION]: 'status-change', - [EVENT_KIND]: 'event', - [EVENT_SEQUENCE]: this.sequence++, - [MESSAGE]: message, - [ALERT_RULE_UUID]: ruleId ?? '', - [RULE_STATUS_SEVERITY]: statusSeverityDict[newStatus], - [RULE_STATUS]: newStatus, - [TIMESTAMP]: new Date().toISOString(), - [ALERT_RULE_CONSUMER]: SERVER_APP_ID, - [ALERT_RULE_TYPE_ID]: SERVER_APP_ID, - }, - namespace - ); - } - - public async create(event: RuleExecutionEvent, namespace?: string) { - await this.ruleDataClient.getWriter({ namespace }).bulk({ - body: [{ index: {} }, event], - }); - } -} diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts deleted file mode 100644 index 713cf73890e7f..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/rule_registry_adapter/rule_registry_log_client/utils.ts +++ /dev/null @@ -1,76 +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 { SearchSort } from '@elastic/elasticsearch/api/types'; -import { EVENT_ACTION, TIMESTAMP } from '@kbn/rule-data-utils'; -import { RuleExecutionStatus } from '../../../../../../common/detection_engine/schemas/common/schemas'; -import { ExecutionMetric } from '../../types'; -import { RULE_STATUS, EVENT_SEQUENCE, EVENT_DURATION, EVENT_END } from './constants'; - -const METRIC_FIELDS = { - [ExecutionMetric.executionGap]: EVENT_DURATION, - [ExecutionMetric.searchDurationMax]: EVENT_DURATION, - [ExecutionMetric.indexingDurationMax]: EVENT_DURATION, - [ExecutionMetric.indexingLookback]: EVENT_END, -}; - -/** - * Returns ECS field in which metric value is stored - * @deprecated getMetricField is kept here only as a reference. It will be superseded with EventLog implementation - * - * @param metric - execution metric - * @returns ECS field - */ -export const getMetricField = (metric: T) => METRIC_FIELDS[metric]; - -/** - * @deprecated sortByTimeDesc is kept here only as a reference. It will be superseded with EventLog implementation - */ -export const sortByTimeDesc: SearchSort = [{ [TIMESTAMP]: 'desc' }, { [EVENT_SEQUENCE]: 'desc' }]; - -/** - * Builds aggregation to retrieve the most recent metric value - * @deprecated getMetricAggregation is kept here only as a reference. It will be superseded with EventLog implementation - * - * @param metric - execution metric - * @returns aggregation - */ -export const getMetricAggregation = (metric: ExecutionMetric) => ({ - filter: { - term: { [EVENT_ACTION]: metric }, - }, - aggs: { - event: { - top_hits: { - size: 1, - sort: sortByTimeDesc, - _source: [TIMESTAMP, getMetricField(metric)], - }, - }, - }, -}); - -/** - * Builds aggregation to retrieve the most recent log entry with the given status - * @deprecated getLastEntryAggregation is kept here only as a reference. It will be superseded with EventLog implementation - * - * @param status - rule execution status - * @returns aggregation - */ -export const getLastEntryAggregation = (status: RuleExecutionStatus) => ({ - filter: { - term: { [RULE_STATUS]: status }, - }, - aggs: { - event: { - top_hits: { - sort: sortByTimeDesc, - size: 1, - }, - }, - }, -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts index 27329ebf8f90c..ca806bd58e369 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/saved_objects_adapter/saved_objects_adapter.ts @@ -14,12 +14,11 @@ import { ruleStatusSavedObjectsClientFactory, } from './rule_status_saved_objects_client'; import { - ExecutionMetric, - ExecutionMetricArgs, + LogExecutionMetricsArgs, FindBulkExecutionLogArgs, FindExecutionLogArgs, IRuleExecutionLogClient, - LegacyMetrics, + ExecutionMetrics, LogStatusChangeArgs, UpdateExecutionLogArgs, } from '../types'; @@ -28,14 +27,16 @@ import { assertUnreachable } from '../../../../../common'; // 1st is mutable status, followed by 5 most recent failures export const MAX_RULE_STATUSES = 6; -const METRIC_FIELDS = { - [ExecutionMetric.executionGap]: 'gap', - [ExecutionMetric.searchDurationMax]: 'searchAfterTimeDurations', - [ExecutionMetric.indexingDurationMax]: 'bulkCreateTimeDurations', - [ExecutionMetric.indexingLookback]: 'lastLookBackDate', -} as const; - -const getMetricField = (metric: T) => METRIC_FIELDS[metric]; +const convertMetricFields = ( + metrics: ExecutionMetrics +): Pick< + IRuleStatusSOAttributes, + 'gap' | 'searchAfterTimeDurations' | 'bulkCreateTimeDurations' +> => ({ + gap: metrics.executionGap?.humanize(), + searchAfterTimeDurations: metrics.searchDurations, + bulkCreateTimeDurations: metrics.indexingDurations, +}); export class SavedObjectsAdapter implements IRuleExecutionLogClient { private ruleStatusClient: RuleStatusSavedObjectsClient; @@ -66,16 +67,12 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { await this.ruleStatusClient.delete(id); } - public async logExecutionMetric({ - ruleId, - metric, - value, - }: ExecutionMetricArgs) { + public async logExecutionMetrics({ ruleId, metrics }: LogExecutionMetricsArgs) { const [currentStatus] = await this.getOrCreateRuleStatuses(ruleId); await this.ruleStatusClient.update(currentStatus.id, { ...currentStatus.attributes, - [getMetricField(metric)]: value, + ...convertMetricFields(metrics), }); } @@ -158,11 +155,11 @@ export class SavedObjectsAdapter implements IRuleExecutionLogClient { const buildRuleStatusAttributes: ( status: RuleExecutionStatus, message?: string, - metrics?: LegacyMetrics + metrics?: ExecutionMetrics ) => Partial = (status, message, metrics = {}) => { const now = new Date().toISOString(); const baseAttributes: Partial = { - ...metrics, + ...convertMetricFields(metrics), status: status === RuleExecutionStatus.warning ? RuleExecutionStatus['partial failure'] : status, statusDate: now, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts index 9c66032f681de..e38f974ddee2e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_execution_log/types.ts @@ -5,28 +5,16 @@ * 2.0. */ -import { PublicMethodsOf } from '@kbn/utility-types'; +import { Duration } from 'moment'; import { SavedObjectsFindResult } from '../../../../../../../src/core/server'; -import { RuleDataPluginService } from '../../../../../rule_registry/server'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { IRuleStatusSOAttributes } from '../rules/types'; -export enum ExecutionMetric { - 'executionGap' = 'executionGap', - 'searchDurationMax' = 'searchDurationMax', - 'indexingDurationMax' = 'indexingDurationMax', - 'indexingLookback' = 'indexingLookback', +export enum UnderlyingLogClient { + 'savedObjects' = 'savedObjects', + 'eventLog' = 'eventLog', } -export type IRuleDataPluginService = PublicMethodsOf; - -export type ExecutionMetricValue = { - [ExecutionMetric.executionGap]: number; - [ExecutionMetric.searchDurationMax]: number; - [ExecutionMetric.indexingDurationMax]: number; - [ExecutionMetric.indexingLookback]: Date; -}[T]; - export interface FindExecutionLogArgs { ruleId: string; spaceId: string; @@ -39,29 +27,34 @@ export interface FindBulkExecutionLogArgs { logsCount?: number; } -/** - * @deprecated LegacyMetrics are only kept here for backward compatibility - * and should be replaced by ExecutionMetric in the future - */ -export interface LegacyMetrics { - searchAfterTimeDurations?: string[]; - bulkCreateTimeDurations?: string[]; +export interface ExecutionMetrics { + searchDurations?: string[]; + indexingDurations?: string[]; + /** + * @deprecated lastLookBackDate is logged only by SavedObjectsAdapter and should be removed in the future + */ lastLookBackDate?: string; - gap?: string; + executionGap?: Duration; } export interface LogStatusChangeArgs { ruleId: string; + ruleName: string; + ruleType: string; spaceId: string; newStatus: RuleExecutionStatus; - namespace?: string; message?: string; - metrics?: LegacyMetrics; + /** + * @deprecated Use RuleExecutionLogClient.logExecutionMetrics to write metrics instead + */ + metrics?: ExecutionMetrics; } export interface UpdateExecutionLogArgs { id: string; attributes: IRuleStatusSOAttributes; + ruleName: string; + ruleType: string; spaceId: string; } @@ -70,12 +63,12 @@ export interface CreateExecutionLogArgs { spaceId: string; } -export interface ExecutionMetricArgs { +export interface LogExecutionMetricsArgs { ruleId: string; + ruleName: string; + ruleType: string; spaceId: string; - namespace?: string; - metric: T; - value: ExecutionMetricValue; + metrics: ExecutionMetrics; } export interface FindBulkExecutionLogResponse { @@ -90,5 +83,5 @@ export interface IRuleExecutionLogClient { update: (args: UpdateExecutionLogArgs) => Promise; delete: (id: string) => Promise; logStatusChange: (args: LogStatusChangeArgs) => Promise; - logExecutionMetric: (args: ExecutionMetricArgs) => Promise; + logExecutionMetrics: (args: LogExecutionMetricsArgs) => Promise; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index bcab8a0af5ffb..c6f818f04fc5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -14,6 +14,7 @@ import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; import type { IRuleDataClient } from '../../../../../../rule_registry/server'; import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks'; +import { eventLogServiceMock } from '../../../../../../event_log/server/mocks'; import { PluginSetupContract as AlertingPluginSetupContract } from '../../../../../../alerting/server'; import { ConfigType } from '../../../../config'; import { AlertAttributes } from '../../signals/types'; @@ -55,6 +56,7 @@ export const createRuleTypeMocks = ( references: [], attributes: { actions: [], + alertTypeId: 'siem.signals', enabled: true, name: 'mock rule', tags: [], @@ -89,7 +91,7 @@ export const createRuleTypeMocks = ( ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-security.alerts' ) as IRuleDataClient, - ruleDataService: ruleRegistryMocks.createRuleDataPluginService(), + eventLogService: eventLogServiceMock.create(), }, services, scheduleActions, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts index df15d4b2c0112..9ea36abe997c3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts @@ -40,8 +40,9 @@ import { scheduleThrottledNotificationActions } from '../notifications/schedule_ /* eslint-disable complexity */ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = - ({ lists, logger, mergeStrategy, ignoreFields, ruleDataClient, ruleDataService }) => + ({ lists, logger, config, ruleDataClient, eventLogService }) => (type) => { + const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config; const persistenceRuleType = createPersistenceRuleTypeFactory({ ruleDataClient, logger }); return persistenceRuleType({ ...type, @@ -65,13 +66,15 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = const ruleStatusClient = new RuleExecutionLogClient({ savedObjectsClient, - ruleDataService, + eventLogService, + underlyingClient: config.ruleExecutionLog.underlyingClient, }); const ruleSO = await savedObjectsClient.get('alert', alertId); const { actions, name, + alertTypeId, schedule: { interval }, } = ruleSO.attributes; const refresh = actions.length ? 'wait_for' : false; @@ -87,9 +90,14 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = logger.debug(buildRuleMessage(`interval: ${interval}`)); let wroteWarningStatus = false; - await ruleStatusClient.logStatusChange({ + const basicLogArguments = { spaceId, ruleId: alertId, + ruleName: name, + ruleType: alertTypeId, + }; + await ruleStatusClient.logStatusChange({ + ...basicLogArguments, newStatus: RuleExecutionStatus['going to run'], }); @@ -125,8 +133,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = tryCatch( () => hasReadIndexPrivileges({ - spaceId, - ruleId: alertId, + ...basicLogArguments, privileges, logger, buildRuleMessage, @@ -138,8 +145,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = tryCatch( () => hasTimestampFields({ - spaceId, - ruleId: alertId, + ...basicLogArguments, wroteStatus: wroteStatus as boolean, timestampField: hasTimestampOverride ? (timestampOverride as string) @@ -179,11 +185,10 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = logger.warn(gapMessage); hasError = true; await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message: gapMessage, - metrics: { gap: gapString }, + metrics: { executionGap: remainingGap }, }); } @@ -262,8 +267,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = if (result.warningMessages.length) { const warningMessage = buildRuleMessage(result.warningMessages.join()); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus['partial failure'], message: warningMessage, }); @@ -327,13 +331,12 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = if (!hasError && !wroteWarningStatus && !result.warning) { await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.succeeded, message: 'succeeded', metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookbackDate?.toISOString(), }, }); @@ -356,13 +359,12 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = ); logger.error(errorMessage); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message: errorMessage, metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookbackDate?.toISOString(), }, }); @@ -376,13 +378,12 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = logger.error(message); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message, metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookbackDate?.toISOString(), }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts index 868419179c76b..43860d396ac5d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts @@ -12,6 +12,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { createEqlAlertType } from './create_eql_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { getEqlRuleParams } from '../../schemas/rule_schemas.mock'; +import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../rule_execution_log/rule_execution_log_client'); @@ -26,10 +27,9 @@ describe('Event correlation alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - ignoreFields: [], - mergeStrategy: 'allFields', + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); dependencies.alerting.registerType(eqlAlertType); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 5893c6fdc86c2..9324b469bf644 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -14,23 +14,14 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createEqlAlertType = (createOptions: CreateRuleOptions) => { - const { - experimentalFeatures, - lists, - logger, - ignoreFields, - mergeStrategy, - ruleDataClient, - version, - ruleDataService, - } = createOptions; + const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = + createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, - ignoreFields, - mergeStrategy, + config, ruleDataClient, - ruleDataService, + eventLogService, }); return createSecurityRuleType({ id: EQL_RULE_TYPE_ID, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts index 6daafbfae40f2..a7accc4ae8a0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/utils/build_alert_group_from_sequence.test.ts @@ -44,6 +44,7 @@ describe('buildAlert', () => { const ruleSO = { attributes: { actions: [], + alertTypeId: 'siem.signals', createdAt: new Date().toISOString(), createdBy: 'gandalf', params: getQueryRuleParams(), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts index fe836c872dcad..3db4f5686abdc 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts @@ -16,6 +16,7 @@ import { createIndicatorMatchAlertType } from './create_indicator_match_alert_ty import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { CountResponse } from 'kibana/server'; import { RuleParams } from '../../schemas/rule_schemas'; +import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../utils/get_list_client', () => ({ getListClient: jest.fn().mockReturnValue({ @@ -56,10 +57,9 @@ describe('Indicator Match Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - ignoreFields: [], - mergeStrategy: 'allFields', + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); @@ -97,10 +97,9 @@ describe('Indicator Match Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); @@ -136,10 +135,9 @@ describe('Indicator Match Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index e2d5da1def707..c30fdd7d99c2a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -14,23 +14,14 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) => { - const { - experimentalFeatures, - lists, - logger, - mergeStrategy, - ignoreFields, - ruleDataClient, - version, - ruleDataService, - } = createOptions; + const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = + createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, - mergeStrategy, - ignoreFields, + config, ruleDataClient, - ruleDataService, + eventLogService, }); return createSecurityRuleType({ id: INDICATOR_RULE_TYPE_ID, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts index 23cd2e94aedf8..bffc20c3df1e3 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts @@ -14,6 +14,7 @@ import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { createMlAlertType } from './create_ml_alert_type'; import { RuleParams } from '../../schemas/rule_schemas'; +import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../signals/bulk_create_ml_signals'); @@ -97,11 +98,10 @@ describe('Machine Learning Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ml: mlMock, ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index ec2f5dd104646..ac2d3f14831a4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -14,15 +14,13 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createMlAlertType = (createOptions: CreateRuleOptions) => { - const { lists, logger, mergeStrategy, ignoreFields, ml, ruleDataClient, ruleDataService } = - createOptions; + const { lists, logger, config, ml, ruleDataClient, eventLogService } = createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, - mergeStrategy, - ignoreFields, + config, ruleDataClient, - ruleDataService, + eventLogService, }); return createSecurityRuleType({ id: ML_RULE_TYPE_ID, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index e45d8440386fe..4fdeac8047b1d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -14,6 +14,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { createQueryAlertType } from './create_query_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../utils/get_list_client', () => ({ getListClient: jest.fn().mockReturnValue({ @@ -31,10 +32,9 @@ describe('Custom Query Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); @@ -79,10 +79,9 @@ describe('Custom Query Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index d5af7a4c8b5a4..469c237112dcb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -14,23 +14,14 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createQueryAlertType = (createOptions: CreateRuleOptions) => { - const { - experimentalFeatures, - lists, - logger, - mergeStrategy, - ignoreFields, - ruleDataClient, - version, - ruleDataService, - } = createOptions; + const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = + createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, - mergeStrategy, - ignoreFields, + config, ruleDataClient, - ruleDataService, + eventLogService, }); return createSecurityRuleType({ id: QUERY_RULE_TYPE_ID, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts index 74435cb300472..aff57dbdf3cd4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts @@ -9,6 +9,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { createThresholdAlertType } from './create_threshold_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; +import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../rule_execution_log/rule_execution_log_client'); @@ -20,10 +21,9 @@ describe('Threshold Alerts', () => { experimentalFeatures: allowedExperimentalValues, lists: dependencies.lists, logger: dependencies.logger, - mergeStrategy: 'allFields', - ignoreFields: [], + config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, - ruleDataService: dependencies.ruleDataService, + eventLogService: dependencies.eventLogService, version: '1.0.0', }); dependencies.alerting.registerType(thresholdAlertTpe); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index a503cf5aedbea..789e4525c58ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -16,23 +16,14 @@ import { createSecurityRuleTypeFactory } from '../create_security_rule_type_fact import { CreateRuleOptions } from '../types'; export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { - const { - experimentalFeatures, - lists, - logger, - mergeStrategy, - ignoreFields, - ruleDataClient, - version, - ruleDataService, - } = createOptions; + const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = + createOptions; const createSecurityRuleType = createSecurityRuleTypeFactory({ lists, logger, - mergeStrategy, - ignoreFields, + config, ruleDataClient, - ruleDataService, + eventLogService, }); return createSecurityRuleType({ id: THRESHOLD_RULE_TYPE_ID, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index 6280a50d4981c..c94339da03b93 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -30,12 +30,12 @@ import { import { BaseHit } from '../../../../common/detection_engine/types'; import { ConfigType } from '../../../config'; import { SetupPlugins } from '../../../plugin'; -import { IRuleDataPluginService } from '../rule_execution_log/types'; import { RuleParams } from '../schemas/rule_schemas'; import { BuildRuleMessage } from '../signals/rule_messages'; import { AlertAttributes, BulkCreate, WrapHits, WrapSequences } from '../signals/types'; import { AlertsFieldMap, RulesFieldMap } from './field_maps'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; +import { IEventLogService } from '../../../../../event_log/server'; export interface SecurityAlertTypeReturnValue { bulkCreateTimes: string[]; @@ -98,10 +98,9 @@ type SecurityAlertTypeWithExecutor< export type CreateSecurityRuleTypeFactory = (options: { lists: SetupPlugins['lists']; logger: Logger; - mergeStrategy: ConfigType['alertMergeStrategy']; - ignoreFields: ConfigType['alertIgnoreFields']; + config: ConfigType; ruleDataClient: IRuleDataClient; - ruleDataService: IRuleDataPluginService; + eventLogService: IEventLogService; }) => < TParams extends RuleParams & { index?: string[] | undefined }, TAlertInstanceContext extends AlertInstanceContext, @@ -127,10 +126,9 @@ export interface CreateRuleOptions { experimentalFeatures: ExperimentalFeatures; lists: SetupPlugins['lists']; logger: Logger; - mergeStrategy: ConfigType['alertMergeStrategy']; - ignoreFields: ConfigType['alertIgnoreFields']; + config: ConfigType; ml?: SetupPlugins['ml']; ruleDataClient: IRuleDataClient; version: string; - ruleDataService: IRuleDataPluginService; + eventLogService: IEventLogService; } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts index c6a5c00380242..2f3d05e0c9586 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/enable_rule.ts @@ -44,6 +44,8 @@ export const enableRule = async ({ const currentStatusToDisable = ruleCurrentStatus[0]; await ruleStatusClient.update({ id: currentStatusToDisable.id, + ruleName: rule.name, + ruleType: rule.alertTypeId, attributes: { ...currentStatusToDisable.attributes, status: RuleExecutionStatus['going to run'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 850eee3993b60..207ea497c7e8e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -33,6 +33,7 @@ export const sampleRuleSO = (params: T): SavedObject { updated_at: '2020-03-27T22:55:59.577Z', attributes: { actions: [], + alertTypeId: 'siem.signals', enabled: true, name: 'rule-name', tags: ['some fake tag 1', 'some fake tag 2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts index 5766390099e29..11145405dcc99 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/executors/threshold.test.ts @@ -30,6 +30,7 @@ describe('threshold_executor', () => { updated_at: '2020-03-27T22:55:59.577Z', attributes: { actions: [], + alertTypeId: 'siem.signals', enabled: true, name: 'rule-name', tags: ['some fake tag 1', 'some fake tag 2'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index 2696d6981083e..c2923b566175e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -32,10 +32,11 @@ import { mlExecutor } from './executors/ml'; import { getMlRuleParams, getQueryRuleParams } from '../schemas/rule_schemas.mock'; import { ResponseError } from '@elastic/elasticsearch/lib/errors'; import { allowedExperimentalValues } from '../../../../common/experimental_features'; -import { ruleRegistryMocks } from '../../../../../rule_registry/server/mocks'; import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { eventLogServiceMock } from '../../../../../event_log/server/mocks'; +import { createMockConfig } from '../routes/__mocks__'; jest.mock('./utils', () => { const original = jest.requireActual('./utils'); @@ -124,12 +125,12 @@ describe('signal_rule_alert_type', () => { let alert: ReturnType; let logger: ReturnType; let alertServices: AlertServicesMock; - let ruleDataService: ReturnType; + let eventLogService: ReturnType; beforeEach(() => { alertServices = alertsMock.createAlertServices(); logger = loggingSystemMock.createLogger(); - ruleDataService = ruleRegistryMocks.createRuleDataPluginService(); + eventLogService = eventLogServiceMock.create(); (getListsClient as jest.Mock).mockReturnValue({ listClient: getListClientMock(), exceptionsClient: getExceptionListClientMock(), @@ -194,9 +195,8 @@ describe('signal_rule_alert_type', () => { version, ml: mlMock, lists: listMock.createSetup(), - mergeStrategy: 'missingFields', - ignoreFields: [], - ruleDataService, + config: createMockConfig(), + eventLogService, }); mockRuleExecutionLogClient.logStatusChange.mockClear(); @@ -217,11 +217,18 @@ describe('signal_rule_alert_type', () => { payload.previousStartedAt = moment().subtract(100, 'm').toDate(); await alert.executor(payload); expect(logger.warn).toHaveBeenCalled(); - expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenLastCalledWith( + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + newStatus: RuleExecutionStatus['going to run'], + }) + ); + expect(mockRuleExecutionLogClient.logStatusChange).toHaveBeenNthCalledWith( + 2, expect.objectContaining({ newStatus: RuleExecutionStatus.failed, metrics: { - gap: 'an hour', + executionGap: expect.any(Object), }, }) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 9a6c099ed1760..1e3a8a513c4a1 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -70,9 +70,9 @@ import { ConfigType } from '../../../config'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { injectReferences, extractReferences } from './saved_object_references'; import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; -import { IRuleDataPluginService } from '../rule_execution_log/types'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; +import { IEventLogService } from '../../../../../event_log/server'; export const signalRulesAlertType = ({ logger, @@ -81,9 +81,8 @@ export const signalRulesAlertType = ({ version, ml, lists, - mergeStrategy, - ignoreFields, - ruleDataService, + config, + eventLogService, }: { logger: Logger; eventsTelemetry: TelemetryEventsSender | undefined; @@ -91,10 +90,10 @@ export const signalRulesAlertType = ({ version: string; ml: SetupPlugins['ml']; lists: SetupPlugins['lists'] | undefined; - mergeStrategy: ConfigType['alertMergeStrategy']; - ignoreFields: ConfigType['alertIgnoreFields']; - ruleDataService: IRuleDataPluginService; + config: ConfigType; + eventLogService: IEventLogService; }): SignalRuleAlertTypeDefinition => { + const { alertMergeStrategy: mergeStrategy, alertIgnoreFields: ignoreFields } = config; return { id: SIGNALS_ID, name: 'SIEM signal', @@ -138,14 +137,16 @@ export const signalRulesAlertType = ({ let hasError: boolean = false; let result = createSearchAfterReturnType(); const ruleStatusClient = new RuleExecutionLogClient({ - ruleDataService, + eventLogService, savedObjectsClient: services.savedObjectsClient, + underlyingClient: config.ruleExecutionLog.underlyingClient, }); const savedObject = await services.savedObjectsClient.get('alert', alertId); const { actions, name, + alertTypeId, schedule: { interval }, } = savedObject.attributes; const refresh = actions.length ? 'wait_for' : false; @@ -159,10 +160,16 @@ export const signalRulesAlertType = ({ logger.debug(buildRuleMessage('[+] Starting Signal Rule execution')); logger.debug(buildRuleMessage(`interval: ${interval}`)); let wroteWarningStatus = false; - await ruleStatusClient.logStatusChange({ + const basicLogArguments = { + spaceId, ruleId: alertId, + ruleName: name, + ruleType: alertTypeId, + }; + + await ruleStatusClient.logStatusChange({ + ...basicLogArguments, newStatus: RuleExecutionStatus['going to run'], - spaceId, }); // check if rule has permissions to access given index pattern @@ -194,8 +201,7 @@ export const signalRulesAlertType = ({ tryCatch( () => hasReadIndexPrivileges({ - spaceId, - ruleId: alertId, + ...basicLogArguments, privileges, logger, buildRuleMessage, @@ -207,13 +213,11 @@ export const signalRulesAlertType = ({ tryCatch( () => hasTimestampFields({ - spaceId, - ruleId: alertId, + ...basicLogArguments, wroteStatus: wroteStatus as boolean, timestampField: hasTimestampOverride ? (timestampOverride as string) : '@timestamp', - ruleName: name, timestampFieldCapsResponse: timestampFieldCaps, inputIndices, ruleStatusClient, @@ -247,11 +251,10 @@ export const signalRulesAlertType = ({ logger.warn(gapMessage); hasError = true; await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message: gapMessage, - metrics: { gap: gapString }, + metrics: { executionGap: remainingGap }, }); } try { @@ -383,8 +386,7 @@ export const signalRulesAlertType = ({ if (result.warningMessages.length) { const warningMessage = buildRuleMessage(result.warningMessages.join()); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus['partial failure'], message: warningMessage, }); @@ -445,13 +447,12 @@ export const signalRulesAlertType = ({ ); if (!hasError && !wroteWarningStatus && !result.warning) { await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.succeeded, message: 'succeeded', metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookBackDate?.toISOString(), }, }); @@ -474,13 +475,12 @@ export const signalRulesAlertType = ({ ); logger.error(errorMessage); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message: errorMessage, metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookBackDate?.toISOString(), }, }); @@ -494,13 +494,12 @@ export const signalRulesAlertType = ({ logger.error(message); await ruleStatusClient.logStatusChange({ - spaceId, - ruleId: alertId, + ...basicLogArguments, newStatus: RuleExecutionStatus.failed, message, metrics: { - bulkCreateTimeDurations: result.bulkCreateTimes, - searchAfterTimeDurations: result.searchAfterTimes, + indexingDurations: result.bulkCreateTimes, + searchDurations: result.searchAfterTimes, lastLookBackDate: result.lastLookBackDate?.toISOString(), }, }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index c1e7e23c3b161..82b4a46f482b6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -255,6 +255,7 @@ export interface SignalHit { export interface AlertAttributes { actions: RuleAlertAction[]; + alertTypeId: string; enabled: boolean; name: string; tags: string[]; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts index c3e95d6d196ca..7d2eafa46d382 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.test.ts @@ -789,6 +789,7 @@ describe('utils', () => { inputIndices: ['myfa*'], ruleStatusClient, ruleId: 'ruleId', + ruleType: 'ruleType', spaceId: 'default', logger: mockLogger, buildRuleMessage, @@ -832,6 +833,7 @@ describe('utils', () => { inputIndices: ['myfa*'], ruleStatusClient, ruleId: 'ruleId', + ruleType: 'ruleType', spaceId: 'default', logger: mockLogger, buildRuleMessage, @@ -861,6 +863,7 @@ describe('utils', () => { inputIndices: ['logs-endpoint.alerts-*'], ruleStatusClient, ruleId: 'ruleId', + ruleType: 'ruleType', spaceId: 'default', logger: mockLogger, buildRuleMessage, @@ -890,6 +893,7 @@ describe('utils', () => { inputIndices: ['logs-endpoint.alerts-*'], ruleStatusClient, ruleId: 'ruleId', + ruleType: 'ruleType', spaceId: 'default', logger: mockLogger, buildRuleMessage, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts index 5993dd626729f..2aefc7ea0bd64 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/utils.ts @@ -99,9 +99,20 @@ export const hasReadIndexPrivileges = async (args: { buildRuleMessage: BuildRuleMessage; ruleStatusClient: IRuleExecutionLogClient; ruleId: string; + ruleName: string; + ruleType: string; spaceId: string; }): Promise => { - const { privileges, logger, buildRuleMessage, ruleStatusClient, ruleId, spaceId } = args; + const { + privileges, + logger, + buildRuleMessage, + ruleStatusClient, + ruleId, + ruleName, + ruleType, + spaceId, + } = args; const indexNames = Object.keys(privileges.index); const [indexesWithReadPrivileges, indexesWithNoReadPrivileges] = partition( @@ -119,6 +130,8 @@ export const hasReadIndexPrivileges = async (args: { await ruleStatusClient.logStatusChange({ message: errorString, ruleId, + ruleName, + ruleType, spaceId, newStatus: RuleExecutionStatus['partial failure'], }); @@ -136,6 +149,8 @@ export const hasReadIndexPrivileges = async (args: { await ruleStatusClient.logStatusChange({ message: errorString, ruleId, + ruleName, + ruleType, spaceId, newStatus: RuleExecutionStatus['partial failure'], }); @@ -156,6 +171,7 @@ export const hasTimestampFields = async (args: { ruleStatusClient: IRuleExecutionLogClient; ruleId: string; spaceId: string; + ruleType: string; logger: Logger; buildRuleMessage: BuildRuleMessage; }): Promise => { @@ -167,6 +183,7 @@ export const hasTimestampFields = async (args: { inputIndices, ruleStatusClient, ruleId, + ruleType, spaceId, logger, buildRuleMessage, @@ -184,6 +201,8 @@ export const hasTimestampFields = async (args: { await ruleStatusClient.logStatusChange({ message: errorString.trimEnd(), ruleId, + ruleName, + ruleType, spaceId, newStatus: RuleExecutionStatus['partial failure'], }); @@ -210,6 +229,8 @@ export const hasTimestampFields = async (args: { await ruleStatusClient.logStatusChange({ message: errorString, ruleId, + ruleName, + ruleType, spaceId, newStatus: RuleExecutionStatus['partial failure'], }); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 9c4d739e0f434..bffcc823d047e 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -109,11 +109,14 @@ import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti'; import { legacyRulesNotificationAlertType } from './lib/detection_engine/notifications/legacy_rules_notification_alert_type'; // eslint-disable-next-line no-restricted-imports import { legacyIsNotificationAlertExecutor } from './lib/detection_engine/notifications/legacy_types'; +import { IEventLogClientService, IEventLogService } from '../../event_log/server'; +import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider'; export interface SetupPlugins { alerting: AlertingSetup; data: DataPluginSetup; encryptedSavedObjects?: EncryptedSavedObjectsSetup; + eventLog: IEventLogService; features: FeaturesSetup; lists?: ListPluginSetup; ml?: MlSetup; @@ -121,20 +124,21 @@ export interface SetupPlugins { security?: SecuritySetup; spaces?: SpacesSetup; taskManager?: TaskManagerSetupContract; - usageCollection?: UsageCollectionSetup; telemetry?: TelemetryPluginSetup; + usageCollection?: UsageCollectionSetup; } export interface StartPlugins { alerting: AlertPluginStartContract; + cases?: CasesPluginStartContract; data: DataPluginStart; + eventLog: IEventLogClientService; fleet?: FleetStartContract; licensing: LicensingPluginStart; ruleRegistry: RuleRegistryPluginStartContract; + security: SecurityPluginStart; taskManager?: TaskManagerStartContract; telemetry?: TelemetryPluginStart; - security: SecurityPluginStart; - cases?: CasesPluginStartContract; } // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -202,6 +206,9 @@ export class Plugin implements IPlugin(); core.http.registerRouteHandlerContext( APP_ID, @@ -210,8 +217,9 @@ export class Plugin implements IPlugin plugins.spaces?.spacesService?.getSpaceId(request) || DEFAULT_SPACE_ID, getExecutionLogClient: () => new RuleExecutionLogClient({ - ruleDataService: plugins.ruleRegistry.ruleDataService, savedObjectsClient: context.core.savedObjects.client, + eventLogService, + underlyingClient: config.ruleExecutionLog.underlyingClient, }), }) ); @@ -262,11 +270,10 @@ export class Plugin implements IPlugin Date: Mon, 11 Oct 2021 13:49:54 +0200 Subject: [PATCH 011/287] [bfetch] Fix memory leak (#113756) --- .../create_streaming_batched_function.test.ts | 50 ++++++++--------- .../create_streaming_batched_function.ts | 7 +-- src/plugins/bfetch/public/plugin.ts | 29 ++++------ .../public/streaming/fetch_streaming.test.ts | 29 +++++----- .../public/streaming/fetch_streaming.ts | 55 ++++++++----------- 5 files changed, 76 insertions(+), 94 deletions(-) diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts index 0b6dbe49d0e81..32adc0d7df0cf 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.test.ts @@ -9,7 +9,7 @@ import { createStreamingBatchedFunction } from './create_streaming_batched_function'; import { fetchStreaming as fetchStreamingReal } from '../streaming/fetch_streaming'; import { AbortError, defer, of } from '../../../kibana_utils/public'; -import { Subject, of as rxof } from 'rxjs'; +import { Subject } from 'rxjs'; const flushPromises = () => new Promise((resolve) => setImmediate(resolve)); @@ -61,7 +61,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); expect(typeof fn).toBe('function'); }); @@ -71,7 +71,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const res = fn({}); expect(typeof res.then).toBe('function'); @@ -85,7 +85,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -105,7 +105,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -120,7 +120,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); fn({ foo: 'bar' }); @@ -139,7 +139,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); fn({ foo: 'bar' }); @@ -161,7 +161,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -180,7 +180,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const abortController = new AbortController(); @@ -203,7 +203,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); fn({ a: '1' }); @@ -227,7 +227,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); fn({ a: '1' }); @@ -248,7 +248,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = fn({ a: '1' }); @@ -266,7 +266,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); await flushPromises(); @@ -310,7 +310,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = fn({ a: '1' }); @@ -348,7 +348,7 @@ describe('createStreamingBatchedFunction()', () => { fn({ a: '1' }); - const dontCompress = await fetchStreaming.mock.calls[0][0].compressionDisabled$.toPromise(); + const dontCompress = await fetchStreaming.mock.calls[0][0].getIsCompressionDisabled(); expect(dontCompress).toBe(false); }); @@ -359,7 +359,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = fn({ a: '1' }); @@ -401,7 +401,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise = fn({ a: '1' }); @@ -430,7 +430,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = of(fn({ a: '1' })); @@ -483,7 +483,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const abortController = new AbortController(); @@ -514,7 +514,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const abortController = new AbortController(); @@ -554,7 +554,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = of(fn({ a: '1' })); @@ -585,7 +585,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = of(fn({ a: '1' })); @@ -623,7 +623,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = of(fn({ a: '1' })); @@ -656,7 +656,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); const promise1 = of(fn({ a: '1' })); @@ -693,7 +693,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, - compressionDisabled$: rxof(true), + getIsCompressionDisabled: () => true, }); await flushPromises(); diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts index d5f955f517d13..3ff8da08cfce7 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts @@ -6,7 +6,6 @@ * Side Public License, v 1. */ -import { Observable, of } from 'rxjs'; import { AbortError, abortSignalToPromise, defer } from '../../../kibana_utils/public'; import { ItemBufferParams, @@ -51,7 +50,7 @@ export interface StreamingBatchedFunctionParams { /** * Disabled zlib compression of response chunks. */ - compressionDisabled$?: Observable; + getIsCompressionDisabled?: () => boolean; } /** @@ -69,7 +68,7 @@ export const createStreamingBatchedFunction = ( fetchStreaming: fetchStreamingInjected = fetchStreaming, flushOnMaxItems = 25, maxItemAge = 10, - compressionDisabled$ = of(false), + getIsCompressionDisabled = () => false, } = params; const [fn] = createBatchedFunction({ onCall: (payload: Payload, signal?: AbortSignal) => { @@ -125,7 +124,7 @@ export const createStreamingBatchedFunction = ( body: JSON.stringify({ batch }), method: 'POST', signal: abortController.signal, - compressionDisabled$, + getIsCompressionDisabled, }); const handleStreamError = (error: any) => { diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index 3ad451c7713ea..54bcb305d8675 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -6,13 +6,12 @@ * Side Public License, v 1. */ -import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; -import { from, Observable, of } from 'rxjs'; -import { switchMap } from 'rxjs/operators'; +import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { fetchStreaming as fetchStreamingStatic, FetchStreamingParams } from './streaming'; import { DISABLE_BFETCH_COMPRESSION, removeLeadingSlash } from '../common'; import { createStreamingBatchedFunction, StreamingBatchedFunctionParams } from './batching'; import { BatchedFunc } from './batching/types'; +import { createStartServicesGetter } from '../../kibana_utils/public'; // eslint-disable-next-line export interface BfetchPublicSetupDependencies {} @@ -50,16 +49,12 @@ export class BfetchPublicPlugin const { version } = this.initializerContext.env.packageInfo; const basePath = core.http.basePath.get(); - const compressionDisabled$ = from(core.getStartServices()).pipe( - switchMap((deps) => { - return of(deps[0]); - }), - switchMap((coreStart) => { - return coreStart.uiSettings.get$(DISABLE_BFETCH_COMPRESSION); - }) - ); - const fetchStreaming = this.fetchStreaming(version, basePath, compressionDisabled$); - const batchedFunction = this.batchedFunction(fetchStreaming, compressionDisabled$); + const startServices = createStartServicesGetter(core.getStartServices); + const getIsCompressionDisabled = () => + startServices().core.uiSettings.get(DISABLE_BFETCH_COMPRESSION); + + const fetchStreaming = this.fetchStreaming(version, basePath, getIsCompressionDisabled); + const batchedFunction = this.batchedFunction(fetchStreaming, getIsCompressionDisabled); this.contract = { fetchStreaming, @@ -79,7 +74,7 @@ export class BfetchPublicPlugin ( version: string, basePath: string, - compressionDisabled$: Observable + getIsCompressionDisabled: () => boolean ): BfetchPublicSetup['fetchStreaming'] => (params) => fetchStreamingStatic({ @@ -90,18 +85,18 @@ export class BfetchPublicPlugin 'kbn-version': version, ...(params.headers || {}), }, - compressionDisabled$, + getIsCompressionDisabled, }); private batchedFunction = ( fetchStreaming: BfetchPublicContract['fetchStreaming'], - compressionDisabled$: Observable + getIsCompressionDisabled: () => boolean ): BfetchPublicContract['batchedFunction'] => (params) => createStreamingBatchedFunction({ ...params, - compressionDisabled$, + getIsCompressionDisabled, fetchStreaming: params.fetchStreaming || fetchStreaming, }); } diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts index a5d066f6d9a24..67ebf8d5a1c23 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts @@ -8,7 +8,6 @@ import { fetchStreaming } from './fetch_streaming'; import { mockXMLHttpRequest } from '../test_helpers/xhr'; -import { of } from 'rxjs'; import { promisify } from 'util'; import { deflate } from 'zlib'; const pDeflate = promisify(deflate); @@ -30,7 +29,7 @@ test('returns XHR request', () => { setup(); const { xhr } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(typeof xhr.readyState).toBe('number'); }); @@ -39,7 +38,7 @@ test('returns stream', () => { setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(typeof stream.subscribe).toBe('function'); }); @@ -48,7 +47,7 @@ test('promise resolves when request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); let resolved = false; @@ -81,7 +80,7 @@ test('promise resolves when compressed request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(false), + getIsCompressionDisabled: () => false, }); let resolved = false; @@ -116,7 +115,7 @@ test('promise resolves when compressed chunked request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(false), + getIsCompressionDisabled: () => false, }); let resolved = false; @@ -160,7 +159,7 @@ test('streams incoming text as it comes through, according to separators', async const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); const spy = jest.fn(); @@ -201,7 +200,7 @@ test('completes stream observable when request finishes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); const spy = jest.fn(); @@ -226,7 +225,7 @@ test('completes stream observable when aborted', async () => { const { stream } = fetchStreaming({ url: 'http://example.com', signal: abort.signal, - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); const spy = jest.fn(); @@ -252,7 +251,7 @@ test('promise throws when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); const spy = jest.fn(); @@ -279,7 +278,7 @@ test('stream observable errors when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); const spy = jest.fn(); @@ -312,7 +311,7 @@ test('sets custom headers', async () => { 'Content-Type': 'text/plain', Authorization: 'Bearer 123', }, - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(env.xhr.setRequestHeader).toHaveBeenCalledWith('Content-Type', 'text/plain'); @@ -326,7 +325,7 @@ test('uses credentials', async () => { fetchStreaming({ url: 'http://example.com', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(env.xhr.withCredentials).toBe(true); @@ -342,7 +341,7 @@ test('opens XHR request and sends specified body', async () => { url: 'http://elastic.co', method: 'GET', body: 'foobar', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(env.xhr.open).toHaveBeenCalledTimes(1); @@ -355,7 +354,7 @@ test('uses POST request method by default', async () => { const env = setup(); fetchStreaming({ url: 'http://elastic.co', - compressionDisabled$: of(true), + getIsCompressionDisabled: () => true, }); expect(env.xhr.open).toHaveBeenCalledWith('POST', 'http://elastic.co'); }); diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.ts index 1af35ef68fb85..a94c8d3980cba 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.ts @@ -6,8 +6,7 @@ * Side Public License, v 1. */ -import { Observable, of } from 'rxjs'; -import { map, share, switchMap } from 'rxjs/operators'; +import { map, share } from 'rxjs/operators'; import { inflateResponse } from '.'; import { fromStreamingXhr } from './from_streaming_xhr'; import { split } from './split'; @@ -18,7 +17,7 @@ export interface FetchStreamingParams { method?: 'GET' | 'POST'; body?: string; signal?: AbortSignal; - compressionDisabled$?: Observable; + getIsCompressionDisabled?: () => boolean; } /** @@ -31,49 +30,39 @@ export function fetchStreaming({ method = 'POST', body = '', signal, - compressionDisabled$ = of(false), + getIsCompressionDisabled = () => false, }: FetchStreamingParams) { const xhr = new window.XMLHttpRequest(); - const msgStream = compressionDisabled$.pipe( - switchMap((compressionDisabled) => { - // Begin the request - xhr.open(method, url); - xhr.withCredentials = true; + // Begin the request + xhr.open(method, url); + xhr.withCredentials = true; - if (!compressionDisabled) { - headers['X-Chunk-Encoding'] = 'deflate'; - } + const isCompressionDisabled = getIsCompressionDisabled(); - // Set the HTTP headers - Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); + if (!isCompressionDisabled) { + headers['X-Chunk-Encoding'] = 'deflate'; + } - const stream = fromStreamingXhr(xhr, signal); + // Set the HTTP headers + Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); - // Send the payload to the server - xhr.send(body); + const stream = fromStreamingXhr(xhr, signal); - // Return a stream of chunked decompressed messages - return stream.pipe( - split('\n'), - map((msg) => { - return compressionDisabled ? msg : inflateResponse(msg); - }) - ); + // Send the payload to the server + xhr.send(body); + + // Return a stream of chunked decompressed messages + const stream$ = stream.pipe( + split('\n'), + map((msg) => { + return isCompressionDisabled ? msg : inflateResponse(msg); }), share() ); - // start execution - const msgStreamSub = msgStream.subscribe({ - error: (e) => {}, - complete: () => { - msgStreamSub.unsubscribe(); - }, - }); - return { xhr, - stream: msgStream, + stream: stream$, }; } From 5ff38a122b7fb4b0c77f9b3bbc43c6878ebb060c Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 11 Oct 2021 13:10:52 +0100 Subject: [PATCH 012/287] skip failing es promotion suites (#114471) --- x-pack/test/api_integration/apis/maps/get_tile.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index 03a16175931a5..b153cc1ff030c 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -13,7 +13,8 @@ import { MVT_SOURCE_LAYER_NAME } from '../../../../plugins/maps/common/constants export default function ({ getService }) { const supertest = getService('supertest'); - describe('getTile', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/114471 + describe.skip('getTile', () => { it('should return vector tile containing document', async () => { const resp = await supertest .get( From 76546920fc3daab9fb3ddb891e1973775f4c61aa Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 11 Oct 2021 13:17:33 +0100 Subject: [PATCH 013/287] skip failing es promotion suites (#114473, #114474) --- .../functional/apps/index_lifecycle_management/home_page.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts index c51e2968baee0..e2540d80280c2 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/home_page.ts @@ -16,7 +16,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const retry = getService('retry'); const esClient = getService('es'); - describe('Home page', function () { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/114473 and https://github.com/elastic/kibana/issues/114474 + describe.skip('Home page', function () { before(async () => { await pageObjects.common.navigateToApp('indexLifecycleManagement'); }); From c34e99ee73ede89c3425dfdd13ad05747637c4b7 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 11 Oct 2021 13:37:04 +0100 Subject: [PATCH 014/287] skip flaky suites (#100951) --- .../__jest__/client_integration/follower_indices_list.test.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js index b9e47b029e302..bcd4aaf82eeb3 100644 --- a/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js +++ b/x-pack/plugins/cross_cluster_replication/public/__jest__/client_integration/follower_indices_list.test.js @@ -314,7 +314,8 @@ describe('', () => { }); }); - describe('detail panel', () => { + // FLAKY: https://github.com/elastic/kibana/issues/100951 + describe.skip('detail panel', () => { test('should open a detail panel when clicking on a follower index', async () => { expect(exists('followerIndexDetail')).toBe(false); From f2bfa595ee4cbc4cdea33947f8c539c40ffbf1ed Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 11 Oct 2021 13:50:27 +0100 Subject: [PATCH 015/287] skip flaky suite (#106053) --- .../test/functional/apps/ml/anomaly_detection/custom_urls.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts index 0dcb767309608..7d4df75ccdcf7 100644 --- a/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts +++ b/x-pack/test/functional/apps/ml/anomaly_detection/custom_urls.ts @@ -81,7 +81,8 @@ export default function ({ getService }: FtrProviderContext) { const ml = getService('ml'); const browser = getService('browser'); - describe('custom urls', function () { + // FLAKY: https://github.com/elastic/kibana/issues/106053 + describe.skip('custom urls', function () { this.tags(['mlqa']); before(async () => { await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); From 2ffbf6e58eadade0bb9b5386de7d3406d3cc5ae8 Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Mon, 11 Oct 2021 15:20:27 +0200 Subject: [PATCH 016/287] [Security Solution] Add host isolation exception IPs UI (#113762) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../host_isolation_exceptions/service.ts | 14 ++ .../host_isolation_exceptions/store/action.ts | 17 +- .../store/builders.ts | 4 + .../store/middleware.test.ts | 80 ++++++- .../store/middleware.ts | 46 +++- .../store/reducer.test.ts | 10 + .../store/reducer.ts | 18 ++ .../pages/host_isolation_exceptions/types.ts | 5 + .../pages/host_isolation_exceptions/utils.ts | 41 ++++ .../view/components/empty.tsx | 12 +- .../view/components/form.test.tsx | 75 +++++++ .../view/components/form.tsx | 206 ++++++++++++++++++ .../view/components/form_flyout.test.tsx | 114 ++++++++++ .../view/components/form_flyout.tsx | 180 +++++++++++++++ .../view/components/translations.ts | 64 ++++++ .../host_isolation_exceptions_list.test.tsx | 22 +- .../view/host_isolation_exceptions_list.tsx | 33 ++- 17 files changed, 925 insertions(+), 16 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts index 79ca595fbb61b..8af353a3c9531 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/service.ts @@ -6,6 +6,7 @@ */ import { + CreateExceptionListItemSchema, ExceptionListItemSchema, FoundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; @@ -65,6 +66,19 @@ export async function getHostIsolationExceptionItems({ return entries; } +export async function createHostIsolationExceptionItem({ + http, + exception, +}: { + http: HttpStart; + exception: CreateExceptionListItemSchema; +}): Promise { + await ensureHostIsolationExceptionsListExists(http); + return http.post(EXCEPTION_LIST_ITEM_URL, { + body: JSON.stringify(exception), + }); +} + export async function deleteHostIsolationExceptionItems(http: HttpStart, id: string) { await ensureHostIsolationExceptionsListExists(http); return http.delete(EXCEPTION_LIST_ITEM_URL, { diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts index 0a9f776655371..a5fae36486f98 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/action.ts @@ -14,6 +14,20 @@ export type HostIsolationExceptionsPageDataChanged = payload: HostIsolationExceptionsPageState['entries']; }; +export type HostIsolationExceptionsFormStateChanged = + Action<'hostIsolationExceptionsFormStateChanged'> & { + payload: HostIsolationExceptionsPageState['form']['status']; + }; + +export type HostIsolationExceptionsFormEntryChanged = + Action<'hostIsolationExceptionsFormEntryChanged'> & { + payload: HostIsolationExceptionsPageState['form']['entry']; + }; + +export type HostIsolationExceptionsCreateEntry = Action<'hostIsolationExceptionsCreateEntry'> & { + payload: HostIsolationExceptionsPageState['form']['entry']; +}; + export type HostIsolationExceptionsDeleteItem = Action<'hostIsolationExceptionsMarkToDelete'> & { payload?: ExceptionListItemSchema; }; @@ -24,9 +38,10 @@ export type HostIsolationExceptionsDeleteStatusChanged = Action<'hostIsolationExceptionsDeleteStatusChanged'> & { payload: HostIsolationExceptionsPageState['deletion']['status']; }; - export type HostIsolationExceptionsPageAction = | HostIsolationExceptionsPageDataChanged + | HostIsolationExceptionsCreateEntry + | HostIsolationExceptionsFormStateChanged | HostIsolationExceptionsDeleteItem | HostIsolationExceptionsSubmitDelete | HostIsolationExceptionsDeleteStatusChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts index 68a50f9c813f4..8f32d9cf8d456 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/builders.ts @@ -16,6 +16,10 @@ export const initialHostIsolationExceptionsPageState = (): HostIsolationExceptio page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, filter: '', }, + form: { + entry: undefined, + status: createUninitialisedResourceState(), + }, deletion: { item: undefined, status: createUninitialisedResourceState(), diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts index 984794e074ebb..266853fdab5e2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.test.ts @@ -5,10 +5,11 @@ * 2.0. */ +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { applyMiddleware, createStore, Store } from 'redux'; -import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; import { coreMock } from '../../../../../../../../src/core/public/mocks'; import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; import { AppAction } from '../../../../common/store/actions'; import { createSpyMiddleware, @@ -19,8 +20,13 @@ import { isLoadedResourceState, isLoadingResourceState, } from '../../../state'; -import { getHostIsolationExceptionItems, deleteHostIsolationExceptionItems } from '../service'; +import { + createHostIsolationExceptionItem, + deleteHostIsolationExceptionItems, + getHostIsolationExceptionItems, +} from '../service'; import { HostIsolationExceptionsPageState } from '../types'; +import { createEmptyHostIsolationException } from '../utils'; import { initialHostIsolationExceptionsPageState } from './builders'; import { createHostIsolationExceptionsPageMiddleware } from './middleware'; import { hostIsolationExceptionsPageReducer } from './reducer'; @@ -29,6 +35,7 @@ import { getListFetchError } from './selector'; jest.mock('../service'); const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; const deleteHostIsolationExceptionItemsMock = deleteHostIsolationExceptionItems as jest.Mock; +const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.Mock; const fakeCoreStart = coreMock.createStart({ basePath: '/mock' }); @@ -81,7 +88,7 @@ describe('Host isolation exceptions middleware', () => { }; beforeEach(() => { - getHostIsolationExceptionItemsMock.mockClear(); + getHostIsolationExceptionItemsMock.mockReset(); getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); }); @@ -145,11 +152,74 @@ describe('Host isolation exceptions middleware', () => { }); }); + describe('When adding an item to host isolation exceptions', () => { + let entry: CreateExceptionListItemSchema; + beforeEach(() => { + createHostIsolationExceptionItemMock.mockReset(); + entry = { + ...createEmptyHostIsolationException(), + name: 'test name', + description: 'description', + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: '10.0.0.1', + }, + ], + }; + }); + it('should dispatch a form loading state when an entry is submited', async () => { + const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', { + validate({ payload }) { + return isLoadingResourceState(payload); + }, + }); + store.dispatch({ + type: 'hostIsolationExceptionsCreateEntry', + payload: entry, + }); + await waiter; + }); + it('should dispatch a form success state when an entry is confirmed by the API', async () => { + const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', { + validate({ payload }) { + return isLoadedResourceState(payload); + }, + }); + store.dispatch({ + type: 'hostIsolationExceptionsCreateEntry', + payload: entry, + }); + await waiter; + expect(createHostIsolationExceptionItemMock).toHaveBeenCalledWith({ + http: fakeCoreStart.http, + exception: entry, + }); + }); + it('should dispatch a form failure state when an entry is rejected by the API', async () => { + createHostIsolationExceptionItemMock.mockRejectedValue({ + body: { message: 'error message', statusCode: 500, error: 'Not today' }, + }); + const waiter = spyMiddleware.waitForAction('hostIsolationExceptionsFormStateChanged', { + validate({ payload }) { + return isFailedResourceState(payload); + }, + }); + store.dispatch({ + type: 'hostIsolationExceptionsCreateEntry', + payload: entry, + }); + await waiter; + }); + }); + describe('When deleting an item from host isolation exceptions', () => { beforeEach(() => { - deleteHostIsolationExceptionItemsMock.mockClear(); + deleteHostIsolationExceptionItemsMock.mockReset(); deleteHostIsolationExceptionItemsMock.mockReturnValue(undefined); - getHostIsolationExceptionItemsMock.mockClear(); + getHostIsolationExceptionItemsMock.mockReset(); getHostIsolationExceptionItemsMock.mockImplementation(getFoundExceptionListItemSchemaMock); store.dispatch({ type: 'hostIsolationExceptionsMarkToDelete', diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts index 4946cac488700..bbc754e8155b0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/middleware.ts @@ -6,11 +6,13 @@ */ import { + CreateExceptionListItemSchema, ExceptionListItemSchema, FoundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { CoreStart, HttpSetup, HttpStart } from 'kibana/public'; import { matchPath } from 'react-router-dom'; +import { transformNewItemOutput } from '@kbn/securitysolution-list-hooks'; import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; import { ImmutableMiddleware, ImmutableMiddlewareAPI } from '../../../../common/store'; import { AppAction } from '../../../../common/store/actions'; @@ -20,7 +22,11 @@ import { createFailedResourceState, createLoadedResourceState, } from '../../../state/async_resource_builders'; -import { deleteHostIsolationExceptionItems, getHostIsolationExceptionItems } from '../service'; +import { + deleteHostIsolationExceptionItems, + getHostIsolationExceptionItems, + createHostIsolationExceptionItem, +} from '../service'; import { HostIsolationExceptionsPageState } from '../types'; import { getCurrentListPageDataState, getCurrentLocation, getItemToDelete } from './selector'; @@ -39,12 +45,50 @@ export const createHostIsolationExceptionsPageMiddleware = ( if (action.type === 'userChangedUrl' && isHostIsolationExceptionsPage(action.payload)) { loadHostIsolationExceptionsList(store, coreStart.http); } + + if (action.type === 'hostIsolationExceptionsCreateEntry') { + createHostIsolationException(store, coreStart.http); + } + if (action.type === 'hostIsolationExceptionsSubmitDelete') { deleteHostIsolationExceptionsItem(store, coreStart.http); } }; }; +async function createHostIsolationException( + store: ImmutableMiddlewareAPI, + http: HttpStart +) { + const { dispatch } = store; + const entry = transformNewItemOutput( + store.getState().form.entry as CreateExceptionListItemSchema + ); + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'LoadingResourceState', + // @ts-expect-error-next-line will be fixed with when AsyncResourceState is refactored (#830) + previousState: entry, + }, + }); + try { + const response = await createHostIsolationExceptionItem({ + http, + exception: entry, + }); + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: createLoadedResourceState(response), + }); + } catch (error) { + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: createFailedResourceState(error.body ?? error), + }); + } +} + async function loadHostIsolationExceptionsList( store: ImmutableMiddlewareAPI, http: HttpStart diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts index 211b03f36d965..98b459fac41d3 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.test.ts @@ -11,6 +11,7 @@ import { initialHostIsolationExceptionsPageState } from './builders'; import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; import { hostIsolationExceptionsPageReducer } from './reducer'; import { getCurrentLocation } from './selector'; +import { createEmptyHostIsolationException } from '../utils'; describe('Host Isolation Exceptions Reducer', () => { let initialState: HostIsolationExceptionsPageState; @@ -41,4 +42,13 @@ describe('Host Isolation Exceptions Reducer', () => { }); }); }); + it('should set an initial loading state when creating new entries', () => { + const entry = createEmptyHostIsolationException(); + const result = hostIsolationExceptionsPageReducer(initialState, { + type: 'hostIsolationExceptionsCreateEntry', + payload: entry, + }); + expect(result.form.status).toEqual({ type: 'UninitialisedResourceState' }); + expect(result.form.entry).toBe(entry); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts index 09182661a80b3..d97295598f445 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/store/reducer.ts @@ -38,6 +38,24 @@ export const hostIsolationExceptionsPageReducer: StateReducer = ( action ) => { switch (action.type) { + case 'hostIsolationExceptionsCreateEntry': { + return { + ...state, + form: { + entry: action.payload, + status: createUninitialisedResourceState(), + }, + }; + } + case 'hostIsolationExceptionsFormStateChanged': { + return { + ...state, + form: { + ...state.form, + status: action.payload, + }, + }; + } case 'hostIsolationExceptionsPageDataChanged': { return { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts index 443a86fefab83..1a74042fb652e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/types.ts @@ -6,6 +6,7 @@ */ import type { + CreateExceptionListItemSchema, ExceptionListItemSchema, FoundExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; @@ -27,4 +28,8 @@ export interface HostIsolationExceptionsPageState { item?: ExceptionListItemSchema; status: AsyncResourceState; }; + form: { + entry?: CreateExceptionListItemSchema; + status: AsyncResourceState; + }; } diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts new file mode 100644 index 0000000000000..bfb1ac048e286 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/utils.ts @@ -0,0 +1,41 @@ +/* + * 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 { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import ipaddr from 'ipaddr.js'; + +export function createEmptyHostIsolationException(): CreateExceptionListItemSchema { + return { + comments: [], + description: '', + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: '', + }, + ], + item_id: undefined, + list_id: ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID, + name: '', + namespace_type: 'agnostic', + os_types: ['windows', 'linux', 'macos'], + tags: ['policy:all'], + type: 'simple', + }; +} + +export function isValidIPv4OrCIDR(maybeIp: string): boolean { + try { + ipaddr.IPv4.parseCIDR(maybeIp); + return true; + } catch (e) { + return ipaddr.IPv4.isValid(maybeIp); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx index d7c512794173c..eb53268a9fbd8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/empty.tsx @@ -7,7 +7,7 @@ import React, { memo } from 'react'; import styled, { css } from 'styled-components'; -import { EuiEmptyPrompt } from '@elastic/eui'; +import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; const EmptyPrompt = styled(EuiEmptyPrompt)` @@ -16,7 +16,7 @@ const EmptyPrompt = styled(EuiEmptyPrompt)` `} `; -export const HostIsolationExceptionsEmptyState = memo<{}>(() => { +export const HostIsolationExceptionsEmptyState = memo<{ onAdd: () => void }>(({ onAdd }) => { return ( (() => { defaultMessage="There are currently no host isolation exceptions" /> } + actions={ + + + + } /> ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx new file mode 100644 index 0000000000000..b06449de69d8c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -0,0 +1,75 @@ +/* + * 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 { createEmptyHostIsolationException } from '../../utils'; +import { HostIsolationExceptionsForm } from './form'; +import React from 'react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import userEvent from '@testing-library/user-event'; + +describe('When on the host isolation exceptions add entry form', () => { + let render: ( + exception: CreateExceptionListItemSchema + ) => ReturnType; + let renderResult: ReturnType; + const onChange = jest.fn(); + const onError = jest.fn(); + + beforeEach(() => { + onChange.mockReset(); + onError.mockReset(); + const mockedContext = createAppRootMockRenderer(); + render = (exception: CreateExceptionListItemSchema) => { + return mockedContext.render( + + ); + }; + }); + + describe('When creating a new exception', () => { + let newException: CreateExceptionListItemSchema; + beforeEach(() => { + newException = createEmptyHostIsolationException(); + renderResult = render(newException); + }); + it('should render the form with empty inputs', () => { + expect(renderResult.getByTestId('hostIsolationExceptions-form-name-input')).toHaveValue(''); + expect(renderResult.getByTestId('hostIsolationExceptions-form-ip-input')).toHaveValue(''); + expect( + renderResult.getByTestId('hostIsolationExceptions-form-description-input') + ).toHaveValue(''); + }); + it('should call onError with true when a wrong ip value is introduced', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + userEvent.type(ipInput, 'not an ip'); + expect(onError).toHaveBeenCalledWith(true); + }); + it('should call onError with false when a correct values are introduced', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + const nameInput = renderResult.getByTestId('hostIsolationExceptions-form-name-input'); + + userEvent.type(nameInput, 'test name'); + userEvent.type(ipInput, '10.0.0.1'); + + expect(onError).toHaveBeenLastCalledWith(false); + }); + it('should call onChange when a value is introduced in a field', () => { + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + userEvent.type(ipInput, '10.0.0.1'); + expect(onChange).toHaveBeenLastCalledWith({ + ...newException, + entries: [ + { field: 'destination.ip', operator: 'included', type: 'match', value: '10.0.0.1' }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx new file mode 100644 index 0000000000000..84263f9d07c81 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.tsx @@ -0,0 +1,206 @@ +/* + * 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 { + EuiFieldText, + EuiForm, + EuiFormRow, + EuiHorizontalRule, + EuiSpacer, + EuiText, + EuiTextArea, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { isValidIPv4OrCIDR } from '../../utils'; +import { + DESCRIPTION_LABEL, + DESCRIPTION_PLACEHOLDER, + IP_ERROR, + IP_LABEL, + IP_PLACEHOLDER, + NAME_ERROR, + NAME_LABEL, + NAME_PLACEHOLDER, +} from './translations'; + +interface ExceptionIpEntry { + field: 'destination.ip'; + operator: 'included'; + type: 'match'; + value: ''; +} + +export const HostIsolationExceptionsForm: React.FC<{ + exception: CreateExceptionListItemSchema; + onError: (error: boolean) => void; + onChange: (exception: CreateExceptionListItemSchema) => void; +}> = memo(({ exception, onError, onChange }) => { + const [hasBeenInputNameVisited, setHasBeenInputNameVisited] = useState(false); + const [hasBeenInputIpVisited, setHasBeenInputIpVisited] = useState(false); + const [hasNameError, setHasNameError] = useState(true); + const [hasIpError, setHasIpError] = useState(true); + + useEffect(() => { + onError(hasNameError || hasIpError); + }, [hasNameError, hasIpError, onError]); + + const handleOnChangeName = useCallback( + (event: React.ChangeEvent) => { + const name = event.target.value; + if (!name.trim()) { + setHasNameError(true); + return; + } + setHasNameError(false); + onChange({ ...exception, name }); + }, + [exception, onChange] + ); + + const handleOnIpChange = useCallback( + (event: React.ChangeEvent) => { + const ip = event.target.value; + if (!isValidIPv4OrCIDR(ip)) { + setHasIpError(true); + return; + } + setHasIpError(false); + onChange({ + ...exception, + entries: [ + { + field: 'destination.ip', + operator: 'included', + type: 'match', + value: ip, + }, + ], + }); + }, + [exception, onChange] + ); + + const handleOnDescriptionChange = useCallback( + (event: React.ChangeEvent) => { + onChange({ ...exception, description: event.target.value }); + }, + [exception, onChange] + ); + + const nameInput = useMemo( + () => ( + + !hasBeenInputNameVisited && setHasBeenInputNameVisited(true)} + /> + + ), + [hasNameError, hasBeenInputNameVisited, exception.name, handleOnChangeName] + ); + + const ipInput = useMemo( + () => ( + + !hasBeenInputIpVisited && setHasBeenInputIpVisited(true)} + /> + + ), + [hasIpError, hasBeenInputIpVisited, exception.entries, handleOnIpChange] + ); + + const descriptionInput = useMemo( + () => ( + + + + ), + [exception.description, handleOnDescriptionChange] + ); + + return ( + + +

+ +

+
+ + + + + {nameInput} + {descriptionInput} + + + +

+ +

+
+ + + + + {ipInput} +
+ ); +}); + +HostIsolationExceptionsForm.displayName = 'HostIsolationExceptionsForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx new file mode 100644 index 0000000000000..6cfc9f56beadf --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.test.tsx @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../common/mock/endpoint'; +import userEvent from '@testing-library/user-event'; +import { HostIsolationExceptionsFormFlyout } from './form_flyout'; +import { act } from 'react-dom/test-utils'; +import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../../common/constants'; + +jest.mock('../../service.ts'); + +describe('When on the host isolation exceptions flyout form', () => { + let mockedContext: AppContextTestRender; + let render: () => ReturnType; + let renderResult: ReturnType; + let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + + // const createHostIsolationExceptionItemMock = createHostIsolationExceptionItem as jest.mock; + + beforeEach(() => { + mockedContext = createAppRootMockRenderer(); + render = () => { + return mockedContext.render(); + }; + waitForAction = mockedContext.middlewareSpy.waitForAction; + }); + + describe('When creating a new exception', () => { + describe('with invalid data', () => { + it('should show disabled buttons when the form first load', () => { + renderResult = render(); + expect(renderResult.getByTestId('add-exception-cancel-button')).not.toHaveAttribute( + 'disabled' + ); + expect(renderResult.getByTestId('add-exception-confirm-button')).toHaveAttribute( + 'disabled' + ); + }); + }); + describe('with valid data', () => { + beforeEach(() => { + renderResult = render(); + const ipInput = renderResult.getByTestId('hostIsolationExceptions-form-ip-input'); + const nameInput = renderResult.getByTestId('hostIsolationExceptions-form-name-input'); + userEvent.type(nameInput, 'test name'); + userEvent.type(ipInput, '10.0.0.1'); + }); + it('should show enable buttons when the form is valid', () => { + expect(renderResult.getByTestId('add-exception-cancel-button')).not.toHaveAttribute( + 'disabled' + ); + expect(renderResult.getByTestId('add-exception-confirm-button')).not.toHaveAttribute( + 'disabled' + ); + }); + it('should submit the entry data when submit is pressed with valid data', async () => { + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).not.toHaveAttribute('disabled'); + const waiter = waitForAction('hostIsolationExceptionsCreateEntry'); + userEvent.click(confirmButton); + await waiter; + }); + it('should disable the submit button when an operation is in progress', () => { + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'LoadingResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).toHaveAttribute('disabled'); + }); + it('should show a toast and close the flyout when the operation is finished', () => { + mockedContext.history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'LoadedResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + expect(mockedContext.coreStart.notifications.toasts.addSuccess).toHaveBeenCalled(); + expect(mockedContext.history.location.search).toBe(''); + }); + it('should show an error toast operation fails and enable the submit button', () => { + act(() => { + mockedContext.store.dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'FailedResourceState', + previousState: { type: 'UninitialisedResourceState' }, + }, + }); + }); + expect(mockedContext.coreStart.notifications.toasts.addDanger).toHaveBeenCalled(); + const confirmButton = renderResult.getByTestId('add-exception-confirm-button'); + expect(confirmButton).not.toHaveAttribute('disabled'); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx new file mode 100644 index 0000000000000..5502a1b8ea2b1 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form_flyout.tsx @@ -0,0 +1,180 @@ +/* + * 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, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CreateExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import React, { memo, useCallback, useEffect, useMemo, useState } from 'react'; +import { useDispatch } from 'react-redux'; +import { Dispatch } from 'redux'; +import { Loader } from '../../../../../common/components/loader'; +import { useToasts } from '../../../../../common/lib/kibana'; +import { + isFailedResourceState, + isLoadedResourceState, + isLoadingResourceState, +} from '../../../../state/async_resource_state'; +import { HostIsolationExceptionsPageAction } from '../../store/action'; +import { createEmptyHostIsolationException } from '../../utils'; +import { + useHostIsolationExceptionsNavigateCallback, + useHostIsolationExceptionsSelector, +} from '../hooks'; +import { HostIsolationExceptionsForm } from './form'; + +export const HostIsolationExceptionsFormFlyout: React.FC<{}> = memo(() => { + const dispatch = useDispatch>(); + const toasts = useToasts(); + + const creationInProgress = useHostIsolationExceptionsSelector((state) => + isLoadingResourceState(state.form.status) + ); + const creationSuccessful = useHostIsolationExceptionsSelector((state) => + isLoadedResourceState(state.form.status) + ); + const creationFailure = useHostIsolationExceptionsSelector((state) => + isFailedResourceState(state.form.status) + ); + + const navigateCallback = useHostIsolationExceptionsNavigateCallback(); + + const [formHasError, setFormHasError] = useState(true); + const [exception, setException] = useState(undefined); + + const onCancel = useCallback( + () => + navigateCallback({ + show: undefined, + id: undefined, + }), + [navigateCallback] + ); + + useEffect(() => { + setException(createEmptyHostIsolationException()); + }, []); + + useEffect(() => { + if (creationSuccessful) { + onCancel(); + dispatch({ + type: 'hostIsolationExceptionsFormStateChanged', + payload: { + type: 'UninitialisedResourceState', + }, + }); + toasts.addSuccess( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.creationSuccessToastTitle', + { + defaultMessage: '"{name}" has been added to the host isolation exceptions list.', + values: { name: exception?.name }, + } + ) + ); + } + }, [creationSuccessful, onCancel, dispatch, toasts, exception?.name]); + + useEffect(() => { + if (creationFailure) { + toasts.addDanger( + i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.creationFailureToastTitle', + { + defaultMessage: 'There was an error creating the exception', + } + ) + ); + } + }, [dispatch, toasts, creationFailure]); + + const handleOnCancel = useCallback(() => { + if (creationInProgress) return; + onCancel(); + }, [creationInProgress, onCancel]); + + const handleOnSubmit = useCallback(() => { + dispatch({ + type: 'hostIsolationExceptionsCreateEntry', + payload: exception, + }); + }, [dispatch, exception]); + + const confirmButtonMemo = useMemo( + () => ( + + + + ), + [formHasError, creationInProgress, handleOnSubmit] + ); + + return exception ? ( + + + +

+ +

+
+
+ + + + + + + + + + + + + {confirmButtonMemo} + + +
+ ) : ( + + ); +}); + +HostIsolationExceptionsFormFlyout.displayName = 'HostIsolationExceptionsFormFlyout'; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts new file mode 100644 index 0000000000000..df179c7a2221c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/translations.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const NAME_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.placeholder', + { + defaultMessage: 'New IP', + } +); + +export const NAME_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.label', + { + defaultMessage: 'Name your host isolation exceptions', + } +); + +export const NAME_ERROR = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.name.error', + { + defaultMessage: "The name can't be empty", + } +); + +export const DESCRIPTION_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.description.placeholder', + { + defaultMessage: 'Describe your Host Isolation Exception', + } +); + +export const DESCRIPTION_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.description.label', + { + defaultMessage: 'Description', + } +); + +export const IP_PLACEHOLDER = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.placeholder', + { + defaultMessage: 'Ex 0.0.0.0/24', + } +); + +export const IP_LABEL = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.label', + { + defaultMessage: 'Enter IP Address', + } +); + +export const IP_ERROR = i18n.translate( + 'xpack.securitySolution.hostIsolationExceptions.form.ip.error', + { + defaultMessage: 'The ip is invalid. Only IPv4 with optional CIDR is supported', + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index 53b8bc33c252f..ac472fdae4d7b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -5,16 +5,18 @@ * 2.0. */ -import React from 'react'; import { act } from '@testing-library/react'; -import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; +import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; import { HOST_ISOLATION_EXCEPTIONS_PATH } from '../../../../../common/constants'; -import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../../common/mock/endpoint'; import { isFailedResourceState, isLoadedResourceState } from '../../../state'; import { getHostIsolationExceptionItems } from '../service'; -import { getFoundExceptionListItemSchemaMock } from '../../../../../../lists/common/schemas/response/found_exception_list_item_schema.mock'; +import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; jest.mock('../service'); + const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; describe('When on the host isolation exceptions page', () => { @@ -103,5 +105,17 @@ describe('When on the host isolation exceptions page', () => { ).toEqual(' Server is too far away'); }); }); + it('should show the create flyout when the add button is pressed', () => { + render(); + act(() => { + userEvent.click(renderResult.getByTestId('hostIsolationExceptionsListAddButton')); + }); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + }); + it('should show the create flyout when the show location is create', () => { + history.push(`${HOST_ISOLATION_EXCEPTIONS_PATH}?show=create`); + render(); + expect(renderResult.getByTestId('hostIsolationExceptionsCreateEditFlyout')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx index 53fb74d5bd8f7..cfb0121396e24 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.tsx @@ -8,7 +8,7 @@ import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; import { i18n } from '@kbn/i18n'; import React, { Dispatch, useCallback } from 'react'; -import { EuiSpacer } from '@elastic/eui'; +import { EuiButton, EuiSpacer } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { ExceptionItem } from '../../../../common/components/exceptions/viewer/exception_item'; @@ -32,6 +32,7 @@ import { ArtifactEntryCard, ArtifactEntryCardProps } from '../../../components/a import { HostIsolationExceptionsEmptyState } from './components/empty'; import { HostIsolationExceptionsPageAction } from '../store/action'; import { HostIsolationExceptionDeleteModal } from './components/delete_modal'; +import { HostIsolationExceptionsFormFlyout } from './components/form_flyout'; type HostIsolationExceptionPaginatedContent = PaginatedContentProps< Immutable, @@ -54,6 +55,8 @@ export const HostIsolationExceptionsList = () => { const dispatch = useDispatch>(); const itemToDelete = useHostIsolationExceptionsSelector(getItemToDelete); + const showFlyout = !!location.show; + const navigateCallback = useHostIsolationExceptionsNavigateCallback(); const handleOnSearch = useCallback( @@ -92,6 +95,15 @@ export const HostIsolationExceptionsList = () => { [navigateCallback] ); + const handleAddButtonClick = useCallback( + () => + navigateCallback({ + show: 'create', + id: undefined, + }), + [navigateCallback] + ); + return ( { defaultMessage="Host Isolation Exceptions" /> } - actions={[]} + actions={ + + + + } > + {showFlyout && } + { pagination={pagination} contentClassName="host-isolation-exceptions-container" data-test-subj="hostIsolationExceptionsContent" - noItemsMessage={} + noItemsMessage={} /> ); From 7c5c566696fa3837b48d6147ae61dd97526f1a1d Mon Sep 17 00:00:00 2001 From: Orhan Toy Date: Mon, 11 Oct 2021 15:52:15 +0200 Subject: [PATCH 017/287] [Enterprise Search] Fix typo (#114462) Fixing a small typo (`recieve` -> `receive`) I noticed in comments. --- .../crawler_status_indicator/crawler_status_indicator.test.tsx | 2 +- .../applications/shared/flash_messages/handle_api_errors.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx index 9d585789d8e50..c46c360934d0b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/crawler/components/crawler_status_indicator/crawler_status_indicator.test.tsx @@ -37,7 +37,7 @@ describe('CrawlerStatusIndicator', () => { describe('when status is not a valid status', () => { it('is disabled', () => { // this tests a codepath that should be impossible to reach, status should always be a CrawlerStatus - // but we use a switch statement and need to test the default case for this to recieve 100% coverage + // but we use a switch statement and need to test the default case for this to receive 100% coverage setMockValues({ ...MOCK_VALUES, mostRecentCrawlRequestStatus: null, diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts index 4fe8ad1cb851d..abaa67e06f606 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/flash_messages/handle_api_errors.ts @@ -14,7 +14,7 @@ import { IFlashMessage } from './types'; /** * The API errors we are handling can come from one of two ways: - * - When our http calls recieve a response containing an error code, such as a 404 or 500 + * - When our http calls receive a response containing an error code, such as a 404 or 500 * - Our own JS while handling a successful response * * In the first case, if it is a purposeful error (like a 404) we will receive an From 53109bdcd5fb00c8e4287facd03ed923362f8540 Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Mon, 11 Oct 2021 15:20:39 +0100 Subject: [PATCH 018/287] Detection Rule Exception List telemetry (#113239) * Add telemetry for detection rule exception lists to improve UX. * Add length for debugging. * Fix type. * Clean up exception list telemetry document. * Dynamically set kibana index (just in case). * Update task title. * Rename version to rule_version. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/lib/telemetry/constants.ts | 6 +- .../server/lib/telemetry/filters.ts | 5 +- .../server/lib/telemetry/helpers.test.ts | 80 +++++++--- .../server/lib/telemetry/helpers.ts | 48 ++++-- .../server/lib/telemetry/mocks.ts | 11 +- .../server/lib/telemetry/receiver.ts | 68 +++++++- .../server/lib/telemetry/sender.ts | 10 +- .../telemetry/tasks/detection_rule.test.ts | 127 +++++++++++++++ .../lib/telemetry/tasks/detection_rule.ts | 149 ++++++++++++++++++ .../server/lib/telemetry/tasks/endpoint.ts | 4 +- .../server/lib/telemetry/tasks/index.ts | 1 + .../server/lib/telemetry/types.ts | 39 ++++- .../security_solution/server/plugin.ts | 8 +- 13 files changed, 499 insertions(+), 57 deletions(-) create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.test.ts create mode 100644 x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts index 771e3e059c336..91f83d5e7cb37 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/constants.ts @@ -7,12 +7,14 @@ export const TELEMETRY_MAX_BUFFER_SIZE = 100; -export const TELEMETRY_CHANNEL_LISTS = 'security-lists'; +export const TELEMETRY_CHANNEL_LISTS = 'security-lists-v2'; export const TELEMETRY_CHANNEL_ENDPOINT_META = 'endpoint-metadata'; -export const LIST_TRUSTED_APPLICATION = 'trusted_application'; +export const LIST_DETECTION_RULE_EXCEPTION = 'detection_rule_exception'; export const LIST_ENDPOINT_EXCEPTION = 'endpoint_exception'; export const LIST_ENDPOINT_EVENT_FILTER = 'endpoint_event_filter'; + +export const LIST_TRUSTED_APPLICATION = 'trusted_application'; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts index 61172fac511f7..a29f195ed5ecc 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts @@ -129,13 +129,12 @@ export const allowlistEventFields: AllowlistFields = { export const exceptionListEventFields: AllowlistFields = { created_at: true, - description: true, effectScope: true, entries: true, id: true, name: true, - os: true, os_types: true, + rule_version: true, }; /** @@ -143,7 +142,7 @@ export const exceptionListEventFields: AllowlistFields = { * * @param allowlist * @param event - * @returns + * @returns TelemetryEvent with explicitly required fields */ export function copyAllowlistedFields( allowlist: AllowlistFields, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts index 647219e8c5585..528082d8cb5b1 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.test.ts @@ -8,13 +8,14 @@ import moment from 'moment'; import { createMockPackagePolicy } from './mocks'; import { + LIST_DETECTION_RULE_EXCEPTION, LIST_ENDPOINT_EXCEPTION, LIST_ENDPOINT_EVENT_FILTER, LIST_TRUSTED_APPLICATION, } from './constants'; import { getPreviousDiagTaskTimestamp, - getPreviousEpMetaTaskTimestamp, + getPreviousDailyTaskTimestamp, batchTelemetryRecords, isPackagePolicyList, templateExceptionList, @@ -53,7 +54,7 @@ describe('test endpoint meta telemetry scheduled task timing helper', () => { test('test -24 hours is returned when there is no previous task run', async () => { const executeTo = moment().utc().toISOString(); const executeFrom = undefined; - const newExecuteFrom = getPreviousEpMetaTaskTimestamp(executeTo, executeFrom); + const newExecuteFrom = getPreviousDailyTaskTimestamp(executeTo, executeFrom); expect(newExecuteFrom).toEqual(moment(executeTo).subtract(24, 'hours').toISOString()); }); @@ -61,7 +62,7 @@ describe('test endpoint meta telemetry scheduled task timing helper', () => { test('test -24 hours is returned when there was a previous task run', async () => { const executeTo = moment().utc().toISOString(); const executeFrom = moment(executeTo).subtract(24, 'hours').toISOString(); - const newExecuteFrom = getPreviousEpMetaTaskTimestamp(executeTo, executeFrom); + const newExecuteFrom = getPreviousDailyTaskTimestamp(executeTo, executeFrom); expect(newExecuteFrom).toEqual(executeFrom); }); @@ -71,7 +72,7 @@ describe('test endpoint meta telemetry scheduled task timing helper', () => { test('test 24 hours is returned when previous task run took longer than 24 hours', async () => { const executeTo = moment().utc().toISOString(); const executeFrom = moment(executeTo).subtract(72, 'hours').toISOString(); // down 3 days - const newExecuteFrom = getPreviousEpMetaTaskTimestamp(executeTo, executeFrom); + const newExecuteFrom = getPreviousDailyTaskTimestamp(executeTo, executeFrom); expect(newExecuteFrom).toEqual(moment(executeTo).subtract(24, 'hours').toISOString()); }); @@ -134,61 +135,88 @@ describe('test package policy type guard', () => { }); describe('list telemetry schema', () => { + test('detection rules document is correctly formed', () => { + const data = [{ id: 'test_1' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_DETECTION_RULE_EXCEPTION); + + expect(templatedItems[0]?.detection_rule).not.toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); + }); + + test('detection rules document is correctly formed with multiple entries', () => { + const data = [{ id: 'test_2' }, { id: 'test_2' }] as ExceptionListItem[]; + const templatedItems = templateExceptionList(data, LIST_DETECTION_RULE_EXCEPTION); + + expect(templatedItems[0]?.detection_rule).not.toBeUndefined(); + expect(templatedItems[1]?.detection_rule).not.toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); + }); + test('trusted apps document is correctly formed', () => { const data = [{ id: 'test_1' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_TRUSTED_APPLICATION); - expect(templatedItems[0]?.trusted_application.length).toEqual(1); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(0); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).not.toBeUndefined(); }); test('trusted apps document is correctly formed with multiple entries', () => { const data = [{ id: 'test_2' }, { id: 'test_2' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_TRUSTED_APPLICATION); - expect(templatedItems[0]?.trusted_application.length).toEqual(1); - expect(templatedItems[1]?.trusted_application.length).toEqual(1); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(0); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).not.toBeUndefined(); + expect(templatedItems[1]?.trusted_application).not.toBeUndefined(); }); test('endpoint exception document is correctly formed', () => { const data = [{ id: 'test_3' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EXCEPTION); - expect(templatedItems[0]?.trusted_application.length).toEqual(0); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(1); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(0); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).not.toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); }); test('endpoint exception document is correctly formed with multiple entries', () => { const data = [{ id: 'test_4' }, { id: 'test_4' }, { id: 'test_4' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EXCEPTION); - expect(templatedItems[0]?.trusted_application.length).toEqual(0); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(1); - expect(templatedItems[1]?.endpoint_exception.length).toEqual(1); - expect(templatedItems[2]?.endpoint_exception.length).toEqual(1); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(0); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).not.toBeUndefined(); + expect(templatedItems[1]?.endpoint_exception).not.toBeUndefined(); + expect(templatedItems[2]?.endpoint_exception).not.toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); }); test('endpoint event filters document is correctly formed', () => { const data = [{ id: 'test_5' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EVENT_FILTER); - expect(templatedItems[0]?.trusted_application.length).toEqual(0); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(1); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).not.toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); }); test('endpoint event filters document is correctly formed with multiple entries', () => { const data = [{ id: 'test_6' }, { id: 'test_6' }] as ExceptionListItem[]; const templatedItems = templateExceptionList(data, LIST_ENDPOINT_EVENT_FILTER); - expect(templatedItems[0]?.trusted_application.length).toEqual(0); - expect(templatedItems[0]?.endpoint_exception.length).toEqual(0); - expect(templatedItems[0]?.endpoint_event_filter.length).toEqual(1); - expect(templatedItems[1]?.endpoint_event_filter.length).toEqual(1); + expect(templatedItems[0]?.detection_rule).toBeUndefined(); + expect(templatedItems[0]?.endpoint_event_filter).not.toBeUndefined(); + expect(templatedItems[1]?.endpoint_event_filter).not.toBeUndefined(); + expect(templatedItems[0]?.endpoint_exception).toBeUndefined(); + expect(templatedItems[0]?.trusted_application).toBeUndefined(); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index a9eaef3ce6edc..e72b0ba7d16fe 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -11,6 +11,7 @@ import { PackagePolicy } from '../../../../fleet/common/types/models/package_pol import { copyAllowlistedFields, exceptionListEventFields } from './filters'; import { ExceptionListItem, ListTemplate, TelemetryEvent } from './types'; import { + LIST_DETECTION_RULE_EXCEPTION, LIST_ENDPOINT_EXCEPTION, LIST_ENDPOINT_EVENT_FILTER, LIST_TRUSTED_APPLICATION, @@ -46,7 +47,7 @@ export const getPreviousDiagTaskTimestamp = ( * @param lastExecutionTimestamp * @returns the timestamp to search from */ -export const getPreviousEpMetaTaskTimestamp = ( +export const getPreviousDailyTaskTimestamp = ( executeTo: string, lastExecutionTimestamp?: string ) => { @@ -97,18 +98,16 @@ export function isPackagePolicyList( * Maps trusted application to shared telemetry object * * @param exceptionListItem - * @returns collection of endpoint exceptions + * @returns collection of trusted applications */ export const trustedApplicationToTelemetryEntry = (trustedApplication: TrustedApp) => { return { id: trustedApplication.id, - version: trustedApplication.version || '', name: trustedApplication.name, - description: trustedApplication.description, created_at: trustedApplication.created_at, updated_at: trustedApplication.updated_at, entries: trustedApplication.entries, - os: trustedApplication.os, + os_types: [trustedApplication.os], } as ExceptionListItem; }; @@ -121,9 +120,29 @@ export const trustedApplicationToTelemetryEntry = (trustedApplication: TrustedAp export const exceptionListItemToTelemetryEntry = (exceptionListItem: ExceptionListItemSchema) => { return { id: exceptionListItem.id, - version: exceptionListItem._version || '', name: exceptionListItem.name, - description: exceptionListItem.description, + created_at: exceptionListItem.created_at, + updated_at: exceptionListItem.updated_at, + entries: exceptionListItem.entries, + os_types: exceptionListItem.os_types, + } as ExceptionListItem; +}; + +/** + * Maps detection rule exception list items to shared telemetry object + * + * @param exceptionListItem + * @param ruleVersion + * @returns collection of detection rule exceptions + */ +export const ruleExceptionListItemToTelemetryEvent = ( + exceptionListItem: ExceptionListItemSchema, + ruleVersion: number +) => { + return { + id: exceptionListItem.item_id, + name: exceptionListItem.description, + rule_version: ruleVersion, created_at: exceptionListItem.created_at, updated_at: exceptionListItem.updated_at, entries: exceptionListItem.entries, @@ -141,9 +160,7 @@ export const exceptionListItemToTelemetryEntry = (exceptionListItem: ExceptionLi export const templateExceptionList = (listData: ExceptionListItem[], listType: string) => { return listData.map((item) => { const template: ListTemplate = { - trusted_application: [], - endpoint_exception: [], - endpoint_event_filter: [], + '@timestamp': new Date().getTime(), }; // cast exception list type to a TelemetryEvent for allowlist filtering @@ -152,18 +169,23 @@ export const templateExceptionList = (listData: ExceptionListItem[], listType: s item as unknown as TelemetryEvent ); + if (listType === LIST_DETECTION_RULE_EXCEPTION) { + template.detection_rule = filteredListItem; + return template; + } + if (listType === LIST_TRUSTED_APPLICATION) { - template.trusted_application.push(filteredListItem); + template.trusted_application = filteredListItem; return template; } if (listType === LIST_ENDPOINT_EXCEPTION) { - template.endpoint_exception.push(filteredListItem); + template.endpoint_exception = filteredListItem; return template; } if (listType === LIST_ENDPOINT_EVENT_FILTER) { - template.endpoint_event_filter.push(filteredListItem); + template.endpoint_event_filter = filteredListItem; return template; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts b/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts index 20a71657b2ffe..9168683141e48 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/mocks.ts @@ -8,7 +8,7 @@ // eslint-disable-next-line max-classes-per-file import { TelemetryEventsSender } from './sender'; import { TelemetryReceiver } from './receiver'; -import { DiagnosticTask, EndpointTask, ExceptionListsTask } from './tasks'; +import { DiagnosticTask, EndpointTask, ExceptionListsTask, DetectionRulesTask } from './tasks'; import { PackagePolicy } from '../../../../fleet/common/types/models/package_policy'; /** @@ -40,6 +40,8 @@ export const createMockTelemetryReceiver = (): jest.Mocked => fetchEndpointMetrics: jest.fn(), fetchEndpointPolicyResponses: jest.fn(), fetchTrustedApplications: jest.fn(), + fetchDetectionRules: jest.fn(), + fetchDetectionExceptionList: jest.fn(), } as unknown as jest.Mocked; }; @@ -79,3 +81,10 @@ export class MockTelemetryEndpointTask extends EndpointTask { export class MockExceptionListsTask extends ExceptionListsTask { public runTask = jest.fn(); } + +/** + * Creates a mocked Telemetry detection rules lists Task + */ +export class MockDetectionRuleListsTask extends DetectionRulesTask { + public runTask = jest.fn(); +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 038b7687784f4..94aa6c867304f 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -17,8 +17,18 @@ import { AgentService, AgentPolicyServiceInterface } from '../../../../fleet/ser import { ExceptionListClient } from '../../../../lists/server'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; import { TELEMETRY_MAX_BUFFER_SIZE } from './constants'; -import { exceptionListItemToTelemetryEntry, trustedApplicationToTelemetryEntry } from './helpers'; -import { TelemetryEvent, ESLicense, ESClusterInfo, GetEndpointListResponse } from './types'; +import { + exceptionListItemToTelemetryEntry, + trustedApplicationToTelemetryEntry, + ruleExceptionListItemToTelemetryEvent, +} from './helpers'; +import { + TelemetryEvent, + ESLicense, + ESClusterInfo, + GetEndpointListResponse, + RuleSearchResult, +} from './types'; export class TelemetryReceiver { private readonly logger: Logger; @@ -27,6 +37,7 @@ export class TelemetryReceiver { private esClient?: ElasticsearchClient; private exceptionListClient?: ExceptionListClient; private soClient?: SavedObjectsClientContract; + private kibanaIndex?: string; private readonly max_records = 10_000; constructor(logger: Logger) { @@ -35,9 +46,11 @@ export class TelemetryReceiver { public async start( core?: CoreStart, + kibanaIndex?: string, endpointContextService?: EndpointAppContextService, exceptionListClient?: ExceptionListClient ) { + this.kibanaIndex = kibanaIndex; this.agentService = endpointContextService?.getAgentService(); this.agentPolicyService = endpointContextService?.getAgentPolicyService(); this.esClient = core?.elasticsearch.client.asInternalUser; @@ -240,6 +253,57 @@ export class TelemetryReceiver { }; } + public async fetchDetectionRules() { + if (this.esClient === undefined || this.esClient === null) { + throw Error('elasticsearch client is unavailable: cannot retrieve diagnostic alerts'); + } + + const query: SearchRequest = { + expand_wildcards: 'open,hidden', + index: `${this.kibanaIndex}*`, + ignore_unavailable: true, + size: this.max_records, + body: { + query: { + bool: { + filter: [ + { term: { 'alert.alertTypeId': 'siem.signals' } }, + { term: { 'alert.params.immutable': true } }, + ], + }, + }, + }, + }; + + return this.esClient.search(query); + } + + public async fetchDetectionExceptionList(listId: string, ruleVersion: number) { + if (this?.exceptionListClient === undefined || this?.exceptionListClient === null) { + throw Error('exception list client is unavailable: could not retrieve trusted applications'); + } + + // Ensure list is created if it does not exist + await this.exceptionListClient.createTrustedAppsList(); + + const results = await this.exceptionListClient?.findExceptionListsItem({ + listId: [listId], + filter: [], + perPage: this.max_records, + page: 1, + sortField: 'exception-list.created_at', + sortOrder: 'desc', + namespaceType: ['single'], + }); + + return { + data: results?.data.map((r) => ruleExceptionListItemToTelemetryEvent(r, ruleVersion)) ?? [], + total: results?.total ?? 0, + page: results?.page ?? 1, + per_page: results?.per_page ?? this.max_records, + }; + } + public async fetchClusterInfo(): Promise { if (this.esClient === undefined || this.esClient === null) { throw Error('elasticsearch client is unavailable: cannot retrieve cluster infomation'); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts index 0037aaa28fee3..b0792ed7b4610 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.ts @@ -18,7 +18,7 @@ import { } from '../../../../task_manager/server'; import { TelemetryReceiver } from './receiver'; import { allowlistEventFields, copyAllowlistedFields } from './filters'; -import { DiagnosticTask, EndpointTask, ExceptionListsTask } from './tasks'; +import { DiagnosticTask, EndpointTask, ExceptionListsTask, DetectionRulesTask } from './tasks'; import { createUsageCounterLabel } from './helpers'; import { TelemetryEvent } from './types'; import { TELEMETRY_MAX_BUFFER_SIZE } from './constants'; @@ -42,6 +42,7 @@ export class TelemetryEventsSender { private diagnosticTask?: DiagnosticTask; private endpointTask?: EndpointTask; private exceptionListsTask?: ExceptionListsTask; + private detectionRulesTask?: DetectionRulesTask; constructor(logger: Logger) { this.logger = logger.get('telemetry_events'); @@ -59,6 +60,12 @@ export class TelemetryEventsSender { if (taskManager) { this.diagnosticTask = new DiagnosticTask(this.logger, taskManager, this, telemetryReceiver); this.endpointTask = new EndpointTask(this.logger, taskManager, this, telemetryReceiver); + this.detectionRulesTask = new DetectionRulesTask( + this.logger, + taskManager, + this, + telemetryReceiver + ); this.exceptionListsTask = new ExceptionListsTask( this.logger, taskManager, @@ -80,6 +87,7 @@ export class TelemetryEventsSender { this.logger.debug(`starting security telemetry tasks`); this.diagnosticTask.start(taskManager); this.endpointTask.start(taskManager); + this.detectionRulesTask?.start(taskManager); this.exceptionListsTask?.start(taskManager); } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.test.ts new file mode 100644 index 0000000000000..0a05afb8a6535 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.test.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggingSystemMock } from 'src/core/server/mocks'; +import { taskManagerMock } from '../../../../../task_manager/server/mocks'; +import { TaskStatus } from '../../../../../task_manager/server'; +import { + TelemetryDetectionRulesTask, + TelemetryDetectionRuleListsTaskConstants, +} from './detection_rule'; +import { + createMockTelemetryEventsSender, + MockDetectionRuleListsTask, + createMockTelemetryReceiver, +} from '../mocks'; + +describe('test detection rule exception lists telemetry', () => { + let logger: ReturnType; + + beforeEach(() => { + logger = loggingSystemMock.createLogger(); + }); + + describe('basic telemetry sanity checks', () => { + test('detection rule lists task can register', () => { + const telemetryDiagTask = new TelemetryDetectionRulesTask( + logger, + taskManagerMock.createSetup(), + createMockTelemetryEventsSender(true), + createMockTelemetryReceiver() + ); + + expect(telemetryDiagTask).toBeInstanceOf(TelemetryDetectionRulesTask); + }); + }); + + test('detection rule task should be registered', () => { + const mockTaskManager = taskManagerMock.createSetup(); + new TelemetryDetectionRulesTask( + logger, + mockTaskManager, + createMockTelemetryEventsSender(true), + createMockTelemetryReceiver() + ); + + expect(mockTaskManager.registerTaskDefinitions).toHaveBeenCalled(); + }); + + test('detection rule task should be scheduled', async () => { + const mockTaskManagerSetup = taskManagerMock.createSetup(); + const telemetryDiagTask = new TelemetryDetectionRulesTask( + logger, + mockTaskManagerSetup, + createMockTelemetryEventsSender(true), + createMockTelemetryReceiver() + ); + + const mockTaskManagerStart = taskManagerMock.createStart(); + await telemetryDiagTask.start(mockTaskManagerStart); + expect(mockTaskManagerStart.ensureScheduled).toHaveBeenCalled(); + }); + + test('detection rule task should run', async () => { + const mockContext = createMockTelemetryEventsSender(true); + const mockTaskManager = taskManagerMock.createSetup(); + const mockReceiver = createMockTelemetryReceiver(); + const telemetryDiagTask = new MockDetectionRuleListsTask( + logger, + mockTaskManager, + mockContext, + mockReceiver + ); + + const mockTaskInstance = { + id: TelemetryDetectionRuleListsTaskConstants.TYPE, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TelemetryDetectionRuleListsTaskConstants.TYPE, + }; + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ + TelemetryDetectionRuleListsTaskConstants.TYPE + ].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + await taskRunner.run(); + expect(telemetryDiagTask.runTask).toHaveBeenCalled(); + }); + + test('detection rule task should not query elastic if telemetry is not opted in', async () => { + const mockSender = createMockTelemetryEventsSender(false); + const mockTaskManager = taskManagerMock.createSetup(); + const mockReceiver = createMockTelemetryReceiver(); + new MockDetectionRuleListsTask(logger, mockTaskManager, mockSender, mockReceiver); + + const mockTaskInstance = { + id: TelemetryDetectionRuleListsTaskConstants.TYPE, + runAt: new Date(), + attempts: 0, + ownerId: '', + status: TaskStatus.Running, + startedAt: new Date(), + scheduledAt: new Date(), + retryAt: new Date(), + params: {}, + state: {}, + taskType: TelemetryDetectionRuleListsTaskConstants.TYPE, + }; + const createTaskRunner = + mockTaskManager.registerTaskDefinitions.mock.calls[0][0][ + TelemetryDetectionRuleListsTaskConstants.TYPE + ].createTaskRunner; + const taskRunner = createTaskRunner({ taskInstance: mockTaskInstance }); + await taskRunner.run(); + expect(mockReceiver.fetchDiagnosticAlerts).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts new file mode 100644 index 0000000000000..a362be187921d --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/detection_rule.ts @@ -0,0 +1,149 @@ +/* + * 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 moment from 'moment'; +import { Logger } from 'src/core/server'; +import { + ConcreteTaskInstance, + TaskManagerSetupContract, + TaskManagerStartContract, +} from '../../../../../task_manager/server'; +import { LIST_DETECTION_RULE_EXCEPTION, TELEMETRY_CHANNEL_LISTS } from '../constants'; +import { batchTelemetryRecords, templateExceptionList } from '../helpers'; +import { TelemetryEventsSender } from '../sender'; +import { TelemetryReceiver } from '../receiver'; +import { ExceptionListItem, RuleSearchResult } from '../types'; + +export const TelemetryDetectionRuleListsTaskConstants = { + TIMEOUT: '10m', + TYPE: 'security:telemetry-detection-rules', + INTERVAL: '24h', + VERSION: '1.0.0', +}; + +const MAX_TELEMETRY_BATCH = 1_000; + +export class TelemetryDetectionRulesTask { + private readonly logger: Logger; + private readonly sender: TelemetryEventsSender; + private readonly receiver: TelemetryReceiver; + + constructor( + logger: Logger, + taskManager: TaskManagerSetupContract, + sender: TelemetryEventsSender, + receiver: TelemetryReceiver + ) { + this.logger = logger; + this.sender = sender; + this.receiver = receiver; + + taskManager.registerTaskDefinitions({ + [TelemetryDetectionRuleListsTaskConstants.TYPE]: { + title: 'Security Solution Detection Rule Lists Telemetry', + timeout: TelemetryDetectionRuleListsTaskConstants.TIMEOUT, + createTaskRunner: ({ taskInstance }: { taskInstance: ConcreteTaskInstance }) => { + const { state } = taskInstance; + + return { + run: async () => { + const taskExecutionTime = moment().utc().toISOString(); + const hits = await this.runTask(taskInstance.id); + + return { + state: { + lastExecutionTimestamp: taskExecutionTime, + runs: (state.runs || 0) + 1, + hits, + }, + }; + }, + cancel: async () => {}, + }; + }, + }, + }); + } + + public start = async (taskManager: TaskManagerStartContract) => { + try { + await taskManager.ensureScheduled({ + id: this.getTaskId(), + taskType: TelemetryDetectionRuleListsTaskConstants.TYPE, + scope: ['securitySolution'], + schedule: { + interval: TelemetryDetectionRuleListsTaskConstants.INTERVAL, + }, + state: { runs: 0 }, + params: { version: TelemetryDetectionRuleListsTaskConstants.VERSION }, + }); + } catch (e) { + this.logger.error(`Error scheduling task, received ${e.message}`); + } + }; + + private getTaskId = (): string => { + return `${TelemetryDetectionRuleListsTaskConstants.TYPE}:${TelemetryDetectionRuleListsTaskConstants.VERSION}`; + }; + + public runTask = async (taskId: string) => { + if (taskId !== this.getTaskId()) { + return 0; + } + + const isOptedIn = await this.sender.isTelemetryOptedIn(); + if (!isOptedIn) { + return 0; + } + + // Lists Telemetry: Detection Rules + + const { body: prebuiltRules } = await this.receiver.fetchDetectionRules(); + + const cacheArray = prebuiltRules.hits.hits.reduce((cache, searchHit) => { + const rule = searchHit._source as RuleSearchResult; + const ruleId = rule.alert.params.ruleId; + + const shouldNotProcess = + rule === null || + rule === undefined || + ruleId === null || + ruleId === undefined || + searchHit._source?.alert.params.exceptionsList.length === 0; + + if (shouldNotProcess) { + return cache; + } + + cache.push(rule); + return cache; + }, [] as RuleSearchResult[]); + + const detectionRuleExceptions = [] as ExceptionListItem[]; + for (const item of cacheArray) { + const ruleVersion = item.alert.params.version; + + for (const ex of item.alert.params.exceptionsList) { + const listItem = await this.receiver.fetchDetectionExceptionList(ex.list_id, ruleVersion); + for (const exceptionItem of listItem.data) { + detectionRuleExceptions.push(exceptionItem); + } + } + } + + const detectionRuleExceptionsJson = templateExceptionList( + detectionRuleExceptions, + LIST_DETECTION_RULE_EXCEPTION + ); + + batchTelemetryRecords(detectionRuleExceptionsJson, MAX_TELEMETRY_BATCH).forEach((batch) => { + this.sender.sendOnDemand(TELEMETRY_CHANNEL_LISTS, batch); + }); + + return detectionRuleExceptions.length; + }; +} diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts index 0c066deea17d9..c6bf4b06e70f0 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/endpoint.ts @@ -14,7 +14,7 @@ import { } from '../../../../../task_manager/server'; import { batchTelemetryRecords, - getPreviousEpMetaTaskTimestamp, + getPreviousDailyTaskTimestamp, isPackagePolicyList, } from '../helpers'; import { TelemetryEventsSender } from '../sender'; @@ -76,7 +76,7 @@ export class TelemetryEndpointTask { return { run: async () => { const taskExecutionTime = moment().utc().toISOString(); - const lastExecutionTimestamp = getPreviousEpMetaTaskTimestamp( + const lastExecutionTimestamp = getPreviousDailyTaskTimestamp( taskExecutionTime, taskInstance.state?.lastExecutionTimestamp ); diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts index e090252b88d8f..a850f848567cb 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/tasks/index.ts @@ -8,3 +8,4 @@ export { TelemetryDiagTask as DiagnosticTask } from './diagnostic'; export { TelemetryEndpointTask as EndpointTask } from './endpoint'; export { TelemetryExceptionListsTask as ExceptionListsTask } from './security_lists'; +export { TelemetryDetectionRulesTask as DetectionRulesTask } from './detection_rule'; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts index abcad26ed000c..6aaf6f4371475 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/types.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/types.ts @@ -217,18 +217,45 @@ export interface GetEndpointListResponse { export interface ExceptionListItem { id: string; - version: string; + rule_version?: number; name: string; - description: string; created_at: string; updated_at: string; entries: object; - os: string; os_types: object; } export interface ListTemplate { - trusted_application: TelemetryEvent[]; - endpoint_exception: TelemetryEvent[]; - endpoint_event_filter: TelemetryEvent[]; + '@timestamp': number; + detection_rule?: TelemetryEvent; + endpoint_exception?: TelemetryEvent; + endpoint_event_filter?: TelemetryEvent; + trusted_application?: TelemetryEvent; +} + +// Detection Rule types + +interface ExceptionListEntry { + id: string; + list_id: string; + type: string; + namespace_type: string; +} + +interface DetectionRuleParms { + ruleId: string; + version: number; + type: string; + exceptionsList: ExceptionListEntry[]; +} + +export interface RuleSearchResult { + alert: { + name: string; + enabled: boolean; + tags: string[]; + createdAt: string; + updatedAt: string; + params: DetectionRuleParms; + }; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index bffcc823d047e..f0a91f8b06c00 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -459,7 +459,13 @@ export class Plugin implements IPlugin Date: Mon, 11 Oct 2021 17:30:18 +0200 Subject: [PATCH 019/287] [bfetch] Pass `compress` flag in query instead of headers (#113929) --- src/plugins/bfetch/common/util/index.ts | 1 + .../bfetch/common/util/query_params.ts | 12 +++ .../public/streaming/fetch_streaming.ts | 12 +-- src/plugins/bfetch/server/index.ts | 1 - src/plugins/bfetch/server/mocks.ts | 1 - src/plugins/bfetch/server/plugin.ts | 78 +------------------ .../bfetch/server/streaming/create_stream.ts | 8 +- src/plugins/bfetch/server/types.ts | 27 ------- test/api_integration/apis/search/bsearch.ts | 66 +++++++--------- 9 files changed, 56 insertions(+), 150 deletions(-) create mode 100644 src/plugins/bfetch/common/util/query_params.ts delete mode 100644 src/plugins/bfetch/server/types.ts diff --git a/src/plugins/bfetch/common/util/index.ts b/src/plugins/bfetch/common/util/index.ts index 1651d24d96b14..f20d30eb3cdf0 100644 --- a/src/plugins/bfetch/common/util/index.ts +++ b/src/plugins/bfetch/common/util/index.ts @@ -8,3 +8,4 @@ export * from './normalize_error'; export * from './remove_leading_slash'; +export * from './query_params'; diff --git a/src/plugins/bfetch/common/util/query_params.ts b/src/plugins/bfetch/common/util/query_params.ts new file mode 100644 index 0000000000000..ed65699fbcafc --- /dev/null +++ b/src/plugins/bfetch/common/util/query_params.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export const appendQueryParam = (url: string, key: string, value: string): string => { + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}${key}=${value}`; +}; diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.ts index a94c8d3980cba..77e5acffc1af3 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.ts @@ -10,6 +10,7 @@ import { map, share } from 'rxjs/operators'; import { inflateResponse } from '.'; import { fromStreamingXhr } from './from_streaming_xhr'; import { split } from './split'; +import { appendQueryParam } from '../../common'; export interface FetchStreamingParams { url: string; @@ -34,16 +35,15 @@ export function fetchStreaming({ }: FetchStreamingParams) { const xhr = new window.XMLHttpRequest(); - // Begin the request - xhr.open(method, url); - xhr.withCredentials = true; - const isCompressionDisabled = getIsCompressionDisabled(); - if (!isCompressionDisabled) { - headers['X-Chunk-Encoding'] = 'deflate'; + url = appendQueryParam(url, 'compress', 'true'); } + // Begin the request + xhr.open(method, url); + xhr.withCredentials = true; + // Set the HTTP headers Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); diff --git a/src/plugins/bfetch/server/index.ts b/src/plugins/bfetch/server/index.ts index c533b2ad7a3df..f4c41d10e42cb 100644 --- a/src/plugins/bfetch/server/index.ts +++ b/src/plugins/bfetch/server/index.ts @@ -10,7 +10,6 @@ import { PluginInitializerContext } from '../../../core/server'; import { BfetchServerPlugin } from './plugin'; export { BfetchServerSetup, BfetchServerStart, BatchProcessingRouteParams } from './plugin'; -export { StreamingRequestHandler } from './types'; export function plugin(initializerContext: PluginInitializerContext) { return new BfetchServerPlugin(initializerContext); diff --git a/src/plugins/bfetch/server/mocks.ts b/src/plugins/bfetch/server/mocks.ts index bbb596bf8d5ff..dfa365d9e70b2 100644 --- a/src/plugins/bfetch/server/mocks.ts +++ b/src/plugins/bfetch/server/mocks.ts @@ -17,7 +17,6 @@ const createSetupContract = (): Setup => { const setupContract: Setup = { addBatchProcessingRoute: jest.fn(), addStreamingResponseRoute: jest.fn(), - createStreamingRequestHandler: jest.fn(), }; return setupContract; }; diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 7b60be9a8fc75..f7127445f96c5 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -13,9 +13,6 @@ import type { Plugin, Logger, KibanaRequest, - RouteMethod, - RequestHandler, - RequestHandlerContext, StartServicesAccessor, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; @@ -28,7 +25,6 @@ import { removeLeadingSlash, normalizeError, } from '../common'; -import { StreamingRequestHandler } from './types'; import { createStream } from './streaming'; import { getUiSettings } from './ui_settings'; @@ -52,44 +48,6 @@ export interface BfetchServerSetup { path: string, params: (request: KibanaRequest) => StreamingResponseHandler ) => void; - /** - * Create a streaming request handler to be able to use an Observable to return chunked content to the client. - * This is meant to be used with the `fetchStreaming` API of the `bfetch` client-side plugin. - * - * @example - * ```ts - * setup({ http }: CoreStart, { bfetch }: SetupDeps) { - * const router = http.createRouter(); - * router.post( - * { - * path: '/api/my-plugin/stream-endpoint, - * validate: { - * body: schema.object({ - * term: schema.string(), - * }), - * } - * }, - * bfetch.createStreamingResponseHandler(async (ctx, req) => { - * const { term } = req.body; - * const results$ = await myApi.getResults$(term); - * return results$; - * }) - * )} - * - * ``` - * - * @param streamHandler - */ - createStreamingRequestHandler: < - Response, - P, - Q, - B, - Context extends RequestHandlerContext = RequestHandlerContext, - Method extends RouteMethod = any - >( - streamHandler: StreamingRequestHandler - ) => RequestHandler; } // eslint-disable-next-line @@ -124,15 +82,10 @@ export class BfetchServerPlugin logger, }); const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); - const createStreamingRequestHandler = this.createStreamingRequestHandler({ - getStartServices: core.getStartServices, - logger, - }); return { addBatchProcessingRoute, addStreamingResponseRoute, - createStreamingRequestHandler, }; } @@ -142,10 +95,6 @@ export class BfetchServerPlugin public stop() {} - private getCompressionDisabled(request: KibanaRequest) { - return request.headers['x-chunk-encoding'] !== 'deflate'; - } - private addStreamingResponseRoute = ({ getStartServices, @@ -162,42 +111,21 @@ export class BfetchServerPlugin path: `/${removeLeadingSlash(path)}`, validate: { body: schema.any(), + query: schema.object({ compress: schema.boolean({ defaultValue: false }) }), }, }, async (context, request, response) => { const handlerInstance = handler(request); const data = request.body; - const compressionDisabled = this.getCompressionDisabled(request); + const compress = request.query.compress; return response.ok({ headers: streamingHeaders, - body: createStream( - handlerInstance.getResponseStream(data), - logger, - compressionDisabled - ), + body: createStream(handlerInstance.getResponseStream(data), logger, compress), }); } ); }; - private createStreamingRequestHandler = - ({ - logger, - getStartServices, - }: { - logger: Logger; - getStartServices: StartServicesAccessor; - }): BfetchServerSetup['createStreamingRequestHandler'] => - (streamHandler) => - async (context, request, response) => { - const response$ = await streamHandler(context, request); - const compressionDisabled = this.getCompressionDisabled(request); - return response.ok({ - headers: streamingHeaders, - body: createStream(response$, logger, compressionDisabled), - }); - }; - private addBatchProcessingRoute = ( addStreamingResponseRoute: BfetchServerSetup['addStreamingResponseRoute'] diff --git a/src/plugins/bfetch/server/streaming/create_stream.ts b/src/plugins/bfetch/server/streaming/create_stream.ts index 7d6981294341b..756a806a60229 100644 --- a/src/plugins/bfetch/server/streaming/create_stream.ts +++ b/src/plugins/bfetch/server/streaming/create_stream.ts @@ -15,9 +15,9 @@ import { createNDJSONStream } from './create_ndjson_stream'; export function createStream( response$: Observable, logger: Logger, - compressionDisabled: boolean + compress: boolean ): Stream { - return compressionDisabled - ? createNDJSONStream(response$, logger) - : createCompressedStream(response$, logger); + return compress + ? createCompressedStream(response$, logger) + : createNDJSONStream(response$, logger); } diff --git a/src/plugins/bfetch/server/types.ts b/src/plugins/bfetch/server/types.ts deleted file mode 100644 index 4e54744f4c374..0000000000000 --- a/src/plugins/bfetch/server/types.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { Observable } from 'rxjs'; -import { KibanaRequest, RequestHandlerContext, RouteMethod } from 'kibana/server'; - -/** - * Request handler modified to allow to return an observable. - * - * See {@link BfetchServerSetup.createStreamingRequestHandler} for usage example. - * @public - */ -export type StreamingRequestHandler< - Response = unknown, - P = unknown, - Q = unknown, - B = unknown, - Method extends RouteMethod = any -> = ( - context: RequestHandlerContext, - request: KibanaRequest -) => Observable | Promise>; diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts index f80bc1d0d9dfa..6aee2b542da0f 100644 --- a/test/api_integration/apis/search/bsearch.ts +++ b/test/api_integration/apis/search/bsearch.ts @@ -29,28 +29,25 @@ export default function ({ getService }: FtrProviderContext) { describe('bsearch', () => { describe('post', () => { it('should return 200 a single response', async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set({ 'X-Chunk-Encoding': '' }) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - query: { - match_all: {}, - }, + const resp = await supertest.post(`/internal/bsearch`).send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, }, }, }, - options: { - strategy: 'es', - }, }, - ], - }); + options: { + strategy: 'es', + }, + }, + ], + }); const jsonBody = parseBfetchResponse(resp); @@ -62,28 +59,25 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return 200 a single response from compressed', async () => { - const resp = await supertest - .post(`/internal/bsearch`) - .set({ 'X-Chunk-Encoding': 'deflate' }) - .send({ - batch: [ - { - request: { - params: { - index: '.kibana', - body: { - query: { - match_all: {}, - }, + const resp = await supertest.post(`/internal/bsearch?compress=true`).send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, }, }, }, - options: { - strategy: 'es', - }, }, - ], - }); + options: { + strategy: 'es', + }, + }, + ], + }); const jsonBody = parseBfetchResponse(resp, true); From 32e00f1b0cd9b87a492b2bafa3f8fa2ed46731db Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Mon, 11 Oct 2021 11:41:01 -0400 Subject: [PATCH 020/287] [Fleet] Fix previous configuration modal title (#114475) * Fix previous configuration modal title * Revert translation --- .../sections/agent_policy/edit_package_policy_page/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 7a2f46247d14a..4d940534c4a7b 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -687,7 +687,7 @@ const UpgradeStatusCallout: React.FunctionComponent<{

From b03237a72d6675bd6deb976a73c2dddcdcb6b445 Mon Sep 17 00:00:00 2001 From: Josh Dover <1813008+joshdover@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:16:26 +0200 Subject: [PATCH 021/287] Enable `bearer` scheme by default to support service token authorization (#112654) Co-authored-by: Aleh Zasypkin --- docs/settings/security-settings.asciidoc | 2 +- .../security/authentication/index.asciidoc | 6 +- .../authentication/authenticator.test.ts | 6 +- x-pack/plugins/security/server/config.test.ts | 9 ++ x-pack/plugins/security/server/config.ts | 2 +- .../security_usage_collector.test.ts | 2 +- x-pack/scripts/functional_tests.js | 1 + .../http_bearer.config.ts | 36 ++++++ .../tests/http_bearer/header.ts | 103 ++++++++++++++++++ .../tests/http_bearer/index.ts | 15 +++ 10 files changed, 174 insertions(+), 8 deletions(-) create mode 100644 x-pack/test/security_api_integration/http_bearer.config.ts create mode 100644 x-pack/test/security_api_integration/tests/http_bearer/header.ts create mode 100644 x-pack/test/security_api_integration/tests/http_bearer/index.ts diff --git a/docs/settings/security-settings.asciidoc b/docs/settings/security-settings.asciidoc index 906af1dfbb28e..11072509da1fc 100644 --- a/docs/settings/security-settings.asciidoc +++ b/docs/settings/security-settings.asciidoc @@ -218,7 +218,7 @@ There is a very limited set of cases when you'd want to change these settings. F | Determines if HTTP authentication schemes used by the enabled authentication providers should be automatically supported during HTTP authentication. By default, this setting is set to `true`. | `xpack.security.authc.http.schemes[]` -| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey']` to support HTTP authentication with <> scheme. +| List of HTTP authentication schemes that {kib} HTTP authentication should support. By default, this setting is set to `['apikey', 'bearer']` to support HTTP authentication with the <> and <> schemes. |=== diff --git a/docs/user/security/authentication/index.asciidoc b/docs/user/security/authentication/index.asciidoc index bc564308c057e..2f2b279389799 100644 --- a/docs/user/security/authentication/index.asciidoc +++ b/docs/user/security/authentication/index.asciidoc @@ -437,14 +437,14 @@ This type of authentication is usually useful for machine-to-machine interaction By default {kib} supports <> authentication scheme _and_ any scheme supported by the currently enabled authentication provider. For example, `Basic` authentication scheme is automatically supported when basic authentication provider is enabled, or `Bearer` scheme when any of the token based authentication providers is enabled (Token, SAML, OpenID Connect, PKI or Kerberos). But it's also possible to add support for any other authentication scheme in the `kibana.yml` configuration file, as follows: -NOTE: Don't forget to explicitly specify default `apikey` scheme when you just want to add a new one to the list. +NOTE: Don't forget to explicitly specify the default `apikey` and `bearer` schemes when you just want to add a new one to the list. [source,yaml] -------------------------------------------------------------------------------- -xpack.security.authc.http.schemes: [apikey, basic, something-custom] +xpack.security.authc.http.schemes: [apikey, bearer, basic, something-custom] -------------------------------------------------------------------------------- -With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. +With this configuration, you can send requests to {kib} with the `Authorization` header using `ApiKey`, `Bearer`, `Basic` or `Something-Custom` HTTP schemes (case insensitive). Under the hood, {kib} relays this header to {es}, then {es} authenticates the request using the credentials in the header. [float] [[embedded-content-authentication]] diff --git a/x-pack/plugins/security/server/authentication/authenticator.test.ts b/x-pack/plugins/security/server/authentication/authenticator.test.ts index ce97c142f5584..4e35b84a93119 100644 --- a/x-pack/plugins/security/server/authentication/authenticator.test.ts +++ b/x-pack/plugins/security/server/authentication/authenticator.test.ts @@ -210,7 +210,7 @@ describe('Authenticator', () => { expect( jest.requireMock('./providers/http').HTTPAuthenticationProvider ).toHaveBeenCalledWith(expect.anything(), { - supportedSchemes: new Set(['apikey', 'basic']), + supportedSchemes: new Set(['apikey', 'bearer', 'basic']), }); }); @@ -238,7 +238,9 @@ describe('Authenticator', () => { expect( jest.requireMock('./providers/http').HTTPAuthenticationProvider - ).toHaveBeenCalledWith(expect.anything(), { supportedSchemes: new Set(['apikey']) }); + ).toHaveBeenCalledWith(expect.anything(), { + supportedSchemes: new Set(['apikey', 'bearer']), + }); }); it('disabled if explicitly disabled', () => { diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 4a7d8c7961cf5..1baf3fd4aac50 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -27,6 +27,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -80,6 +81,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -133,6 +135,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { @@ -311,6 +314,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "oidc": Object { @@ -342,6 +346,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "oidc": Object { @@ -373,6 +378,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -391,6 +397,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -412,6 +419,7 @@ describe('config schema', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Array [ @@ -1485,6 +1493,7 @@ describe('createConfig()', () => { "enabled": true, "schemes": Array [ "apikey", + "bearer", ], }, "providers": Object { diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index 07ff81e092f5f..89918e73369d3 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -269,7 +269,7 @@ export const ConfigSchema = schema.object({ http: schema.object({ enabled: schema.boolean({ defaultValue: true }), autoSchemesEnabled: schema.boolean({ defaultValue: true }), - schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey'] }), + schemes: schema.arrayOf(schema.string(), { defaultValue: ['apikey', 'bearer'] }), }), }), audit: schema.object( diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts index 0515a1e1969bf..83f09ef017b01 100644 --- a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -46,7 +46,7 @@ describe('Security UsageCollector', () => { authProviderCount: 1, enabledAuthProviders: ['basic'], loginSelectorEnabled: false, - httpAuthSchemes: ['apikey'], + httpAuthSchemes: ['apikey', 'bearer'], sessionIdleTimeoutInMinutes: 60, sessionLifespanInMinutes: 43200, sessionCleanupInMinutes: 60, diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index 3c1cdd5790f3c..f7b978c2b58bd 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -54,6 +54,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/security_api_integration/session_lifespan.config.ts'), require.resolve('../test/security_api_integration/login_selector.config.ts'), require.resolve('../test/security_api_integration/audit.config.ts'), + require.resolve('../test/security_api_integration/http_bearer.config.ts'), require.resolve('../test/security_api_integration/kerberos.config.ts'), require.resolve('../test/security_api_integration/kerberos_anonymous_access.config.ts'), require.resolve('../test/security_api_integration/pki.config.ts'), diff --git a/x-pack/test/security_api_integration/http_bearer.config.ts b/x-pack/test/security_api_integration/http_bearer.config.ts new file mode 100644 index 0000000000000..b0a9f4a920347 --- /dev/null +++ b/x-pack/test/security_api_integration/http_bearer.config.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import { services } from './services'; + +export default async function ({ readConfigFile }: FtrConfigProviderContext) { + const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); + + return { + testFiles: [require.resolve('./tests/http_bearer')], + servers: xPackAPITestsConfig.get('servers'), + security: { disableTestUser: true }, + services, + junit: { + reportName: 'X-Pack Security API Integration Tests (HTTP Bearer)', + }, + + esTestCluster: { + ...xPackAPITestsConfig.get('esTestCluster'), + serverArgs: [ + ...xPackAPITestsConfig.get('esTestCluster.serverArgs'), + 'xpack.security.authc.token.enabled=true', + 'xpack.security.authc.token.timeout=15s', + ], + }, + + kbnTestServer: { + ...xPackAPITestsConfig.get('kbnTestServer'), + }, + }; +} diff --git a/x-pack/test/security_api_integration/tests/http_bearer/header.ts b/x-pack/test/security_api_integration/tests/http_bearer/header.ts new file mode 100644 index 0000000000000..f7ebef4f16d09 --- /dev/null +++ b/x-pack/test/security_api_integration/tests/http_bearer/header.ts @@ -0,0 +1,103 @@ +/* + * 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 { adminTestUser } from '@kbn/test'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService }: FtrProviderContext) { + const supertest = getService('supertestWithoutAuth'); + const es = getService('es'); + + async function createToken() { + const { + body: { access_token: accessToken, authentication }, + } = await es.security.getToken({ + body: { + grant_type: 'password', + ...adminTestUser, + }, + }); + + return { + accessToken, + expectedUser: { + ...authentication, + authentication_provider: { name: '__http__', type: 'http' }, + authentication_type: 'token', + }, + }; + } + + describe('header', () => { + it('accepts valid access token via authorization Bearer header', async () => { + const { accessToken, expectedUser } = await createToken(); + + const response = await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + + // Make sure we don't automatically create a session + expect(response.headers['set-cookie']).to.be(undefined); + }); + + it('accepts multiple requests for a single valid access token', async () => { + const { accessToken, expectedUser } = await createToken(); + + // try it once + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + + // try it again to verity it isn't invalidated after a single request + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(200, expectedUser); + }); + + it('rejects invalid access token via authorization Bearer header', async () => { + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', 'Bearer notreal') + .expect(401); + }); + + it('rejects invalidated access token via authorization Bearer header', async () => { + const { accessToken } = await createToken(); + await es.security.invalidateToken({ body: { token: accessToken } }); + + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(401); + }); + + it('rejects expired access token via authorization Bearer header', async function () { + this.timeout(40000); + + const { accessToken } = await createToken(); + + // Access token expiration is set to 15s for API integration tests. + // Let's wait for 20s to make sure token expires. + await new Promise((resolve) => setTimeout(resolve, 20000)); + + await supertest + .get('/internal/security/me') + .set('kbn-xsrf', 'true') + .set('authorization', `Bearer ${accessToken}`) + .expect(401); + }); + }); +} diff --git a/x-pack/test/security_api_integration/tests/http_bearer/index.ts b/x-pack/test/security_api_integration/tests/http_bearer/index.ts new file mode 100644 index 0000000000000..4dbad2660ebaa --- /dev/null +++ b/x-pack/test/security_api_integration/tests/http_bearer/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 { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('security APIs - HTTP Bearer', function () { + this.tags('ciGroup6'); + loadTestFile(require.resolve('./header')); + }); +} From c8a01082696a94453060ded28ee67a32a2487e0d Mon Sep 17 00:00:00 2001 From: Chris Cowan Date: Mon, 11 Oct 2021 10:53:51 -0600 Subject: [PATCH 022/287] [Stack Monitoring] Adding alerts to react app (#114029) * [Stack Monitoring] Adding alerts to react app * Fixing global state context path * adding alerts to pages; adding alerts model to cluster_overview; removing loadAlerts from page template * Fixing request for enable alerts * remove loadAlerts from page template * Adding request error handlers * removing redundent error handling * Changing useRequestErrorHandler function to be async due to error.response.json call * removing old comment * Fixing contexts paths * Converting ajaxRequestErrorHandler to useRequestErrorHandler * Refactoring error handler for page template and setup mode * Removing unnecessary async/await * Removing unnecessary async/await in useClusters * adding alertTypeIds to each page * fixing instance count * Adding alertTypeIds to index page * Adding alert filters for specific pages * Adding alerts to Logstash nodes Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/monitoring/common/types/alerts.ts | 1 + .../public/alerts/alerts_dropdown.tsx | 5 +- .../external_config_context.tsx | 0 .../{ => contexts}/global_state_context.tsx | 8 +- .../contexts/header_action_menu_context.tsx | 15 + .../application/hooks/use_alerts_modal.ts | 10 +- .../public/application/hooks/use_clusters.ts | 6 +- .../application/hooks/use_monitoring_time.ts | 2 +- .../hooks/use_request_error_handler.tsx | 79 +++ .../monitoring/public/application/index.tsx | 467 +++++++++--------- .../public/application/pages/apm/instance.tsx | 2 +- .../application/pages/apm/instances.tsx | 2 +- .../public/application/pages/apm/overview.tsx | 2 +- .../application/pages/beats/instance.tsx | 2 +- .../application/pages/beats/instances.tsx | 2 +- .../application/pages/beats/overview.tsx | 2 +- .../pages/cluster/overview_page.tsx | 49 +- .../pages/elasticsearch/ccr_page.tsx | 41 +- .../pages/elasticsearch/ccr_shard_page.tsx | 47 +- .../elasticsearch/index_advanced_page.tsx | 46 +- .../pages/elasticsearch/index_page.tsx | 58 ++- .../pages/elasticsearch/indices_page.tsx | 51 +- .../pages/elasticsearch/ml_jobs_page.tsx | 2 +- .../elasticsearch/node_advanced_page.tsx | 63 ++- .../pages/elasticsearch/node_page.tsx | 70 ++- .../pages/elasticsearch/nodes_page.tsx | 66 ++- .../pages/elasticsearch/overview.tsx | 2 +- .../pages/home/cluster_listing.tsx | 32 +- .../application/pages/kibana/instance.tsx | 41 +- .../application/pages/kibana/instances.tsx | 43 +- .../application/pages/kibana/overview.tsx | 2 +- .../public/application/pages/license_page.tsx | 2 +- .../application/pages/logstash/advanced.tsx | 41 +- .../application/pages/logstash/node.tsx | 41 +- .../pages/logstash/node_pipelines.tsx | 2 +- .../application/pages/logstash/nodes.tsx | 40 +- .../application/pages/logstash/overview.tsx | 2 +- .../application/pages/logstash/pipeline.tsx | 4 +- .../application/pages/logstash/pipelines.tsx | 2 +- .../pages/no_data/no_data_page.tsx | 6 +- .../application/pages/page_template.tsx | 44 +- .../public/application/route_init.tsx | 2 +- .../application/setup_mode/setup_mode.tsx | 15 +- .../setup_mode/setup_mode_renderer.js | 16 +- .../public/components/action_menu/index.tsx | 34 ++ .../public/components/shared/toolbar.tsx | 2 +- .../monitoring/public/lib/fetch_alerts.ts | 36 ++ 47 files changed, 990 insertions(+), 517 deletions(-) rename x-pack/plugins/monitoring/public/application/{ => contexts}/external_config_context.tsx (100%) rename x-pack/plugins/monitoring/public/application/{ => contexts}/global_state_context.tsx (90%) create mode 100644 x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx create mode 100644 x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx create mode 100644 x-pack/plugins/monitoring/public/components/action_menu/index.tsx create mode 100644 x-pack/plugins/monitoring/public/lib/fetch_alerts.ts diff --git a/x-pack/plugins/monitoring/common/types/alerts.ts b/x-pack/plugins/monitoring/common/types/alerts.ts index 1f68b0c55a046..bbd217169469d 100644 --- a/x-pack/plugins/monitoring/common/types/alerts.ts +++ b/x-pack/plugins/monitoring/common/types/alerts.ts @@ -32,6 +32,7 @@ export interface CommonAlertState { export interface CommonAlertFilter { nodeUuid?: string; shardId?: string; + shardIndex?: string; } export interface CommonAlertParamDetail { diff --git a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx index 261685a532882..976569f39de4c 100644 --- a/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx +++ b/x-pack/plugins/monitoring/public/alerts/alerts_dropdown.tsx @@ -14,13 +14,12 @@ import { import { i18n } from '@kbn/i18n'; import React, { useState } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { Legacy } from '../legacy_shims'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { MonitoringStartPluginDependencies } from '../types'; +import { useAlertsModal } from '../application/hooks/use_alerts_modal'; export const AlertsDropdown: React.FC<{}> = () => { - const $injector = Legacy.shims.getAngularInjector(); - const alertsEnableModalProvider: any = $injector.get('enableAlertsModal'); + const alertsEnableModalProvider = useAlertsModal(); const { navigateToApp } = useKibana().services.application; diff --git a/x-pack/plugins/monitoring/public/application/external_config_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx similarity index 100% rename from x-pack/plugins/monitoring/public/application/external_config_context.tsx rename to x-pack/plugins/monitoring/public/application/contexts/external_config_context.tsx diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx similarity index 90% rename from x-pack/plugins/monitoring/public/application/global_state_context.tsx rename to x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx index 6c952f80eff57..e6638b4c4fede 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/contexts/global_state_context.tsx @@ -5,10 +5,10 @@ * 2.0. */ import React, { createContext } from 'react'; -import { GlobalState } from '../url_state'; -import { MonitoringStartPluginDependencies } from '../types'; -import { TimeRange, RefreshInterval } from '../../../../../src/plugins/data/public'; -import { Legacy } from '../legacy_shims'; +import { GlobalState } from '../../url_state'; +import { MonitoringStartPluginDependencies } from '../../types'; +import { TimeRange, RefreshInterval } from '../../../../../../src/plugins/data/public'; +import { Legacy } from '../../legacy_shims'; interface GlobalStateProviderProps { query: MonitoringStartPluginDependencies['data']['query']; diff --git a/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx b/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx new file mode 100644 index 0000000000000..88862d9e6a807 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/contexts/header_action_menu_context.tsx @@ -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 React from 'react'; +import { AppMountParameters } from 'kibana/public'; + +interface ContextProps { + setHeaderActionMenu?: AppMountParameters['setHeaderActionMenu']; +} + +export const HeaderActionMenuContext = React.createContext({}); diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts b/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts index 9a2a2b80cc40f..123dd39f7b54d 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_alerts_modal.ts @@ -6,10 +6,11 @@ */ import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { showAlertsToast } from '../../alerts/lib/alerts_toast'; -import { ajaxErrorHandlersProvider } from '../../lib/ajax_error_handler'; +import { useRequestErrorHandler } from './use_request_error_handler'; export const useAlertsModal = () => { const { services } = useKibana(); + const handleRequestError = useRequestErrorHandler(); function shouldShowAlertsModal(alerts: {}) { const modalHasBeenShown = @@ -28,12 +29,11 @@ export const useAlertsModal = () => { async function enableAlerts() { try { - const { data } = await services.http?.post('../api/monitoring/v1/alerts/enable', {}); + const response = await services.http?.post('../api/monitoring/v1/alerts/enable', {}); window.localStorage.setItem('ALERTS_MODAL_DECISION_MADE', 'true'); - showAlertsToast(data); + showAlertsToast(response); } catch (err) { - const ajaxErrorHandlers = ajaxErrorHandlersProvider(); - return ajaxErrorHandlers(err); + await handleRequestError(err); } } diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts index b4b8c21ca4d40..1961bd53b909f 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -7,6 +7,7 @@ import { useState, useEffect } from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { fetchClusters } from '../../lib/fetch_clusters'; +import { useRequestErrorHandler } from './use_request_error_handler'; export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: string[]) { const { services } = useKibana<{ data: any }>(); @@ -17,6 +18,7 @@ export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: const [clusters, setClusters] = useState([] as any); const [loaded, setLoaded] = useState(false); + const handleRequestError = useRequestErrorHandler(); useEffect(() => { async function makeRequest() { @@ -34,13 +36,13 @@ export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: setClusters(response); } } catch (e) { - // TODO: Handle errors + handleRequestError(e); } finally { setLoaded(true); } } makeRequest(); - }, [clusterUuid, ccs, services.http, codePaths, min, max]); + }, [handleRequestError, clusterUuid, ccs, services.http, codePaths, min, max]); return { clusters, loaded }; } diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts index 3054714ec3aa6..e8973ce18232c 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_monitoring_time.ts @@ -8,7 +8,7 @@ import { useCallback, useState, useContext, useEffect } from 'react'; import createContainer from 'constate'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { Legacy } from '../../legacy_shims'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../contexts/global_state_context'; interface TimeOptions { from: string; diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx new file mode 100644 index 0000000000000..3a64531844451 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/hooks/use_request_error_handler.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useCallback } from 'react'; +import { includes } from 'lodash'; +import { IHttpFetchError } from 'kibana/public'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiButton, EuiSpacer, EuiText } from '@elastic/eui'; +import { formatMsg } from '../../../../../../src/plugins/kibana_legacy/public'; +import { toMountPoint, useKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { MonitoringStartPluginDependencies } from '../../types'; + +export function formatMonitoringError(err: IHttpFetchError) { + if (err.response?.status && err.response?.status !== -1) { + return ( + +

{err.body?.message}

+ + + +
+ ); + } + + return formatMsg(err); +} + +export const useRequestErrorHandler = () => { + const { services } = useKibana(); + return useCallback( + (err: IHttpFetchError) => { + if (err.response?.status === 403) { + // redirect to error message view + history.replaceState(null, '', '#/access-denied'); + } else if (err.response?.status === 404 && !includes(window.location.hash, 'no-data')) { + // pass through if this is a 404 and we're already on the no-data page + const formattedError = formatMonitoringError(err); + services.notifications?.toasts.addDanger({ + title: toMountPoint( + + ), + text: toMountPoint( +
+ {formattedError} + + window.location.reload()}> + + +
+ ), + }); + } else { + services.notifications?.toasts.addDanger({ + title: toMountPoint( + + ), + text: toMountPoint(formatMonitoringError(err)), + }); + } + }, + [services.notifications] + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index bc81dd826f849..7b4c73475338f 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CoreStart, AppMountParameters } from 'kibana/public'; +import { CoreStart, AppMountParameters, MountPoint } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; import { Route, Switch, Redirect, Router } from 'react-router-dom'; @@ -15,8 +15,8 @@ import { LicensePage } from './pages/license_page'; import { ClusterOverview } from './pages/cluster/overview_page'; import { ClusterListing } from './pages/home/cluster_listing'; import { MonitoringStartPluginDependencies } from '../types'; -import { GlobalStateProvider } from './global_state_context'; -import { ExternalConfigContext, ExternalConfig } from './external_config_context'; +import { GlobalStateProvider } from './contexts/global_state_context'; +import { ExternalConfigContext, ExternalConfig } from './contexts/external_config_context'; import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; import { NoDataPage } from './pages/no_data'; @@ -45,6 +45,7 @@ import { ElasticsearchCcrPage } from './pages/elasticsearch/ccr_page'; import { ElasticsearchCcrShardPage } from './pages/elasticsearch/ccr_shard_page'; import { MonitoringTimeContainer } from './hooks/use_monitoring_time'; import { BreadcrumbContainer } from './hooks/use_breadcrumbs'; +import { HeaderActionMenuContext } from './contexts/header_action_menu_context'; import { LogStashOverviewPage } from './pages/logstash/overview'; import { LogStashNodesPage } from './pages/logstash/nodes'; import { LogStashPipelinesPage } from './pages/logstash/pipelines'; @@ -58,11 +59,16 @@ import { LogStashNodePipelinesPage } from './pages/logstash/node_pipelines'; export const renderApp = ( core: CoreStart, plugins: MonitoringStartPluginDependencies, - { element }: AppMountParameters, + { element, setHeaderActionMenu }: AppMountParameters, externalConfig: ExternalConfig ) => { ReactDOM.render( - , + , element ); @@ -75,236 +81,239 @@ const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; externalConfig: ExternalConfig; -}> = ({ core, plugins, externalConfig }) => { + setHeaderActionMenu: (element: MountPoint | undefined) => void; +}> = ({ core, plugins, externalConfig, setHeaderActionMenu }) => { const history = createPreserveQueryHistory(); return ( - - - - - - - - - - - {/* ElasticSearch Views */} - - - - - - - - - - - - - - - - - - - - - {/* Kibana Views */} - - - - - - - {/* Beats Views */} - - - - - - - {/* Logstash Routes */} - - - - - - - - - - - - - - - {/* APM Views */} - - - - - - - - - - - + + + + + + + + + + + + {/* ElasticSearch Views */} + + + + + + + + + + + + + + + + + + + + + {/* Kibana Views */} + + + + + + + {/* Beats Views */} + + + + + + + {/* Logstash Routes */} + + + + + + + + + + + + + + + {/* APM Views */} + + + + + + + + + + + + diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx index dc55ecb22b61a..3fa7819c5e417 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instance.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx index bc60f26cdbfad..fedb07fa65a40 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { ApmTemplate } from './apm_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx index cca31c0a7e65d..516c293c53546 100644 --- a/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/apm/overview.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; import { ApmTemplate } from './apm_template'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useCharts } from '../../hooks/use_charts'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx index f7ff03898fda6..4c66bbba631fb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instance.tsx @@ -10,7 +10,7 @@ import { useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx index 18f941c398af0..489ad110c40fd 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { BeatsTemplate } from './beats_template'; diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx index 8d28119c4ec1b..1fa37a2c7b3e6 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/overview.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; import { BeatsTemplate } from './beats_template'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useCharts } from '../../hooks/use_charts'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx index 3a717036396e9..b78df27cd12c4 100644 --- a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -10,14 +10,17 @@ import { i18n } from '@kbn/i18n'; import { CODE_PATH_ALL } from '../../../../common/constants'; import { PageTemplate } from '../page_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { TabMenuItem } from '../page_template'; import { Overview } from '../../../components/cluster/overview'; -import { ExternalConfigContext } from '../../external_config_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { fetchClusters } from '../../../lib/fetch_clusters'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal'; const CODE_PATHS = [CODE_PATH_ALL]; @@ -28,6 +31,7 @@ export const ClusterOverview: React.FC<{}> = () => { const clusterUuid = state.cluster_uuid; const ccs = state.ccs; const [clusters, setClusters] = useState([] as any); + const [alerts, setAlerts] = useState({}); const [loaded, setLoaded] = useState(false); const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); @@ -54,23 +58,27 @@ export const ClusterOverview: React.FC<{}> = () => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); - try { - if (services.http?.fetch) { - const response = await fetchClusters({ - fetch: services.http.fetch, - timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), - }, - ccs, - clusterUuid, - codePaths: CODE_PATHS, - }); - setClusters(response); - } - } catch (err) { - // TODO: handle errors - } finally { + if (services.http?.fetch && clusterUuid) { + const response = await fetchClusters({ + fetch: services.http.fetch, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ccs, + clusterUuid, + codePaths: CODE_PATHS, + }); + setClusters(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + timeRange: { + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), + }, + }); + setAlerts(alertsResponse); setLoaded(true); } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); @@ -89,7 +97,7 @@ export const ClusterOverview: React.FC<{}> = () => { {flyoutComponent} @@ -98,6 +106,7 @@ export const ClusterOverview: React.FC<{}> = () => { )} /> + ); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx index 294aeade5e38b..8a9a736286c3f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx @@ -9,13 +9,15 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { Ccr } from '../../../components/elasticsearch/ccr'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; interface SetupModeProps { setupMode: any; @@ -33,6 +35,7 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => }) as any; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.title', { defaultMessage: 'Elasticsearch - Ccr', @@ -46,18 +49,30 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/ccr`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -73,7 +88,7 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx index bec2f278f1774..21f9fd10f0806 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx @@ -10,13 +10,15 @@ import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { PageTemplate } from '../page_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore import { CcrShardReact } from '../../../components/elasticsearch/ccr_shard'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; interface SetupModeProps { setupMode: any; @@ -24,7 +26,7 @@ interface SetupModeProps { bottomBarComponent: any; } -export const ElasticsearchCcrShardPage: React.FC = ({ clusters }) => { +export const ElasticsearchCcrShardPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index, shardId }: { index: string; shardId: string } = useParams(); @@ -32,6 +34,7 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.title', { defaultMessage: 'Elasticsearch - Ccr - Shard', @@ -57,18 +60,34 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/ccr/${index}/shard/${shardId}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_CCR_READ_EXCEPTIONS], + clusterUuid, + filters: [ + { + shardId, + }, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, index, shardId]); return ( @@ -84,7 +103,7 @@ export const ElasticsearchCcrShardPage: React.FC = ({ clusters } render={({ flyoutComponent, bottomBarComponent }: SetupModeProps) => ( {flyoutComponent} - + {bottomBarComponent} )} diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx index a635d98fcbbb0..86dba4e2f921c 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; @@ -16,15 +16,18 @@ import { useCharts } from '../../hooks/use_charts'; import { ItemTemplate } from './item_template'; // @ts-ignore import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced'; -import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { ELASTICSEARCH_SYSTEM_ID, RULE_LARGE_SHARD_SIZE } from '../../../../common/constants'; -export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => { +export const ElasticsearchIndexAdvancedPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); const { zoomInfo, onBrush } = useCharts(); const clusterUuid = globalState.cluster_uuid; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', @@ -36,17 +39,34 @@ export const ElasticsearchIndexAdvancedPage: React.FC = ({ clust const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: index, + }, + ], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - setData(response); + }); + setAlerts(alertsResponse); + } }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); return ( @@ -58,7 +78,7 @@ export const ElasticsearchIndexAdvancedPage: React.FC = ({ clust {flyoutComponent} = ({ clusters }) => { +export const ElasticsearchIndexPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); @@ -31,6 +33,7 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = const [data, setData] = useState({} as any); const [indexLabel, setIndexLabel] = useState(labels.index as any); const [nodesByIndicesData, setNodesByIndicesData] = useState([]); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', @@ -49,23 +52,40 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices/${index}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + setData(response); + const transformer = indicesByNodes(); + setNodesByIndicesData(transformer(response.shards, response.nodes)); + + const shards = response.shards; + if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) { + setIndexLabel(labels.indexWithUnassigned); + } + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], + filters: [ + { + shardIndex: index, + }, + ], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - setData(response); - const transformer = indicesByNodes(); - setNodesByIndicesData(transformer(response.shards, response.nodes)); - - const shards = response.shards; - if (shards.some((shard: any) => shard.state === 'UNASSIGNED')) { - setIndexLabel(labels.indexWithUnassigned); + }); + setAlerts(alertsResponse); } }, [clusterUuid, services.data?.query.timefilter.timefilter, services.http, index]); @@ -85,7 +105,7 @@ export const ElasticsearchIndexPage: React.FC = ({ clusters }) = = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -32,6 +34,7 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) 'showSystemIndices', false ); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { defaultMessage: 'Elasticsearch - Indices', @@ -49,26 +52,38 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/indices`; - const response = await services.http?.fetch(url, { - method: 'POST', - query: { - show_system_indices: showSystemIndices, - }, - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + query: { + show_system_indices: showSystemIndices, + }, + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [RULE_LARGE_SHARD_SIZE], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - setData(response); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - showSystemIndices, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, + showSystemIndices, + ccs, ]); return ( @@ -88,7 +103,7 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) = ({ clusters }) => { +export const ElasticsearchNodeAdvancedPage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { zoomInfo, onBrush } = useCharts(); @@ -25,6 +35,7 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.node.advanced.title', { defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Advanced', @@ -43,20 +54,42 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes/${node}`; - - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, + ], + filters: [ + { + nodeUuid: node, + }, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, node]); return ( @@ -69,7 +102,7 @@ export const ElasticsearchNodeAdvancedPage: React.FC = ({ cluste > = ({ clusters }) => { +export const ElasticsearchNodePage: React.FC = () => { const globalState = useContext(GlobalStateContext); const { zoomInfo, onBrush } = useCharts(); const [showSystemIndices, setShowSystemIndices] = useLocalStorage( 'showSystemIndices', false ); + const [alerts, setAlerts] = useState({}); const { node }: { node: string } = useParams(); const { services } = useKibana<{ data: any }>(); @@ -54,30 +65,49 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes/${node}`; + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + showSystemIndices, + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - showSystemIndices, - ccs, + setData(response); + const transformer = nodesByIndices(); + setNodesByIndicesData(transformer(response.shards, response.nodes)); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MISSING_MONITORING_DATA, + RULE_DISK_USAGE, + RULE_MEMORY_USAGE, + ], + filters: [{ nodeUuid: node }], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - - setData(response); - const transformer = nodesByIndices(); - setNodesByIndicesData(transformer(response.shards, response.nodes)); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, node, showSystemIndices, + ccs, ]); const toggleShowSystemIndices = useCallback(() => { @@ -98,7 +128,7 @@ export const ElasticsearchNodePage: React.FC = ({ clusters }) => {flyoutComponent} = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -32,6 +42,7 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = cluster_uuid: clusterUuid, }) as any; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.nodes.routeTitle', { defaultMessage: 'Elasticsearch - Nodes', @@ -52,25 +63,44 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/elasticsearch/nodes`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ...getPaginationRouteOptions(), + }), + }); + + setData(response); + updateTotalItemCount(response.totalNodeCount); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + clusterUuid, + alertTypeIds: [ + RULE_CPU_USAGE, + RULE_DISK_USAGE, + RULE_THREAD_POOL_SEARCH_REJECTIONS, + RULE_THREAD_POOL_WRITE_REJECTIONS, + RULE_MEMORY_USAGE, + RULE_MISSING_MONITORING_DATA, + ], timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - ...getPaginationRouteOptions(), - }), - }); - - setData(response); - updateTotalItemCount(response.totalNodeCount); + }); + setAlerts(alertsResponse); + } }, [ - ccs, - clusterUuid, services.data?.query.timefilter.timefilter, services.http, + clusterUuid, + ccs, getPaginationRouteOptions, updateTotalItemCount, ]); @@ -94,7 +124,7 @@ export const ElasticsearchNodesPage: React.FC = ({ clusters }) = clusterUuid={globalState.cluster_uuid} setupMode={setupMode} nodes={data.nodes} - alerts={{}} + alerts={alerts} showCgroupMetricsElasticsearch={showCgroupMetricsElasticsearch} {...getPaginationTableProps()} /> diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx index 3334c7e7b880a..c58aaa5dffb04 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ElasticsearchOverview } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; diff --git a/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx b/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx index 906db1b57f0f5..a31f2bc317fa6 100644 --- a/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/home/cluster_listing.tsx @@ -12,8 +12,8 @@ import { useKibana } from '../../../../../../../src/plugins/kibana_react/public' // @ts-ignore import { Listing } from '../../../components/cluster/listing'; import { EnableAlertsModal } from '../../../alerts/enable_alerts_modal'; -import { GlobalStateContext } from '../../global_state_context'; -import { ExternalConfigContext } from '../../external_config_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { ComponentProps } from '../../route_init'; import { useTable } from '../../hooks/use_table'; import { PageTemplate, TabMenuItem } from '../page_template'; @@ -69,23 +69,19 @@ export const ClusterListing: React.FC = () => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); - try { - if (services.http?.fetch) { - const response = await fetchClusters({ - fetch: services.http.fetch, - timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), - }, - ccs: globalState.ccs, - codePaths: ['all'], - }); - setClusters(response); - } - } catch (err) { - // TODO: handle errors + if (services.http?.fetch) { + const response = await fetchClusters({ + fetch: services.http.fetch, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + ccs: globalState.ccs, + codePaths: ['all'], + }); + setClusters(response); } - }, [globalState, services.data?.query.timefilter.timefilter, services.http]); + }, [globalState.ccs, services.data?.query.timefilter.timefilter, services.http]); if (globalState.save && clusters.length === 1) { globalState.cluster_uuid = clusters[0].cluster_uuid; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx index 8b88fc47a9007..444794d118b0f 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx @@ -19,7 +19,7 @@ import { EuiPanel, } from '@elastic/eui'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore @@ -30,6 +30,9 @@ import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { DetailStatus } from '../../../components/kibana/detail_status'; import { PageTemplate } from '../page_template'; import { AlertsCallout } from '../../../alerts/callout'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; const KibanaInstance = ({ data, alerts }: { data: any; alerts: any }) => { const { zoomInfo, onBrush } = useCharts(); @@ -112,6 +115,7 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { }) as any; const [data, setData] = useState({} as any); const [instanceName, setInstanceName] = useState(''); + const [alerts, setAlerts] = useState({}); const title = `Kibana - ${instanceName}`; const pageTitle = i18n.translate('xpack.monitoring.kibana.instance.pageTitle', { @@ -133,19 +137,30 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/${instance}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + setData(response); + setInstanceName(response.kibanaSummary.name); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); - setInstanceName(response.kibanaSummary.name); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, instance, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -156,7 +171,7 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { data-test-subj="kibanaInstancePage" >
- +
); diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx index 436a1a72b2fdb..ae0237ea40472 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx @@ -9,7 +9,7 @@ import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ComponentProps } from '../../route_init'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { useTable } from '../../hooks/use_table'; import { KibanaTemplate } from './kibana_template'; @@ -19,7 +19,9 @@ import { KibanaInstances } from '../../../components/kibana/instances'; import { SetupModeRenderer, SetupModeProps } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -import { KIBANA_SYSTEM_ID } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { KIBANA_SYSTEM_ID, RULE_KIBANA_VERSION_MISMATCH } from '../../../../common/constants'; export const KibanaInstancesPage: React.FC = ({ clusters }) => { const { cluster_uuid: clusterUuid, ccs } = useContext(GlobalStateContext); @@ -30,6 +32,7 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }) as any; const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.kibana.instances.routeTitle', { defaultMessage: 'Kibana - Instances', @@ -50,19 +53,31 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/kibana/instances`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + updateTotalItemCount(response.kibanas.length); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_KIBANA_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); - updateTotalItemCount(response.stats.total); + }); + setAlerts(alertsResponse); + } }, [ ccs, clusterUuid, @@ -85,7 +100,7 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { {flyoutComponent} = ({ clusters }) => { const globalState = useContext(GlobalStateContext); @@ -42,6 +45,7 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.logstash.node.advanced.routeTitle', { defaultMessage: 'Logstash - {nodeName} - Advanced', @@ -60,19 +64,30 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: true, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: true, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ ccs, clusterUuid, @@ -105,7 +120,7 @@ export const LogStashNodeAdvancedPage: React.FC = ({ clusters }) {data.nodeSummary && } - + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx index 301d3c45dedb5..1163a619dd84b 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node.tsx @@ -18,7 +18,7 @@ import { EuiFlexItem, } from '@elastic/eui'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { List } from '../../../components/logstash/pipeline_viewer/models/list'; @@ -30,6 +30,9 @@ import { DetailStatus } from '../../../components/logstash/detail_status'; import { MonitoringTimeseriesContainer } from '../../../components/chart'; import { AlertsCallout } from '../../../alerts/callout'; import { useCharts } from '../../hooks/use_charts'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; +import { RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; export const LogStashNodePage: React.FC = ({ clusters }) => { const match = useRouteMatch<{ uuid: string | undefined }>(); @@ -41,6 +44,7 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const { zoomInfo, onBrush } = useCharts(); const title = i18n.translate('xpack.monitoring.logstash.node.routeTitle', { defaultMessage: 'Logstash - {nodeName}', @@ -59,19 +63,30 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/node/${match.params.uuid}`; const bounds = services.data?.query.timefilter.timefilter.getBounds(); - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + is_advanced: false, + }), + }); + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - is_advanced: false, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http, match.params]); const metricsToShow = useMemo(() => { @@ -99,7 +114,7 @@ export const LogStashNodePage: React.FC = ({ clusters }) => { {data.nodeSummary && } - + {metricsToShow.map((metric, index) => ( diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx index 1c956603f99bd..e09850eaad5c9 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/node_pipelines.tsx @@ -12,7 +12,7 @@ import { useRouteMatch } from 'react-router-dom'; // @ts-ignore import { isPipelineMonitoringSupportedInVersion } from '../../../lib/logstash/pipelines'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { Listing } from '../../../components/logstash/listing'; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx index 09a97925c56f5..0fd10a93bcd83 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/nodes.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { Listing } from '../../../components/logstash/listing'; @@ -16,7 +16,9 @@ import { LogstashTemplate } from './logstash_template'; import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; -import { LOGSTASH_SYSTEM_ID } from '../../../../common/constants'; +import { LOGSTASH_SYSTEM_ID, RULE_LOGSTASH_VERSION_MISMATCH } from '../../../../common/constants'; +import { AlertsByName } from '../../../alerts/types'; +import { fetchAlerts } from '../../../lib/fetch_alerts'; interface SetupModeProps { setupMode: any; @@ -33,6 +35,7 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { cluster_uuid: clusterUuid, }); const [data, setData] = useState({} as any); + const [alerts, setAlerts] = useState({}); const { getPaginationTableProps } = useTable('logstash.nodes'); const title = i18n.translate('xpack.monitoring.logstash.nodes.routeTitle', { @@ -46,18 +49,30 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { const getPageData = useCallback(async () => { const bounds = services.data?.query.timefilter.timefilter.getBounds(); const url = `../api/monitoring/v1/clusters/${clusterUuid}/logstash/nodes`; - const response = await services.http?.fetch(url, { - method: 'POST', - body: JSON.stringify({ - ccs, + if (services.http?.fetch && clusterUuid) { + const response = await services.http?.fetch(url, { + method: 'POST', + body: JSON.stringify({ + ccs, + timeRange: { + min: bounds.min.toISOString(), + max: bounds.max.toISOString(), + }, + }), + }); + + setData(response); + const alertsResponse = await fetchAlerts({ + fetch: services.http.fetch, + alertTypeIds: [RULE_LOGSTASH_VERSION_MISMATCH], + clusterUuid, timeRange: { - min: bounds.min.toISOString(), - max: bounds.max.toISOString(), + min: bounds.min.valueOf(), + max: bounds.max.valueOf(), }, - }), - }); - - setData(response); + }); + setAlerts(alertsResponse); + } }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); return ( @@ -78,6 +93,7 @@ export const LogStashNodesPage: React.FC = ({ clusters }) => { metrics={data.metrics} data={data.nodes} setupMode={setupMode} + alerts={alerts} {...getPaginationTableProps()} /> {bottomBarComponent} diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx index 1edbe5cf71e7d..339b9e9395569 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/overview.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx index abff0ab17b992..20f1caee2b1d8 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipeline.tsx @@ -10,7 +10,7 @@ import { find } from 'lodash'; import moment from 'moment'; import { useRouteMatch } from 'react-router-dom'; import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; // @ts-ignore import { List } from '../../../components/logstash/pipeline_viewer/models/list'; @@ -24,7 +24,7 @@ import { PipelineState } from '../../../components/logstash/pipeline_viewer/mode import { vertexFactory } from '../../../components/logstash/pipeline_viewer/models/graph/vertex_factory'; import { LogstashTemplate } from './logstash_template'; import { useTable } from '../../hooks/use_table'; -import { ExternalConfigContext } from '../../external_config_context'; +import { ExternalConfigContext } from '../../contexts/external_config_context'; import { formatTimestampToDuration } from '../../../../common'; import { CALCULATE_DURATION_SINCE } from '../../../../common/constants'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; diff --git a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx index 5f4fe634177de..ac750ff81ddaa 100644 --- a/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/logstash/pipelines.tsx @@ -8,7 +8,7 @@ import React, { useContext, useState, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { useKibana, useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; // @ts-ignore diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx index b05bd783b2ff2..26072f53f4752 100644 --- a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx @@ -18,7 +18,8 @@ import { Legacy } from '../../../legacy_shims'; import { Enabler } from './enabler'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { initSetupModeState } from '../../setup_mode/setup_mode'; -import { GlobalStateContext } from '../../global_state_context'; +import { GlobalStateContext } from '../../contexts/global_state_context'; +import { useRequestErrorHandler } from '../../hooks/use_request_error_handler'; const CODE_PATHS = [CODE_PATH_LICENSE]; @@ -77,7 +78,8 @@ export const NoDataPage = () => { ]); const globalState = useContext(GlobalStateContext); - initSetupModeState(globalState, services.http); + const handleRequestError = useRequestErrorHandler(); + initSetupModeState(globalState, services.http, handleRequestError); // From x-pack/plugins/monitoring/public/views/no_data/model_updater.js const updateModel = useCallback( diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index 927c464552087..5c030814d9cdf 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -6,8 +6,9 @@ */ import { EuiTab, EuiTabs } from '@elastic/eui'; -import React, { useContext, useState, useEffect } from 'react'; +import React, { useContext, useState, useEffect, useCallback } from 'react'; import { useHistory } from 'react-router-dom'; +import { IHttpFetchError } from 'kibana/public'; import { useTitle } from '../hooks/use_title'; import { MonitoringToolbar } from '../../components/shared/toolbar'; import { MonitoringTimeContainer } from '../hooks/use_monitoring_time'; @@ -18,6 +19,9 @@ import { updateSetupModeData, } from '../setup_mode/setup_mode'; import { SetupModeFeature } from '../../../common/enums'; +import { AlertsDropdown } from '../../alerts/alerts_dropdown'; +import { ActionMenu } from '../../components/action_menu'; +import { useRequestErrorHandler } from '../hooks/use_request_error_handler'; export interface TabMenuItem { id: string; @@ -46,34 +50,52 @@ export const PageTemplate: React.FC = ({ const { currentTimerange } = useContext(MonitoringTimeContainer.Context); const [loaded, setLoaded] = useState(false); const history = useHistory(); + const [hasError, setHasError] = useState(false); + const handleRequestError = useRequestErrorHandler(); + + const getPageDataResponseHandler = useCallback( + (result: any) => { + setHasError(false); + return result; + }, + [setHasError] + ); useEffect(() => { getPageData?.() - .catch((err) => { - // TODO: handle errors + .then(getPageDataResponseHandler) + .catch((err: IHttpFetchError) => { + handleRequestError(err); + setHasError(true); }) .finally(() => { setLoaded(true); }); - }, [getPageData, currentTimerange]); + }, [getPageData, currentTimerange, getPageDataResponseHandler, handleRequestError]); const onRefresh = () => { - const requests = [getPageData?.()]; + getPageData?.().then(getPageDataResponseHandler).catch(handleRequestError); + if (isSetupModeFeatureEnabled(SetupModeFeature.MetricbeatMigration)) { - requests.push(updateSetupModeData()); + updateSetupModeData(); } - - Promise.allSettled(requests).then((results) => { - // TODO: handle errors - }); }; const createHref = (route: string) => history.createHref({ pathname: route }); const isTabSelected = (route: string) => history.location.pathname === route; + const renderContent = () => { + if (hasError) return null; + if (getPageData && !loaded) return ; + return children; + }; + return (
+ + + {tabs && ( @@ -93,7 +115,7 @@ export const PageTemplate: React.FC = ({ })} )} -
{!getPageData ? children : loaded ? children : }
+
{renderContent()}
); }; diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index 8a9a906dbd563..8a11df3de50ae 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; import { Route, Redirect, useLocation } from 'react-router-dom'; import { useClusters } from './hooks/use_clusters'; -import { GlobalStateContext } from './global_state_context'; +import { GlobalStateContext } from './contexts/global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; export interface ComponentProps { diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx index 70932e5177337..bfdf96ef5b2c1 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx @@ -9,13 +9,13 @@ import React from 'react'; import { render } from 'react-dom'; import { get, includes } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { HttpStart } from 'kibana/public'; +import { HttpStart, IHttpFetchError } from 'kibana/public'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { Legacy } from '../../legacy_shims'; import { SetupModeEnterButton } from '../../components/setup_mode/enter_button'; import { SetupModeFeature } from '../../../common/enums'; import { ISetupModeContext } from '../../components/setup_mode/setup_mode_context'; -import { State as GlobalState } from '../../application/global_state_context'; +import { State as GlobalState } from '../contexts/global_state_context'; function isOnPage(hash: string) { return includes(window.location.hash, hash); @@ -23,6 +23,7 @@ function isOnPage(hash: string) { let globalState: GlobalState; let httpService: HttpStart; +let errorHandler: (error: IHttpFetchError) => void; interface ISetupModeState { enabled: boolean; @@ -65,8 +66,8 @@ export const fetchCollectionData = async (uuid?: string, fetchWithoutClusterUuid }); return response; } catch (err) { - // TODO: handle errors - throw new Error(err); + errorHandler(err); + throw err; } }; @@ -122,8 +123,8 @@ export const disableElasticsearchInternalCollection = async () => { const response = await httpService.post(url); return response; } catch (err) { - // TODO: handle errors - throw new Error(err); + errorHandler(err); + throw err; } }; @@ -161,10 +162,12 @@ export const setSetupModeMenuItem = () => { export const initSetupModeState = async ( state: GlobalState, http: HttpStart, + handleErrors: (error: IHttpFetchError) => void, callback?: () => void ) => { globalState = state; httpService = http; + errorHandler = handleErrors; if (callback) { setupModeState.callback = callback; } diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js index 337dacd4ecae9..a9ee2464cd423 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.js @@ -27,8 +27,9 @@ import { import { findNewUuid } from '../../components/renderers/lib/find_new_uuid'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../../application/contexts/global_state_context'; import { withKibana } from '../../../../../../src/plugins/kibana_react/public'; +import { useRequestErrorHandler } from '../hooks/use_request_error_handler'; class WrappedSetupModeRenderer extends React.Component { globalState; @@ -42,8 +43,8 @@ class WrappedSetupModeRenderer extends React.Component { UNSAFE_componentWillMount() { this.globalState = this.context; - const { kibana } = this.props; - initSetupModeState(this.globalState, kibana.services.http, (_oldData) => { + const { kibana, onHttpError } = this.props; + initSetupModeState(this.globalState, kibana.services.http, onHttpError, (_oldData) => { const newState = { renderState: true }; const { productName } = this.props; @@ -213,5 +214,12 @@ class WrappedSetupModeRenderer extends React.Component { } } +function withErrorHandler(Component) { + return function WrappedComponent(props) { + const handleRequestError = useRequestErrorHandler(); + return ; + }; +} + WrappedSetupModeRenderer.contextType = GlobalStateContext; -export const SetupModeRenderer = withKibana(WrappedSetupModeRenderer); +export const SetupModeRenderer = withKibana(withErrorHandler(WrappedSetupModeRenderer)); diff --git a/x-pack/plugins/monitoring/public/components/action_menu/index.tsx b/x-pack/plugins/monitoring/public/components/action_menu/index.tsx new file mode 100644 index 0000000000000..1348ac170395e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/action_menu/index.tsx @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext, useEffect } from 'react'; +import { + KibanaContextProvider, + toMountPoint, + useKibana, +} from '../../../../../../src/plugins/kibana_react/public'; +import { HeaderActionMenuContext } from '../../application/contexts/header_action_menu_context'; + +export const ActionMenu: React.FC<{}> = ({ children }) => { + const { services } = useKibana(); + const { setHeaderActionMenu } = useContext(HeaderActionMenuContext); + useEffect(() => { + if (setHeaderActionMenu) { + setHeaderActionMenu((element) => { + const mount = toMountPoint( + {children} + ); + return mount(element); + }); + return () => { + setHeaderActionMenu(undefined); + }; + } + }, [children, setHeaderActionMenu, services]); + + return null; +}; diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx index 32bbdd6ecbeda..6a1ed1dd16f48 100644 --- a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -14,7 +14,7 @@ import { } from '@elastic/eui'; import React, { useContext, useCallback } from 'react'; import { MonitoringTimeContainer } from '../../application/hooks/use_monitoring_time'; -import { GlobalStateContext } from '../../application/global_state_context'; +import { GlobalStateContext } from '../../application/contexts/global_state_context'; import { Legacy } from '../../legacy_shims'; interface MonitoringToolbarProps { diff --git a/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts b/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts new file mode 100644 index 0000000000000..c0ce7ed260889 --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/fetch_alerts.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { HttpHandler } from 'kibana/public'; +import { CommonAlertFilter } from '../../common/types/alerts'; +import { AlertsByName } from '../alerts/types'; + +interface FetchAlertsParams { + alertTypeIds?: string[]; + filters?: CommonAlertFilter[]; + timeRange: { min: number; max: number }; + clusterUuid: string; + fetch: HttpHandler; +} + +export const fetchAlerts = async ({ + alertTypeIds, + filters, + timeRange, + clusterUuid, + fetch, +}: FetchAlertsParams): Promise => { + const url = `../api/monitoring/v1/alert/${clusterUuid}/status`; + const response = await fetch(url, { + method: 'POST', + body: JSON.stringify({ + alertTypeIds, + filters, + timeRange, + }), + }); + return response as unknown as AlertsByName; +}; From badc77828ec21960708531e4470952ae2d051040 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Mon, 11 Oct 2021 12:55:06 -0400 Subject: [PATCH 023/287] [Cases][Observability] Do not sync alerts status with case status (#114318) * set sync status according to disable alerts * Adding test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/create/form_context.test.tsx | 23 +++++++++++++++++++ .../public/components/create/form_context.tsx | 10 +++++++- .../cases/public/components/create/index.tsx | 2 ++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index b988f13ee34ce..b55542499fbe4 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -239,6 +239,29 @@ describe('Create case', () => { ); }); + it('should set sync alerts to false when the sync setting is passed in as false and alerts are disabled', async () => { + useConnectorsMock.mockReturnValue({ + ...sampleConnectorData, + connectors: connectorsMock, + }); + + const wrapper = mount( + + + + + + + ); + + fillForm(wrapper); + wrapper.find(`[data-test-subj="create-case-submit"]`).first().simulate('click'); + + await waitFor(() => + expect(postCase).toBeCalledWith({ ...sampleData, settings: { syncAlerts: false } }) + ); + }); + it('it should select the default connector set in the configuration', async () => { useCaseConfigureMock.mockImplementation(() => ({ ...useCaseConfigureResponse, diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index f59e1822c70be..03d8ec56fb0ae 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -34,6 +34,7 @@ interface Props { children?: JSX.Element | JSX.Element[]; hideConnectorServiceNowSir?: boolean; onSuccess?: (theCase: Case) => Promise; + syncAlertsDefaultValue?: boolean; } export const FormContext: React.FC = ({ @@ -42,6 +43,7 @@ export const FormContext: React.FC = ({ children, hideConnectorServiceNowSir, onSuccess, + syncAlertsDefaultValue = true, }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); const owner = useOwnerContext(); @@ -51,7 +53,12 @@ export const FormContext: React.FC = ({ const submitCase = useCallback( async ( - { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, + { + connectorId: dataConnectorId, + fields, + syncAlerts = syncAlertsDefaultValue, + ...dataWithoutConnectorId + }, isValid ) => { if (isValid) { @@ -94,6 +101,7 @@ export const FormContext: React.FC = ({ onSuccess, postComment, pushCaseToExternalService, + syncAlertsDefaultValue, ] ); diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index 7f8b8f664529e..d3eaba1ea0bc4 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -58,6 +58,8 @@ const CreateCaseComponent = ({ caseType={caseType} hideConnectorServiceNowSir={hideConnectorServiceNowSir} onSuccess={onSuccess} + // if we are disabling alerts, then we should not sync alerts + syncAlertsDefaultValue={!disableAlerts} > Date: Mon, 11 Oct 2021 12:30:14 -0500 Subject: [PATCH 024/287] [monitoring] fixup types (#114342) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/hooks/use_table.ts | 8 +--- .../setup_mode/setup_mode_renderer.d.ts | 3 +- .../public/components/apm/status_icon.js | 6 +-- .../components/cluster/overview/index.d.ts | 11 ++++- .../elasticsearch/cluster_status/index.d.ts | 9 +++- .../components/elasticsearch/index.d.ts | 12 ------ .../elasticsearch/{index.js => index.ts} | 0 .../indices/{index.js => index.ts} | 0 .../elasticsearch/indices/indices.d.ts | 20 +++++++++ .../ml_job_listing/status_icon.tsx | 10 ++--- .../elasticsearch/ml_jobs/ml_jobs.tsx | 2 +- .../elasticsearch/node/{index.js => index.ts} | 0 .../components/elasticsearch/node/node.d.ts | 20 +++++++++ .../elasticsearch/node/node_react.d.ts | 19 +++++++++ .../node/status_icon.d.ts} | 8 +++- .../elasticsearch/node/status_icon.js | 4 +- .../nodes/{index.js => index.ts} | 0 .../components/elasticsearch/nodes/nodes.d.ts | 15 +++++++ .../overview/{index.js => index.ts} | 0 .../elasticsearch/overview/overview.d.ts | 18 ++++++++ .../transformers/nodes_by_indices.d.ts | 2 +- .../components/elasticsearch/status_icon.js | 4 +- .../public/components/{index.js => index.ts} | 0 .../components/kibana/instances/instances.tsx | 5 +-- .../public/components/kibana/status_icon.js | 6 +-- .../license/{index.js => index.tsx} | 35 ++++++++++++---- .../components/no_data/{index.js => index.ts} | 0 .../{index.d.ts => no_data/no_data.d.ts} | 5 ++- .../page_loading/{index.js => index.tsx} | 12 ++++-- .../{formatting.js => formatting.ts} | 4 +- .../public/components/status_icon/index.js | 28 ------------- .../public/components/status_icon/index.tsx | 42 +++++++++++++++++++ .../summary_status/summary_status.js | 2 +- .../table/{eui_table.js => eui_table.tsx} | 16 +++---- .../table/eui_table_ssp.d.ts} | 8 ++-- .../public/components/table/index.d.ts | 10 ----- .../components/table/{index.js => index.ts} | 0 .../table/{storage.js => storage.ts} | 29 +++++++++---- ...usters.js => get_cluster_from_clusters.ts} | 13 ++++-- 39 files changed, 267 insertions(+), 119 deletions(-) delete mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/elasticsearch/indices/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/node/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts rename x-pack/plugins/monitoring/public/components/{status_icon/index.d.ts => elasticsearch/node/status_icon.d.ts} (56%) rename x-pack/plugins/monitoring/public/components/elasticsearch/nodes/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/nodes/nodes.d.ts rename x-pack/plugins/monitoring/public/components/elasticsearch/overview/{index.js => index.ts} (100%) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts rename x-pack/plugins/monitoring/public/components/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/license/{index.js => index.tsx} (86%) rename x-pack/plugins/monitoring/public/components/no_data/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/{index.d.ts => no_data/no_data.d.ts} (71%) rename x-pack/plugins/monitoring/public/components/page_loading/{index.js => index.tsx} (89%) rename x-pack/plugins/monitoring/public/components/setup_mode/{formatting.js => formatting.ts} (93%) delete mode 100644 x-pack/plugins/monitoring/public/components/status_icon/index.js create mode 100644 x-pack/plugins/monitoring/public/components/status_icon/index.tsx rename x-pack/plugins/monitoring/public/components/table/{eui_table.js => eui_table.tsx} (88%) rename x-pack/plugins/monitoring/public/{lib/get_cluster_from_clusters.d.ts => components/table/eui_table_ssp.d.ts} (68%) delete mode 100644 x-pack/plugins/monitoring/public/components/table/index.d.ts rename x-pack/plugins/monitoring/public/components/table/{index.js => index.ts} (100%) rename x-pack/plugins/monitoring/public/components/table/{storage.js => storage.ts} (71%) rename x-pack/plugins/monitoring/public/lib/{get_cluster_from_clusters.js => get_cluster_from_clusters.ts} (74%) diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts index 2e6018ec89809..45d1f717f5d49 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -6,6 +6,7 @@ */ import { useState, useCallback } from 'react'; +import { EuiTableSortingType } from '@elastic/eui'; import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; @@ -23,12 +24,7 @@ interface Page { index: number; } -interface Sorting { - sort: { - field: string; - direction: string; - }; -} +type Sorting = EuiTableSortingType; const PAGE_SIZE_OPTIONS = [5, 10, 20, 50]; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts index 48e8ee13059c0..c0eda496a09b2 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode_renderer.d.ts @@ -5,8 +5,9 @@ * 2.0. */ -export const SetupModeRenderer: FunctionComponent; +import { FunctionComponent } from 'react'; +export const SetupModeRenderer: FunctionComponent>; export interface SetupModeProps { setupMode: any; flyoutComponent: any; diff --git a/x-pack/plugins/monitoring/public/components/apm/status_icon.js b/x-pack/plugins/monitoring/public/components/apm/status_icon.js index f27bcefc20bcb..14a51313e4aa7 100644 --- a/x-pack/plugins/monitoring/public/components/apm/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/apm/status_icon.js @@ -6,17 +6,17 @@ */ import React from 'react'; -import { StatusIcon } from '../../components/status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../components/status_icon'; import { i18n } from '@kbn/i18n'; export function ApmStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts index 2cfd37e8e27eb..3dc7121446a7a 100644 --- a/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts @@ -5,4 +5,13 @@ * 2.0. */ -export const Overview: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const Overview: FunctionComponent; + +export interface OverviewProps { + cluster: unknown; + setupMode: unknown; + showLicenseExpiration: boolean; + alerts: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts index b7196d25d1791..4f314101ed299 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/cluster_status/index.d.ts @@ -5,4 +5,11 @@ * 2.0. */ -export const ClusterStatus: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const ClusterStatus: FunctionComponent; + +export interface ClusterStatusProps { + stats: unknown; + alerts?: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.ts deleted file mode 100644 index 09f6c1085cfa3..0000000000000 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/index.d.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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const ElasticsearchOverview: FunctionComponent; -export const ElasticsearchNodes: FunctionComponent; -export const ElasticsearchIndices: FunctionComponent; -export const ElasticsearchMLJobs: FunctionComponent; -export const NodeReact: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/indices/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts new file mode 100644 index 0000000000000..2b8ea60b651a6 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/indices/indices.d.ts @@ -0,0 +1,20 @@ +/* + * 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 { FunctionComponent } from 'react'; + +export const ElasticsearchIndices: FunctionComponent; +export interface ElasticsearchIndicesProps { + clusterStatus: unknown; + indices: unknown; + sorting: unknown; + pagination: unknown; + onTableChange: unknown; + toggleShowSystemIndices: unknown; + showSystemIndices: unknown; + alerts: unknown; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx index d5c65aecdec21..a45c8316d1aa3 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_job_listing/status_icon.tsx @@ -7,22 +7,22 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { StatusIcon } from '../../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../status_icon'; export function MachineLearningJobStatusIcon({ status }: { status: string }) { const type = (() => { const statusKey = status.toUpperCase(); if (statusKey === 'OPENED') { - return StatusIcon.TYPES.GREEN; + return STATUS_ICON_TYPES.GREEN; } else if (statusKey === 'CLOSED') { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } else if (statusKey === 'FAILED') { - return StatusIcon.TYPES.RED; + return STATUS_ICON_TYPES.RED; } // basically a "changing" state like OPENING or CLOSING - return StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx index 635f9ecd1e10a..dba9c40fabb2b 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/ml_jobs/ml_jobs.tsx @@ -31,7 +31,7 @@ import { ClusterStatus } from '../cluster_status'; interface Props { clusterStatus: boolean; jobs: MLJobs; - onTableChange: () => void; + onTableChange: (props: any) => void; sorting: EuiTableSortingType; pagination: Pagination; } diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/node/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts new file mode 100644 index 0000000000000..9d7a062e942bb --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node.d.ts @@ -0,0 +1,20 @@ +/* + * 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 { FunctionComponent } from 'react'; + +export const Node: FunctionComponent; +export interface NodeProps { + nodeSummary: unknown; + metrics: unknown; + logs: unknown; + alerts: unknown; + nodeId: unknown; + clusterUuid: unknown; + scope: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts new file mode 100644 index 0000000000000..e0c4f6b301fdb --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/node_react.d.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionComponent } from 'react'; + +export const NodeReact: FunctionComponent; +export interface NodeReactProps { + nodeSummary: unknown; + metrics: unknown; + logs: unknown; + alerts: unknown; + nodeId: unknown; + clusterUuid: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts similarity index 56% rename from x-pack/plugins/monitoring/public/components/status_icon/index.d.ts rename to x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts index 147c2821e3a2a..dfa07524619c9 100644 --- a/x-pack/plugins/monitoring/public/components/status_icon/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.d.ts @@ -5,4 +5,10 @@ * 2.0. */ -export const StatusIcon: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const NodeStatusIcon: FunctionComponent; +export interface NodeStatusIconProps { + isOnline: boolean; + status: string; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js index 7bfffc7b73954..9905a6c3573f7 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/node/status_icon.js @@ -6,11 +6,11 @@ */ import React from 'react'; -import { StatusIcon } from '../../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../../status_icon'; import { i18n } from '@kbn/i18n'; export function NodeStatusIcon({ isOnline, status }) { - const type = isOnline ? StatusIcon.TYPES.GREEN : StatusIcon.TYPES.GRAY; + const type = isOnline ? STATUS_ICON_TYPES.GREEN : STATUS_ICON_TYPES.GRAY; return ( ; +export interface ElasticsearchNodesProps { + clusterStatus: unknown; + showCgroupMetricsElasticsearch: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.js rename to x-pack/plugins/monitoring/public/components/elasticsearch/overview/index.ts diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts new file mode 100644 index 0000000000000..d4c893f87cbd2 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview.d.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FunctionComponent } from 'react'; + +export const ElasticsearchOverview: FunctionComponent; +export interface ElasticsearchOverviewProps { + clusterStatus: unknown; + metrics: unknown; + logs: unknown; + cluster: unknown; + shardActivity: unknown; + [key: string]: any; +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts index d0ec9b85edae7..c430c0ee7b48a 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_allocation/transformers/nodes_by_indices.d.ts @@ -5,4 +5,4 @@ * 2.0. */ -export const nodesByIndices: () => (shards, nodes) => any; +export const nodesByIndices: () => (shards: any, nodes: any) => any; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js index 7c51a1e89d91e..ec027d71a192d 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/status_icon.js @@ -6,13 +6,13 @@ */ import React from 'react'; -import { StatusIcon } from '../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../status_icon'; import { i18n } from '@kbn/i18n'; export function ElasticsearchStatusIcon({ status }) { const type = (() => { const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.GRAY; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/index.js b/x-pack/plugins/monitoring/public/components/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/index.js rename to x-pack/plugins/monitoring/public/components/index.ts diff --git a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx index 4e939682b1dba..3766a09f91b80 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx +++ b/x-pack/plugins/monitoring/public/components/kibana/instances/instances.tsx @@ -25,8 +25,7 @@ import { capitalize, get } from 'lodash'; import { ClusterStatus } from '../cluster_status'; // @ts-ignore import { EuiMonitoringTable } from '../../table'; -// @ts-ignore -import { StatusIcon } from '../../status_icon'; +import { STATUS_ICON_TYPES } from '../../status_icon'; // @ts-ignore import { formatMetric, formatNumber } from '../../../lib/format_number'; import { getSafeForExternalLink } from '../../../lib/get_safe_for_external_link'; @@ -205,7 +204,7 @@ export const KibanaInstances: React.FC = (props: Props) => { _instances.push({ kibana: { ...(instance as any).instance.kibana, - status: StatusIcon.TYPES.GRAY, + status: STATUS_ICON_TYPES.GRAY, }, }); } diff --git a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js index e5b501b1e15e7..976b3ff992e3b 100644 --- a/x-pack/plugins/monitoring/public/components/kibana/status_icon.js +++ b/x-pack/plugins/monitoring/public/components/kibana/status_icon.js @@ -6,17 +6,17 @@ */ import React from 'react'; -import { StatusIcon } from '../status_icon'; +import { StatusIcon, STATUS_ICON_TYPES } from '../status_icon'; import { i18n } from '@kbn/i18n'; export function KibanaStatusIcon({ status, availability = true }) { const type = (() => { if (!availability) { - return StatusIcon.TYPES.GRAY; + return STATUS_ICON_TYPES.GRAY; } const statusKey = status.toUpperCase(); - return StatusIcon.TYPES[statusKey] || StatusIcon.TYPES.YELLOW; + return STATUS_ICON_TYPES[statusKey] || STATUS_ICON_TYPES.YELLOW; })(); return ( diff --git a/x-pack/plugins/monitoring/public/components/license/index.js b/x-pack/plugins/monitoring/public/components/license/index.tsx similarity index 86% rename from x-pack/plugins/monitoring/public/components/license/index.js rename to x-pack/plugins/monitoring/public/components/license/index.tsx index ad16663c88ea7..766f0af3bccc8 100644 --- a/x-pack/plugins/monitoring/public/components/license/index.js +++ b/x-pack/plugins/monitoring/public/components/license/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiPage, EuiPageBody, @@ -27,7 +27,10 @@ import { import { FormattedMessage } from '@kbn/i18n/react'; import { Legacy } from '../../legacy_shims'; -export const AddLicense = ({ uploadPath }) => { +interface AddLicenseProps { + uploadPath?: string; +} +const AddLicense: FunctionComponent = ({ uploadPath }) => { return ( { ); }; -export class LicenseStatus extends React.PureComponent { +export interface LicenseStatusProps { + isExpired: boolean; + status: string; + type: string; + expiryDate: string | Date; +} + +class LicenseStatus extends React.PureComponent { render() { const { isExpired, status, type, expiryDate } = this.props; const typeTitleCase = type.charAt(0).toUpperCase() + type.substr(1).toLowerCase(); @@ -133,7 +143,15 @@ export class LicenseStatus extends React.PureComponent { } } -const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => { +export interface LicenseUpdateInfoProps { + isPrimaryCluster: boolean; + uploadLicensePath?: string; +} + +const LicenseUpdateInfoForPrimary: FunctionComponent = ({ + isPrimaryCluster, + uploadLicensePath, +}) => { if (!isPrimaryCluster) { return null; } @@ -142,7 +160,9 @@ const LicenseUpdateInfoForPrimary = ({ isPrimaryCluster, uploadLicensePath }) => return ; }; -const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { +const LicenseUpdateInfoForRemote: FunctionComponent = ({ + isPrimaryCluster, +}) => { if (isPrimaryCluster) { return null; } @@ -168,7 +188,8 @@ const LicenseUpdateInfoForRemote = ({ isPrimaryCluster }) => { ); }; -export function License(props) { +export interface LicenseProps extends LicenseStatusProps, LicenseUpdateInfoProps {} +export const License: FunctionComponent = (props) => { const { status, type, isExpired, expiryDate } = props; const licenseManagement = `${Legacy.shims.getBasePath()}/app/management/stack/license_management`; return ( @@ -199,4 +220,4 @@ export function License(props) {
); -} +}; diff --git a/x-pack/plugins/monitoring/public/components/no_data/index.js b/x-pack/plugins/monitoring/public/components/no_data/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/no_data/index.js rename to x-pack/plugins/monitoring/public/components/no_data/index.ts diff --git a/x-pack/plugins/monitoring/public/components/index.d.ts b/x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts similarity index 71% rename from x-pack/plugins/monitoring/public/components/index.d.ts rename to x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts index fc1a81cc4dba2..b87d326e834af 100644 --- a/x-pack/plugins/monitoring/public/components/index.d.ts +++ b/x-pack/plugins/monitoring/public/components/no_data/no_data.d.ts @@ -5,5 +5,6 @@ * 2.0. */ -export const PageLoading: FunctionComponent; -export const License: FunctionComponent; +import { FunctionComponent } from 'react'; + +export const NoData: FunctionComponent>; diff --git a/x-pack/plugins/monitoring/public/components/page_loading/index.js b/x-pack/plugins/monitoring/public/components/page_loading/index.tsx similarity index 89% rename from x-pack/plugins/monitoring/public/components/page_loading/index.js rename to x-pack/plugins/monitoring/public/components/page_loading/index.tsx index fd4aa9d848150..e7535fc3dc859 100644 --- a/x-pack/plugins/monitoring/public/components/page_loading/index.js +++ b/x-pack/plugins/monitoring/public/components/page_loading/index.tsx @@ -48,17 +48,21 @@ function PageLoadingUI() { ); } -function PageLoadingTracking({ pageViewTitle }) { +const PageLoadingTracking: React.FunctionComponent<{ pageViewTitle: string }> = ({ + pageViewTitle, +}) => { const path = pageViewTitle.toLowerCase().replace(/-/g, '').replace(/\s+/g, '_'); useTrackPageview({ app: 'stack_monitoring', path }); useTrackPageview({ app: 'stack_monitoring', path, delay: 15000 }); return ; -} +}; -export function PageLoading({ pageViewTitle }) { +export const PageLoading: React.FunctionComponent<{ pageViewTitle?: string }> = ({ + pageViewTitle, +}) => { if (pageViewTitle) { return ; } return ; -} +}; diff --git a/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts similarity index 93% rename from x-pack/plugins/monitoring/public/components/setup_mode/formatting.js rename to x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts index 11e8ca48719fd..06eb029fcc4a0 100644 --- a/x-pack/plugins/monitoring/public/components/setup_mode/formatting.js +++ b/x-pack/plugins/monitoring/public/components/setup_mode/formatting.ts @@ -34,7 +34,7 @@ const SERVER_IDENTIFIER_PLURAL = i18n.translate('xpack.monitoring.setupMode.serv defaultMessage: `servers`, }); -export function formatProductName(productName) { +export function formatProductName(productName: string) { if (productName === APM_SYSTEM_ID) { return productName.toUpperCase(); } @@ -43,7 +43,7 @@ export function formatProductName(productName) { const PRODUCTS_THAT_USE_NODES = [LOGSTASH_SYSTEM_ID, ELASTICSEARCH_SYSTEM_ID]; const PRODUCTS_THAT_USE_INSTANCES = [KIBANA_SYSTEM_ID, BEATS_SYSTEM_ID]; -export function getIdentifier(productName, usePlural = false) { +export function getIdentifier(productName: string, usePlural = false) { if (PRODUCTS_THAT_USE_INSTANCES.includes(productName)) { return usePlural ? INSTANCE_IDENTIFIER_PLURAL : INSTANCE_IDENTIFIER_SINGULAR; } diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.js b/x-pack/plugins/monitoring/public/components/status_icon/index.js deleted file mode 100644 index bcd4b58d6912f..0000000000000 --- a/x-pack/plugins/monitoring/public/components/status_icon/index.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { EuiIcon } from '@elastic/eui'; - -export function StatusIcon({ type, label }) { - const typeToIconMap = { - [StatusIcon.TYPES.RED]: 'danger', - [StatusIcon.TYPES.YELLOW]: 'warning', - [StatusIcon.TYPES.GREEN]: 'success', - [StatusIcon.TYPES.GRAY]: 'subdued', - }; - const icon = typeToIconMap[type]; - - return ; -} - -StatusIcon.TYPES = { - RED: 'RED', - YELLOW: 'YELLOW', - GREEN: 'GREEN', - GRAY: 'GRAY', -}; diff --git a/x-pack/plugins/monitoring/public/components/status_icon/index.tsx b/x-pack/plugins/monitoring/public/components/status_icon/index.tsx new file mode 100644 index 0000000000000..59c87866d57d3 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/status_icon/index.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiIcon } from '@elastic/eui'; + +export const STATUS_ICON_TYPES = { + RED: 'RED' as const, + YELLOW: 'YELLOW' as const, + GREEN: 'GREEN' as const, + GRAY: 'GRAY' as const, +}; + +const typeToIconMap = { + [STATUS_ICON_TYPES.RED]: 'danger', + [STATUS_ICON_TYPES.YELLOW]: 'warning', + [STATUS_ICON_TYPES.GREEN]: 'success', + [STATUS_ICON_TYPES.GRAY]: 'subdued', +}; + +export interface StatusIconProps { + type: keyof typeof STATUS_ICON_TYPES; + label: string; +} +export const StatusIcon: React.FunctionComponent = ({ type, label }) => { + const icon = typeToIconMap[type]; + + return ( + + ); +}; diff --git a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js index 71fcf4e193f20..db4ac9098532b 100644 --- a/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js +++ b/x-pack/plugins/monitoring/public/components/summary_status/summary_status.js @@ -9,7 +9,7 @@ import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { isEmpty, capitalize } from 'lodash'; import { EuiFlexGroup, EuiFlexItem, EuiStat } from '@elastic/eui'; -import { StatusIcon } from '../status_icon/index.js'; +import { StatusIcon } from '../status_icon'; import { AlertsStatus } from '../../alerts/status'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/monitoring/public/components/table/eui_table.js b/x-pack/plugins/monitoring/public/components/table/eui_table.tsx similarity index 88% rename from x-pack/plugins/monitoring/public/components/table/eui_table.js rename to x-pack/plugins/monitoring/public/components/table/eui_table.tsx index a702fdc033572..a383fcf1cd666 100644 --- a/x-pack/plugins/monitoring/public/components/table/eui_table.js +++ b/x-pack/plugins/monitoring/public/components/table/eui_table.tsx @@ -5,21 +5,21 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React, { Fragment, FunctionComponent } from 'react'; import { EuiInMemoryTable, EuiButton, EuiSpacer, EuiSearchBar } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { getIdentifier } from '../setup_mode/formatting'; import { isSetupModeFeatureEnabled } from '../../lib/setup_mode'; import { SetupModeFeature } from '../../../common/enums'; -export function EuiMonitoringTable({ +export const EuiMonitoringTable: FunctionComponent> = ({ rows: items, search = {}, columns: _columns, setupMode, productName, ...props -}) { +}) => { const [hasItems, setHasItem] = React.useState(items.length > 0); if (search.box && !search.box['data-test-subj']) { @@ -32,15 +32,17 @@ export function EuiMonitoringTable({ if (search) { const oldOnChange = search.onChange; - search.onChange = (arg) => { + search.onChange = (arg: any) => { const filteredItems = EuiSearchBar.Query.execute(arg.query, items, props.executeQueryOptions); setHasItem(filteredItems.length > 0); - oldOnChange && oldOnChange(arg); + if (oldOnChange) { + oldOnChange(arg); + } return true; }; } - const columns = _columns.map((column) => { + const columns = _columns.map((column: any) => { if (!('sortable' in column)) { column.sortable = true; } @@ -78,4 +80,4 @@ export function EuiMonitoringTable({ {footerContent}

); -} +}; diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts similarity index 68% rename from x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts rename to x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts index 5a310c977efae..bdc8199b3c57c 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts +++ b/x-pack/plugins/monitoring/public/components/table/eui_table_ssp.d.ts @@ -5,8 +5,6 @@ * 2.0. */ -export const getClusterFromClusters: ( - clusters: any, - globalState: State, - unsetGlobalState: boolean -) => any; +import { FunctionComponent } from 'react'; + +export const EuiMonitoringSSPTable: FunctionComponent>; diff --git a/x-pack/plugins/monitoring/public/components/table/index.d.ts b/x-pack/plugins/monitoring/public/components/table/index.d.ts deleted file mode 100644 index 23406ba9e3a5e..0000000000000 --- a/x-pack/plugins/monitoring/public/components/table/index.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const euiTableStorageGetter: (string) => any; -export const euiTableStorageSetter: (string) => any; -export const EuiMonitoringTable: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/table/index.js b/x-pack/plugins/monitoring/public/components/table/index.ts similarity index 100% rename from x-pack/plugins/monitoring/public/components/table/index.js rename to x-pack/plugins/monitoring/public/components/table/index.ts diff --git a/x-pack/plugins/monitoring/public/components/table/storage.js b/x-pack/plugins/monitoring/public/components/table/storage.ts similarity index 71% rename from x-pack/plugins/monitoring/public/components/table/storage.js rename to x-pack/plugins/monitoring/public/components/table/storage.ts index b9694dc5db420..411bd09872858 100644 --- a/x-pack/plugins/monitoring/public/components/table/storage.js +++ b/x-pack/plugins/monitoring/public/components/table/storage.ts @@ -8,9 +8,22 @@ import { set } from '@elastic/safer-lodash-set'; import { get } from 'lodash'; import { STORAGE_KEY } from '../../../common/constants'; +import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -export const tableStorageGetter = (keyPrefix) => { - return (storage) => { +interface TableValues { + filterText: any; + pageIndex: any; + sortKey: any; + sortOrder: any; +} + +interface EuiTableValues { + sort: any; + page: any; +} + +export const tableStorageGetter = (keyPrefix: string) => { + return (storage: Storage): TableValues => { const localStorageData = storage.get(STORAGE_KEY) || {}; const filterText = get(localStorageData, [keyPrefix, 'filterText']); const pageIndex = get(localStorageData, [keyPrefix, 'pageIndex']); @@ -21,8 +34,8 @@ export const tableStorageGetter = (keyPrefix) => { }; }; -export const tableStorageSetter = (keyPrefix) => { - return (storage, { filterText, pageIndex, sortKey, sortOrder }) => { +export const tableStorageSetter = (keyPrefix: string) => { + return (storage: Storage, { filterText, pageIndex, sortKey, sortOrder }: TableValues) => { const localStorageData = storage.get(STORAGE_KEY) || {}; set(localStorageData, [keyPrefix, 'filterText'], filterText || undefined); // don`t store empty data @@ -36,8 +49,8 @@ export const tableStorageSetter = (keyPrefix) => { }; }; -export const euiTableStorageGetter = (keyPrefix) => { - return (storage) => { +export const euiTableStorageGetter = (keyPrefix: string) => { + return (storage: Storage): EuiTableValues => { const localStorageData = storage.get(STORAGE_KEY) || {}; const sort = get(localStorageData, [keyPrefix, 'sort']); const page = get(localStorageData, [keyPrefix, 'page']); @@ -46,8 +59,8 @@ export const euiTableStorageGetter = (keyPrefix) => { }; }; -export const euiTableStorageSetter = (keyPrefix) => { - return (storage, { sort, page }) => { +export const euiTableStorageSetter = (keyPrefix: string) => { + return (storage: Storage, { sort, page }: EuiTableValues) => { const localStorageData = storage.get(STORAGE_KEY) || {}; set(localStorageData, [keyPrefix, 'sort'], sort || undefined); // don`t store empty data diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts similarity index 74% rename from x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js rename to x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts index 94bd39aa769fd..837f59aaf7c20 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.js +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts @@ -6,15 +6,20 @@ */ import { find, first } from 'lodash'; +import { State } from '../application/global_state_context'; -export function getClusterFromClusters(clusters, globalState, unsetGlobalState = false) { +export function getClusterFromClusters( + clusters: any, + globalState: State, + unsetGlobalState = false +) { const cluster = (() => { const existingCurrent = find(clusters, { cluster_uuid: globalState.cluster_uuid }); if (existingCurrent) { return existingCurrent; } - const firstCluster = first(clusters); + const firstCluster: any = first(clusters); if (firstCluster && firstCluster.cluster_uuid) { return firstCluster; } @@ -25,7 +30,9 @@ export function getClusterFromClusters(clusters, globalState, unsetGlobalState = if (cluster && cluster.license) { globalState.cluster_uuid = unsetGlobalState ? undefined : cluster.cluster_uuid; globalState.ccs = unsetGlobalState ? undefined : cluster.ccs; - globalState.save(); + if (globalState.save) { + globalState.save(); + } return cluster; } From 50b3602d7f5eb046fe57e68c5718ab9c2169def5 Mon Sep 17 00:00:00 2001 From: spalger Date: Mon, 11 Oct 2021 17:41:49 +0000 Subject: [PATCH 025/287] fix import --- .../plugins/monitoring/public/lib/get_cluster_from_clusters.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts index 837f59aaf7c20..93d0c5a6f790e 100644 --- a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.ts @@ -6,7 +6,7 @@ */ import { find, first } from 'lodash'; -import { State } from '../application/global_state_context'; +import { State } from '../application/contexts/global_state_context'; export function getClusterFromClusters( clusters: any, From 9d498b962c88680c306722d172ad1841c7ddea27 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 11 Oct 2021 12:55:06 -0500 Subject: [PATCH 026/287] [APM] Disabling apm e2e test (#114544) [skip-ci] APM Cypress tests are again failing on PRs. Disable temporarily. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- vars/tasks.groovy | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 1842e278282b1..5a015bddc8fbc 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -146,13 +146,14 @@ def functionalXpack(Map params = [:]) { } } - whenChanged([ - 'x-pack/plugins/apm/', - ]) { - if (githubPr.isPr()) { - task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) - } - } + //temporarily disable apm e2e test since it's breaking. + // whenChanged([ + // 'x-pack/plugins/apm/', + // ]) { + // if (githubPr.isPr()) { + // task(kibanaPipeline.functionalTestProcess('xpack-APMCypress', './test/scripts/jenkins_apm_cypress.sh')) + // } + // } whenChanged([ 'x-pack/plugins/uptime/', From e32dd1c493b747345b4d1103d14f61898ad5ccfe Mon Sep 17 00:00:00 2001 From: Chris Roberson Date: Mon, 11 Oct 2021 14:19:28 -0400 Subject: [PATCH 027/287] [Alerting] Allow rule types to specify a default and minimum interval (#113650) * WIP * Remove test defaults * Fix types * Add tests * Add missing files * Fix issue with using default interval after user manually changed it * PR feedback * Fix types * Remove debug --- x-pack/plugins/alerting/README.md | 2 + x-pack/plugins/alerting/common/alert_type.ts | 2 + .../alerting/server/routes/rule_types.test.ts | 6 + .../alerting/server/routes/rule_types.ts | 4 + .../server/rule_type_registry.test.ts | 57 +++- .../alerting/server/rule_type_registry.ts | 44 +++ .../server/rules_client/rules_client.ts | 22 ++ .../server/rules_client/tests/create.test.ts | 26 ++ .../server/rules_client/tests/update.test.ts | 46 +++ x-pack/plugins/alerting/server/types.ts | 2 + .../public/application/constants/index.ts | 2 + .../components/alert_details.tsx | 1 + .../sections/alert_form/alert_add.test.tsx | 24 +- .../sections/alert_form/alert_add.tsx | 57 +++- .../sections/alert_form/alert_edit.test.tsx | 34 ++- .../sections/alert_form/alert_edit.tsx | 33 +- .../sections/alert_form/alert_errors.test.tsx | 284 ++++++++++++++++++ .../sections/alert_form/alert_errors.ts | 132 ++++++++ .../sections/alert_form/alert_form.tsx | 149 +++------ .../alerts_list/components/alerts_list.tsx | 25 +- .../triggers_actions_ui/public/types.ts | 6 +- 21 files changed, 822 insertions(+), 136 deletions(-) create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts diff --git a/x-pack/plugins/alerting/README.md b/x-pack/plugins/alerting/README.md index 58d2ca35dea7e..343960aee9dfb 100644 --- a/x-pack/plugins/alerting/README.md +++ b/x-pack/plugins/alerting/README.md @@ -122,6 +122,8 @@ The following table describes the properties of the `options` object. |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 |isExportable|Whether the rule type is exportable from the Saved Objects Management UI.|boolean| +|defaultScheduleInterval|The default interval that will show up in the UI when creating a rule of this rule type.|boolean| +|minimumScheduleInterval|The minimum interval that will be allowed for all rules of this rule type.|boolean| ### Executor diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerting/common/alert_type.ts index e56034a4c41f8..d71540b4418e8 100644 --- a/x-pack/plugins/alerting/common/alert_type.ts +++ b/x-pack/plugins/alerting/common/alert_type.ts @@ -21,6 +21,8 @@ export interface AlertType< producer: string; minimumLicenseRequired: LicenseType; isExportable: boolean; + defaultScheduleInterval?: string; + minimumScheduleInterval?: string; } export interface ActionGroup { diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index 2e8f43508a969..e4247c9de6cad 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -57,6 +57,8 @@ describe('ruleTypesRoute', () => { }, producer: 'test', enabledInLicense: true, + minimumScheduleInterval: '1m', + defaultScheduleInterval: '10m', } as RegistryAlertTypeWithAuth, ]; const expectedResult: Array> = [ @@ -70,7 +72,9 @@ describe('ruleTypesRoute', () => { }, ], default_action_group_id: 'default', + default_schedule_interval: '10m', minimum_license_required: 'basic', + minimum_schedule_interval: '1m', is_exportable: true, recovery_action_group: RecoveredActionGroup, authorized_consumers: {}, @@ -102,10 +106,12 @@ describe('ruleTypesRoute', () => { }, "authorized_consumers": Object {}, "default_action_group_id": "default", + "default_schedule_interval": "10m", "enabled_in_license": true, "id": "1", "is_exportable": true, "minimum_license_required": "basic", + "minimum_schedule_interval": "1m", "name": "name", "producer": "test", "recovery_action_group": Object { diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index 153ae96ff68ea..72502b25e9aff 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -22,6 +22,8 @@ const rewriteBodyRes: RewriteResponseCase = (result isExportable, actionVariables, authorizedConsumers, + minimumScheduleInterval, + defaultScheduleInterval, ...rest }) => ({ ...rest, @@ -33,6 +35,8 @@ const rewriteBodyRes: RewriteResponseCase = (result is_exportable: isExportable, action_variables: actionVariables, authorized_consumers: authorizedConsumers, + minimum_schedule_interval: minimumScheduleInterval, + default_schedule_interval: defaultScheduleInterval, }) ); }; diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index 1c44e862c261c..beb5f264eb725 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -114,7 +114,7 @@ describe('register()', () => { test('throws if AlertType ruleTaskTimeout is not a valid duration', () => { const alertType: AlertType = { - id: 123 as unknown as string, + id: '123', name: 'Test', actionGroups: [ { @@ -138,6 +138,59 @@ describe('register()', () => { ); }); + test('throws if defaultScheduleInterval isnt valid', () => { + const alertType: AlertType = { + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + producer: 'alerts', + defaultScheduleInterval: 'foobar', + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error( + `Rule type \"123\" has invalid default interval: string is not a valid duration: foobar.` + ) + ); + }); + + test('throws if minimumScheduleInterval isnt valid', () => { + const alertType: AlertType = { + id: '123', + name: 'Test', + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + executor: jest.fn(), + producer: 'alerts', + minimumScheduleInterval: 'foobar', + }; + const registry = new RuleTypeRegistry(ruleTypeRegistryParams); + + expect(() => registry.register(alertType)).toThrowError( + new Error( + `Rule type \"123\" has invalid minimum interval: string is not a valid duration: foobar.` + ) + ); + }); + test('throws if RuleType action groups contains reserved group id', () => { const alertType: AlertType = { id: 'test', @@ -465,10 +518,12 @@ describe('list()', () => { "state": Array [], }, "defaultActionGroupId": "testActionGroup", + "defaultScheduleInterval": undefined, "enabledInLicense": false, "id": "test", "isExportable": true, "minimumLicenseRequired": "basic", + "minimumScheduleInterval": undefined, "name": "Test", "producer": "alerts", "recoveryActionGroup": Object { diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index dc72b644b2c7b..db02edf4d19dd 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -48,6 +48,8 @@ export interface RegistryRuleType | 'producer' | 'minimumLicenseRequired' | 'isExportable' + | 'minimumScheduleInterval' + | 'defaultScheduleInterval' > { id: string; enabledInLicense: boolean; @@ -188,6 +190,44 @@ export class RuleTypeRegistry { } alertType.actionVariables = normalizedActionVariables(alertType.actionVariables); + // validate defaultScheduleInterval here + if (alertType.defaultScheduleInterval) { + const invalidDefaultTimeout = validateDurationSchema(alertType.defaultScheduleInterval); + if (invalidDefaultTimeout) { + throw new Error( + i18n.translate( + 'xpack.alerting.ruleTypeRegistry.register.invalidDefaultTimeoutAlertTypeError', + { + defaultMessage: 'Rule type "{id}" has invalid default interval: {errorMessage}.', + values: { + id: alertType.id, + errorMessage: invalidDefaultTimeout, + }, + } + ) + ); + } + } + + // validate minimumScheduleInterval here + if (alertType.minimumScheduleInterval) { + const invalidMinimumTimeout = validateDurationSchema(alertType.minimumScheduleInterval); + if (invalidMinimumTimeout) { + throw new Error( + i18n.translate( + 'xpack.alerting.ruleTypeRegistry.register.invalidMinimumTimeoutAlertTypeError', + { + defaultMessage: 'Rule type "{id}" has invalid minimum interval: {errorMessage}.', + values: { + id: alertType.id, + errorMessage: invalidMinimumTimeout, + }, + } + ) + ); + } + } + const normalizedAlertType = augmentActionGroupsWithReserved< Params, ExtractedParams, @@ -287,6 +327,8 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + minimumScheduleInterval, + defaultScheduleInterval, }, ]: [string, UntypedNormalizedAlertType]) => ({ id, @@ -298,6 +340,8 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + minimumScheduleInterval, + defaultScheduleInterval, enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType( id, name, diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 231d19ce9a6f8..2228b5d27910f 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -296,6 +296,17 @@ export class RulesClient { await this.validateActions(ruleType, data.actions); + // Validate intervals, if configured + if (ruleType.minimumScheduleInterval) { + const intervalInMs = parseDuration(data.schedule.interval); + const minimumScheduleIntervalInMs = parseDuration(ruleType.minimumScheduleInterval); + if (intervalInMs < minimumScheduleIntervalInMs) { + throw Boom.badRequest( + `Error updating rule: the interval is less than the minimum interval of ${ruleType.minimumScheduleInterval}` + ); + } + } + // Extract saved object references for this rule const { references, @@ -847,6 +858,17 @@ export class RulesClient { ); await this.validateActions(ruleType, data.actions); + // Validate intervals, if configured + if (ruleType.minimumScheduleInterval) { + const intervalInMs = parseDuration(data.schedule.interval); + const minimumScheduleIntervalInMs = parseDuration(ruleType.minimumScheduleInterval); + if (intervalInMs < minimumScheduleIntervalInMs) { + throw Boom.badRequest( + `Error updating rule: the interval is less than the minimum interval of ${ruleType.minimumScheduleInterval}` + ); + } + } + // Extract saved object references for this rule const { references, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts index 2bb92046db68f..fc8f272702e0d 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/create.test.ts @@ -2268,4 +2268,30 @@ describe('create()', () => { expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); expect(taskManager.schedule).not.toHaveBeenCalled(); }); + + test('throws error when updating with an interval less than the minimum configured one', async () => { + ruleTypeRegistry.get.mockImplementation(() => ({ + id: '123', + name: 'Test', + actionGroups: [{ id: 'default', name: 'Default' }], + recoveryActionGroup: RecoveredActionGroup, + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + isExportable: true, + async executor() {}, + producer: 'alerts', + minimumScheduleInterval: '5m', + useSavedObjectReferences: { + extractReferences: jest.fn(), + injectReferences: jest.fn(), + }, + })); + + const data = getMockData({ schedule: { interval: '1m' } }); + await expect(rulesClient.create({ data })).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error updating rule: the interval is less than the minimum interval of 5m"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts index 1328b666f96e7..55ffc49fd3394 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/update.test.ts @@ -140,6 +140,7 @@ describe('update()', () => { recoveryActionGroup: RecoveredActionGroup, async executor() {}, producer: 'alerts', + minimumScheduleInterval: '5s', }); }); @@ -1966,4 +1967,49 @@ describe('update()', () => { ); }); }); + + test('throws error when updating with an interval less than the minimum configured one', async () => { + await expect( + rulesClient.update({ + id: '1', + data: { + schedule: { interval: '1s' }, + name: 'abc', + tags: ['foo'], + params: { + bar: true, + }, + throttle: null, + notifyWhen: 'onActiveAlert', + actions: [ + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '1', + params: { + foo: true, + }, + }, + { + group: 'default', + id: '2', + params: { + foo: true, + }, + }, + ], + }, + }) + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error updating rule: the interval is less than the minimum interval of 5s"` + ); + expect(unsecuredSavedObjectsClient.create).not.toHaveBeenCalled(); + expect(taskManager.schedule).not.toHaveBeenCalled(); + }); }); diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index c73ce86acf785..1dc8291d28756 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -157,6 +157,8 @@ export interface AlertType< injectReferences: (params: ExtractedParams, references: SavedObjectReference[]) => Params; }; isExportable: boolean; + defaultScheduleInterval?: string; + minimumScheduleInterval?: string; ruleTaskTimeout?: string; } export type UntypedAlertType = AlertType< diff --git a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts index bed7b09110d87..c69cbcfe8ac04 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/constants/index.ts @@ -36,3 +36,5 @@ export enum SORT_ORDERS { } export const DEFAULT_SEARCH_PAGE_SIZE: number = 10; + +export const DEFAULT_ALERT_INTERVAL = '1m'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx index 2b13bdf613d96..3b15295cf7a3a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_details/components/alert_details.tsx @@ -171,6 +171,7 @@ export const AlertDetails: React.FunctionComponent = ({ }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleType={alertType} onSave={setAlert} /> )} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 514594ffb855b..4ae570a62f7d9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -69,7 +69,8 @@ describe('alert_add', () => { async function setup( initialValues?: Partial, - onClose: AlertAddProps['onClose'] = jest.fn() + onClose: AlertAddProps['onClose'] = jest.fn(), + defaultScheduleInterval?: string ) { const mocks = coreMock.createSetup(); const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); @@ -84,6 +85,7 @@ describe('alert_add', () => { }, ], defaultActionGroupId: 'testActionGroup', + defaultScheduleInterval, minimumLicenseRequired: 'basic', recoveryActionGroup: { id: 'recovered', name: 'Recovered' }, producer: ALERTS_FEATURE_ID, @@ -243,6 +245,26 @@ describe('alert_add', () => { expect(onClose).toHaveBeenCalledWith(AlertFlyoutCloseReason.SAVED); }); + + it('should enforce any default inteval', async () => { + await setup({ alertTypeId: 'my-alert-type' }, jest.fn(), '3h'); + await delay(1000); + + // Wait for handlers to fire + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + const intervalInputUnit = wrapper + .find('[data-test-subj="intervalInputUnit"]') + .first() + .getElement().props.value; + const intervalInput = wrapper.find('[data-test-subj="intervalInput"]').first().getElement() + .props.value; + expect(intervalInputUnit).toBe('h'); + expect(intervalInput).toBe(3); + }); }); function mockAlert(overloads: Partial = {}): Alert { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 5e4ca42523b39..2b376ea0d0b30 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -17,10 +17,12 @@ import { AlertFlyoutCloseReason, IErrorObject, AlertAddProps, + RuleTypeIndex, } from '../../../types'; -import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; +import { AlertForm } from './alert_form'; +import { getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_errors'; import { alertReducer, InitialAlert, InitialAlertReducer } from './alert_reducer'; -import { createAlert } from '../../lib/alert_api'; +import { createAlert, loadAlertTypes } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { ConfirmAlertSave } from './confirm_alert_save'; import { ConfirmAlertClose } from './confirm_alert_close'; @@ -30,6 +32,7 @@ import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; import { hasAlertChanged, haveAlertParamsChanged } from './has_alert_changed'; import { getAlertWithInvalidatedFields } from '../../lib/value_validators'; +import { DEFAULT_ALERT_INTERVAL } from '../../constants'; const AlertAdd = ({ consumer, @@ -39,26 +42,28 @@ const AlertAdd = ({ canChangeTrigger, alertTypeId, initialValues, + reloadAlerts, onSave, metadata, + ...props }: AlertAddProps) => { const onSaveHandler = onSave ?? reloadAlerts; - const initialAlert: InitialAlert = useMemo( - () => ({ + + const initialAlert: InitialAlert = useMemo(() => { + return { params: {}, consumer, alertTypeId, schedule: { - interval: '1m', + interval: DEFAULT_ALERT_INTERVAL, }, actions: [], tags: [], notifyWhen: 'onActionGroupChange', ...(initialValues ? initialValues : {}), - }), - [alertTypeId, consumer, initialValues] - ); + }; + }, [alertTypeId, consumer, initialValues]); const [{ alert }, dispatch] = useReducer(alertReducer as InitialAlertReducer, { alert: initialAlert, @@ -67,6 +72,10 @@ const AlertAdd = ({ const [isSaving, setIsSaving] = useState(false); const [isConfirmAlertSaveModalOpen, setIsConfirmAlertSaveModalOpen] = useState(false); const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); + const [ruleTypeIndex, setRuleTypeIndex] = useState( + props.ruleTypeIndex + ); + const [changedFromDefaultInterval, setChangedFromDefaultInterval] = useState(false); const setAlert = (value: InitialAlert) => { dispatch({ command: { type: 'setAlert' }, payload: { key: 'alert', value } }); @@ -90,6 +99,19 @@ const AlertAdd = ({ } }, [alertTypeId]); + useEffect(() => { + if (!props.ruleTypeIndex) { + (async () => { + const alertTypes = await loadAlertTypes({ http }); + const index: RuleTypeIndex = new Map(); + for (const alertType of alertTypes) { + index.set(alertType.id, alertType); + } + setRuleTypeIndex(index); + })(); + } + }, [props.ruleTypeIndex, http]); + useEffect(() => { if (isEmpty(alert.params) && !isEmpty(initialAlertParams)) { // alert params are explicitly cleared when the alert type is cleared. @@ -115,6 +137,21 @@ const AlertAdd = ({ })(); }, [alert, actionTypeRegistry]); + useEffect(() => { + if (alert.alertTypeId && ruleTypeIndex) { + const type = ruleTypeIndex.get(alert.alertTypeId); + if (type?.defaultScheduleInterval && !changedFromDefaultInterval) { + setAlertProperty('schedule', { interval: type.defaultScheduleInterval }); + } + } + }, [alert.alertTypeId, ruleTypeIndex, alert.schedule.interval, changedFromDefaultInterval]); + + useEffect(() => { + if (alert.schedule.interval !== DEFAULT_ALERT_INTERVAL && !changedFromDefaultInterval) { + setChangedFromDefaultInterval(true); + } + }, [alert.schedule.interval, changedFromDefaultInterval]); + const checkForChangesAndCloseFlyout = () => { if ( hasAlertChanged(alert, initialAlert, false) || @@ -138,9 +175,11 @@ const AlertAdd = ({ }; const alertType = alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null; + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - alertType + alertType, + alert.alertTypeId ? ruleTypeIndex?.get(alert.alertTypeId) : undefined ); // Confirm before saving if user is able to add actions but hasn't added any to this alert diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx index 1aea6c68acbf8..467f2af6ed704 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.test.tsx @@ -35,6 +35,23 @@ jest.mock('../../lib/alert_api', () => ({ })), })); +jest.mock('./alert_errors', () => ({ + getAlertActionErrors: jest.fn().mockImplementation(() => { + return []; + }), + getAlertErrors: jest.fn().mockImplementation(() => ({ + alertParamsErrors: {}, + alertBaseErrors: {}, + alertErrors: { + name: new Array(), + interval: new Array(), + alertTypeId: new Array(), + actionConnectors: new Array(), + }, + })), + isValidAlert: jest.fn(), +})); + jest.mock('../../../common/lib/health_api', () => ({ triggersActionsUiHealth: jest.fn(() => ({ isAlertsAvailable: true })), })); @@ -47,7 +64,7 @@ describe('alert_edit', () => { mockedCoreSetup = coreMock.createSetup(); }); - async function setup() { + async function setup(initialAlertFields = {}) { const [ { application: { capabilities }, @@ -154,6 +171,7 @@ describe('alert_edit', () => { status: 'unknown', lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, + ...initialAlertFields, }; actionTypeRegistry.get.mockReturnValueOnce(actionTypeModel); actionTypeRegistry.has.mockReturnValue(true); @@ -188,7 +206,11 @@ describe('alert_edit', () => { }); it('displays a toast message on save for server errors', async () => { - await setup(); + const { isValidAlert } = jest.requireMock('./alert_errors'); + (isValidAlert as jest.Mock).mockImplementation(() => { + return true; + }); + await setup({ name: undefined }); await act(async () => { wrapper.find('[data-test-subj="saveEditedAlertButton"]').first().simulate('click'); @@ -197,4 +219,12 @@ describe('alert_edit', () => { 'Fail message' ); }); + + it('should pass in the server alert type into `getAlertErrors`', async () => { + const { getAlertErrors } = jest.requireMock('./alert_errors'); + await setup(); + const lastCall = getAlertErrors.mock.calls[getAlertErrors.mock.calls.length - 1]; + expect(lastCall[2]).toBeDefined(); + expect(lastCall[2].id).toBe('my-alert-type'); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx index 8b79950d03ac9..f8c506fa1e8ba 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_edit.tsx @@ -24,10 +24,17 @@ import { } from '@elastic/eui'; import { cloneDeep } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { Alert, AlertFlyoutCloseReason, AlertEditProps, IErrorObject } from '../../../types'; -import { AlertForm, getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_form'; +import { + Alert, + AlertFlyoutCloseReason, + AlertEditProps, + IErrorObject, + AlertType, +} from '../../../types'; +import { AlertForm } from './alert_form'; +import { getAlertActionErrors, getAlertErrors, isValidAlert } from './alert_errors'; import { alertReducer, ConcreteAlertReducer } from './alert_reducer'; -import { updateAlert } from '../../lib/alert_api'; +import { updateAlert, loadAlertTypes } from '../../lib/alert_api'; import { HealthCheck } from '../../components/health_check'; import { HealthContextProvider } from '../../context/health_context'; import { useKibana } from '../../../common/lib/kibana'; @@ -43,6 +50,7 @@ export const AlertEdit = ({ ruleTypeRegistry, actionTypeRegistry, metadata, + ...props }: AlertEditProps) => { const onSaveHandler = onSave ?? reloadAlerts; const [{ alert }, dispatch] = useReducer(alertReducer as ConcreteAlertReducer, { @@ -55,6 +63,9 @@ export const AlertEdit = ({ const [isConfirmAlertCloseModalOpen, setIsConfirmAlertCloseModalOpen] = useState(false); const [alertActionsErrors, setAlertActionsErrors] = useState([]); const [isLoading, setIsLoading] = useState(false); + const [serverRuleType, setServerRuleType] = useState | undefined>( + props.ruleType + ); const { http, @@ -75,9 +86,23 @@ export const AlertEdit = ({ })(); }, [alert, actionTypeRegistry]); + useEffect(() => { + if (!props.ruleType && !serverRuleType) { + (async () => { + const serverRuleTypes = await loadAlertTypes({ http }); + for (const _serverRuleType of serverRuleTypes) { + if (alertType.id === _serverRuleType.id) { + setServerRuleType(_serverRuleType); + } + } + })(); + } + }, [props.ruleType, alertType.id, serverRuleType, http]); + const { alertBaseErrors, alertErrors, alertParamsErrors } = getAlertErrors( alert as Alert, - alertType + alertType, + serverRuleType ); const checkForChangesAndCloseFlyout = () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx new file mode 100644 index 0000000000000..5ca0fd9dafc18 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.test.tsx @@ -0,0 +1,284 @@ +/* + * 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 uuid from 'uuid'; +import React, { Fragment } from 'react'; +import { + validateBaseProperties, + getAlertErrors, + getAlertActionErrors, + hasObjectErrors, + isValidAlert, +} from './alert_errors'; +import { Alert, AlertType, AlertTypeModel } from '../../../types'; +import { actionTypeRegistryMock } from '../../action_type_registry.mock'; + +describe('alert_errors', () => { + describe('validateBaseProperties()', () => { + it('should validate the name', () => { + const alert = mockAlert(); + alert.name = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: ['Name is required.'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the interval', () => { + const alert = mockAlert(); + alert.schedule.interval = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: ['Check interval is required.'], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the minimumScheduleInterval', () => { + const alert = mockAlert(); + alert.schedule.interval = '2m'; + const result = validateBaseProperties( + alert, + mockserverRuleType({ minimumScheduleInterval: '5m' }) + ); + expect(result.errors).toStrictEqual({ + name: [], + interval: ['Interval is below minimum (5m) for this rule type'], + alertTypeId: [], + actionConnectors: [], + }); + }); + + it('should validate the alertTypeId', () => { + const alert = mockAlert(); + alert.alertTypeId = ''; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: [], + alertTypeId: ['Rule type is required.'], + actionConnectors: [], + }); + }); + + it('should validate the connectors', () => { + const alert = mockAlert(); + alert.actions = [ + { + id: '1234', + actionTypeId: 'myActionType', + group: '', + params: { + name: 'yes', + }, + }, + ]; + const result = validateBaseProperties(alert); + expect(result.errors).toStrictEqual({ + name: [], + interval: [], + alertTypeId: [], + actionConnectors: ['Action for myActionType connector is required.'], + }); + }); + }); + + describe('getAlertErrors()', () => { + it('should return all errors', () => { + const result = getAlertErrors( + mockAlert({ + name: '', + }), + mockAlertTypeModel({ + validate: () => ({ + errors: { + field: ['This is wrong'], + }, + }), + }), + mockserverRuleType() + ); + expect(result).toStrictEqual({ + alertParamsErrors: { field: ['This is wrong'] }, + alertBaseErrors: { + name: ['Name is required.'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }, + alertErrors: { + name: ['Name is required.'], + field: ['This is wrong'], + interval: [], + alertTypeId: [], + actionConnectors: [], + }, + }); + }); + }); + + describe('getAlertActionErrors()', () => { + it('should return an array of errors', async () => { + const actionTypeRegistry = actionTypeRegistryMock.create(); + actionTypeRegistry.get.mockImplementation((actionTypeId: string) => ({ + ...actionTypeRegistryMock.createMockActionTypeModel(), + validateParams: jest.fn().mockImplementation(() => ({ + errors: { + [actionTypeId]: ['Yes, this failed'], + }, + })), + })); + const result = await getAlertActionErrors( + mockAlert({ + actions: [ + { + id: '1234', + actionTypeId: 'myActionType', + group: '', + params: { + name: 'yes', + }, + }, + { + id: '5678', + actionTypeId: 'myActionType2', + group: '', + params: { + name: 'yes', + }, + }, + ], + }), + actionTypeRegistry + ); + expect(result).toStrictEqual([ + { + myActionType: ['Yes, this failed'], + }, + { + myActionType2: ['Yes, this failed'], + }, + ]); + }); + }); + + describe('hasObjectErrors()', () => { + it('should return true for any errors', () => { + expect( + hasObjectErrors({ + foo: ['1'], + }) + ).toBe(true); + expect( + hasObjectErrors({ + foo: { + foo: ['1'], + }, + }) + ).toBe(true); + }); + it('should return false for no errors', () => { + expect(hasObjectErrors({})).toBe(false); + }); + }); + + describe('isValidAlert()', () => { + it('should return true for a valid alert', () => { + const result = isValidAlert(mockAlert(), {}, []); + expect(result).toBe(true); + }); + it('should return false for an invalid alert', () => { + expect( + isValidAlert( + mockAlert(), + { + name: ['This is wrong'], + }, + [] + ) + ).toBe(false); + expect( + isValidAlert(mockAlert(), {}, [ + { + name: ['This is wrong'], + }, + ]) + ).toBe(false); + }); + }); +}); + +function mockserverRuleType( + overloads: Partial> = {} +): AlertType { + return { + actionGroups: [], + defaultActionGroupId: 'default', + minimumLicenseRequired: 'basic', + recoveryActionGroup: { + id: 'recovery', + name: 'doRecovery', + }, + id: 'myAppAlertType', + name: 'myAppAlertType', + producer: 'myApp', + authorizedConsumers: {}, + enabledInLicense: true, + actionVariables: { + context: [], + state: [], + params: [], + }, + ...overloads, + }; +} + +function mockAlertTypeModel(overloads: Partial = {}): AlertTypeModel { + return { + id: 'alertTypeModel', + description: 'some alert', + iconClass: 'something', + documentationUrl: null, + validate: () => ({ errors: {} }), + alertParamsExpression: () => , + requiresAppContext: false, + ...overloads, + }; +} + +function mockAlert(overloads: Partial = {}): Alert { + return { + id: uuid.v4(), + enabled: true, + name: `alert-${uuid.v4()}`, + tags: [], + alertTypeId: '.noop', + consumer: 'consumer', + schedule: { interval: '1m' }, + actions: [], + params: {}, + createdBy: null, + updatedBy: null, + createdAt: new Date(), + updatedAt: new Date(), + apiKeyOwner: null, + throttle: null, + notifyWhen: null, + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'unknown', + lastExecutionDate: new Date('2020-08-20T19:23:38Z'), + }, + ...overloads, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts new file mode 100644 index 0000000000000..3ca6a822b2d2d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_errors.ts @@ -0,0 +1,132 @@ +/* + * 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 { isObject } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { parseDuration } from '../../../../../alerting/common/parse_duration'; +import { + AlertTypeModel, + Alert, + IErrorObject, + AlertAction, + AlertType, + ValidationResult, + ActionTypeRegistryContract, +} from '../../../types'; +import { InitialAlert } from './alert_reducer'; + +export function validateBaseProperties( + alertObject: InitialAlert, + serverRuleType?: AlertType +): ValidationResult { + const validationResult = { errors: {} }; + const errors = { + name: new Array(), + interval: new Array(), + alertTypeId: new Array(), + actionConnectors: new Array(), + }; + validationResult.errors = errors; + if (!alertObject.name) { + errors.name.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredNameText', { + defaultMessage: 'Name is required.', + }) + ); + } + if (alertObject.schedule.interval.length < 2) { + errors.interval.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { + defaultMessage: 'Check interval is required.', + }) + ); + } else if (serverRuleType?.minimumScheduleInterval) { + const duration = parseDuration(alertObject.schedule.interval); + const minimumDuration = parseDuration(serverRuleType.minimumScheduleInterval); + if (duration < minimumDuration) { + errors.interval.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.belowMinimumText', { + defaultMessage: 'Interval is below minimum ({minimum}) for this rule type', + values: { + minimum: serverRuleType.minimumScheduleInterval, + }, + }) + ); + } + } + + if (!alertObject.alertTypeId) { + errors.alertTypeId.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText', { + defaultMessage: 'Rule type is required.', + }) + ); + } + const emptyConnectorActions = alertObject.actions.find( + (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 + ); + if (emptyConnectorActions !== undefined) { + errors.actionConnectors.push( + i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector', { + defaultMessage: 'Action for {actionTypeId} connector is required.', + values: { actionTypeId: emptyConnectorActions.actionTypeId }, + }) + ); + } + return validationResult; +} + +export function getAlertErrors( + alert: Alert, + alertTypeModel: AlertTypeModel | null, + serverRuleType?: AlertType +) { + const alertParamsErrors: IErrorObject = alertTypeModel + ? alertTypeModel.validate(alert.params).errors + : []; + const alertBaseErrors = validateBaseProperties(alert, serverRuleType).errors as IErrorObject; + const alertErrors = { + ...alertParamsErrors, + ...alertBaseErrors, + } as IErrorObject; + + return { + alertParamsErrors, + alertBaseErrors, + alertErrors, + }; +} + +export async function getAlertActionErrors( + alert: Alert, + actionTypeRegistry: ActionTypeRegistryContract +): Promise { + return await Promise.all( + alert.actions.map( + async (alertAction: AlertAction) => + ( + await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) + ).errors + ) + ); +} + +export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => + !!Object.values(errors).find((errorList) => { + if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); + return errorList.length >= 1; + }); + +export function isValidAlert( + alertObject: InitialAlert | Alert, + validationResult: IErrorObject, + actionsErrors: IErrorObject[] +): alertObject is Alert { + return ( + !hasObjectErrors(validationResult) && + actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) + ); +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx index 5facda85e6c89..3f6bf3c955e8b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_form.tsx @@ -35,7 +35,7 @@ import { EuiToolTip, EuiCallOut, } from '@elastic/eui'; -import { capitalize, isObject } from 'lodash'; +import { capitalize } from 'lodash'; import { KibanaFeature } from '../../../../../features/public'; import { getDurationNumberInItsUnit, @@ -48,9 +48,8 @@ import { Alert, IErrorObject, AlertAction, - AlertTypeIndex, + RuleTypeIndex, AlertType, - ValidationResult, RuleTypeRegistryContract, ActionTypeRegistryContract, } from '../../../types'; @@ -74,101 +73,10 @@ import { checkAlertTypeEnabled } from '../../lib/check_alert_type_enabled'; import { alertTypeCompare, alertTypeGroupCompare } from '../../lib/alert_type_compare'; import { VIEW_LICENSE_OPTIONS_LINK } from '../../../common/constants'; import { SectionLoading } from '../../components/section_loading'; +import { DEFAULT_ALERT_INTERVAL } from '../../constants'; const ENTER_KEY = 13; -export function validateBaseProperties(alertObject: InitialAlert): ValidationResult { - const validationResult = { errors: {} }; - const errors = { - name: new Array(), - interval: new Array(), - alertTypeId: new Array(), - actionConnectors: new Array(), - }; - validationResult.errors = errors; - if (!alertObject.name) { - errors.name.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredNameText', { - defaultMessage: 'Name is required.', - }) - ); - } - if (alertObject.schedule.interval.length < 2) { - errors.interval.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredIntervalText', { - defaultMessage: 'Check interval is required.', - }) - ); - } - if (!alertObject.alertTypeId) { - errors.alertTypeId.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredRuleTypeIdText', { - defaultMessage: 'Rule type is required.', - }) - ); - } - const emptyConnectorActions = alertObject.actions.find( - (actionItem) => /^\d+$/.test(actionItem.id) && Object.keys(actionItem.params).length > 0 - ); - if (emptyConnectorActions !== undefined) { - errors.actionConnectors.push( - i18n.translate('xpack.triggersActionsUI.sections.alertForm.error.requiredActionConnector', { - defaultMessage: 'Action for {actionTypeId} connector is required.', - values: { actionTypeId: emptyConnectorActions.actionTypeId }, - }) - ); - } - return validationResult; -} - -export function getAlertErrors(alert: Alert, alertTypeModel: AlertTypeModel | null) { - const alertParamsErrors: IErrorObject = alertTypeModel - ? alertTypeModel.validate(alert.params).errors - : []; - const alertBaseErrors = validateBaseProperties(alert).errors as IErrorObject; - const alertErrors = { - ...alertParamsErrors, - ...alertBaseErrors, - } as IErrorObject; - - return { - alertParamsErrors, - alertBaseErrors, - alertErrors, - }; -} - -export async function getAlertActionErrors( - alert: Alert, - actionTypeRegistry: ActionTypeRegistryContract -): Promise { - return await Promise.all( - alert.actions.map( - async (alertAction: AlertAction) => - ( - await actionTypeRegistry.get(alertAction.actionTypeId)?.validateParams(alertAction.params) - ).errors - ) - ); -} - -export const hasObjectErrors: (errors: IErrorObject) => boolean = (errors) => - !!Object.values(errors).find((errorList) => { - if (isObject(errorList)) return hasObjectErrors(errorList as IErrorObject); - return errorList.length >= 1; - }); - -export function isValidAlert( - alertObject: InitialAlert | Alert, - validationResult: IErrorObject, - actionsErrors: IErrorObject[] -): alertObject is Alert { - return ( - !hasObjectErrors(validationResult) && - actionsErrors.every((error: IErrorObject) => !hasObjectErrors(error)) - ); -} - function getProducerFeatureName(producer: string, kibanaFeatures: KibanaFeature[]) { return kibanaFeatures.find((featureItem) => featureItem.id === producer)?.name; } @@ -186,6 +94,9 @@ interface AlertFormProps> { metadata?: MetaData; } +const defaultScheduleInterval = getDurationNumberInItsUnit(DEFAULT_ALERT_INTERVAL); +const defaultScheduleIntervalUnit = getDurationUnitValue(DEFAULT_ALERT_INTERVAL); + export const AlertForm = ({ alert, canChangeTrigger = true, @@ -212,10 +123,14 @@ export const AlertForm = ({ const [alertTypeModel, setAlertTypeModel] = useState(null); const [alertInterval, setAlertInterval] = useState( - alert.schedule.interval ? getDurationNumberInItsUnit(alert.schedule.interval) : undefined + alert.schedule.interval + ? getDurationNumberInItsUnit(alert.schedule.interval) + : defaultScheduleInterval ); const [alertIntervalUnit, setAlertIntervalUnit] = useState( - alert.schedule.interval ? getDurationUnitValue(alert.schedule.interval) : 'm' + alert.schedule.interval + ? getDurationUnitValue(alert.schedule.interval) + : defaultScheduleIntervalUnit ); const [alertThrottle, setAlertThrottle] = useState( alert.throttle ? getDurationNumberInItsUnit(alert.throttle) : null @@ -224,7 +139,7 @@ export const AlertForm = ({ alert.throttle ? getDurationUnitValue(alert.throttle) : 'h' ); const [defaultActionGroupId, setDefaultActionGroupId] = useState(undefined); - const [alertTypesIndex, setAlertTypesIndex] = useState(null); + const [ruleTypeIndex, setRuleTypeIndex] = useState(null); const [availableAlertTypes, setAvailableAlertTypes] = useState< Array<{ alertTypeModel: AlertTypeModel; alertType: AlertType }> @@ -243,14 +158,14 @@ export const AlertForm = ({ (async () => { try { const alertTypesResult = await loadAlertTypes({ http }); - const index: AlertTypeIndex = new Map(); + const index: RuleTypeIndex = new Map(); for (const alertTypeItem of alertTypesResult) { index.set(alertTypeItem.id, alertTypeItem); } if (alert.alertTypeId && index.has(alert.alertTypeId)) { setDefaultActionGroupId(index.get(alert.alertTypeId)!.defaultActionGroupId); } - setAlertTypesIndex(index); + setRuleTypeIndex(index); const availableAlertTypesResult = getAvailableAlertTypes(alertTypesResult); setAvailableAlertTypes(availableAlertTypesResult); @@ -287,10 +202,24 @@ export const AlertForm = ({ useEffect(() => { setAlertTypeModel(alert.alertTypeId ? ruleTypeRegistry.get(alert.alertTypeId) : null); - if (alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId)) { - setDefaultActionGroupId(alertTypesIndex.get(alert.alertTypeId)!.defaultActionGroupId); + if (alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId)) { + setDefaultActionGroupId(ruleTypeIndex.get(alert.alertTypeId)!.defaultActionGroupId); + } + }, [alert, alert.alertTypeId, ruleTypeIndex, ruleTypeRegistry]); + + useEffect(() => { + if (alert.schedule.interval) { + const interval = getDurationNumberInItsUnit(alert.schedule.interval); + const intervalUnit = getDurationUnitValue(alert.schedule.interval); + + if (interval !== defaultScheduleInterval) { + setAlertInterval(interval); + } + if (intervalUnit !== defaultScheduleIntervalUnit) { + setAlertIntervalUnit(intervalUnit); + } } - }, [alert, alert.alertTypeId, alertTypesIndex, ruleTypeRegistry]); + }, [alert.schedule.interval]); const setAlertProperty = useCallback( (key: Key, value: Alert[Key] | null) => { @@ -372,9 +301,7 @@ export const AlertForm = ({ ? !item.alertTypeModel.requiresAppContext : item.alertType!.producer === alert.consumer ); - const selectedAlertType = alert?.alertTypeId - ? alertTypesIndex?.get(alert?.alertTypeId) - : undefined; + const selectedAlertType = alert?.alertTypeId ? ruleTypeIndex?.get(alert?.alertTypeId) : undefined; const recoveryActionGroup = selectedAlertType?.recoveryActionGroup?.id; const getDefaultActionParams = useCallback( (actionTypeId: string, actionGroupId: string): Record | undefined => @@ -499,8 +426,8 @@ export const AlertForm = ({ setActions([]); setAlertTypeModel(item.alertTypeItem); setAlertProperty('params', {}); - if (alertTypesIndex && alertTypesIndex.has(item.id)) { - setDefaultActionGroupId(alertTypesIndex.get(item.id)!.defaultActionGroupId); + if (ruleTypeIndex && ruleTypeIndex.has(item.id)) { + setDefaultActionGroupId(ruleTypeIndex.get(item.id)!.defaultActionGroupId); } }} /> @@ -518,8 +445,8 @@ export const AlertForm = ({
- {alert.alertTypeId && alertTypesIndex && alertTypesIndex.has(alert.alertTypeId) - ? alertTypesIndex.get(alert.alertTypeId)!.name + {alert.alertTypeId && ruleTypeIndex && ruleTypeIndex.has(alert.alertTypeId) + ? ruleTypeIndex.get(alert.alertTypeId)!.name : ''}
@@ -870,7 +797,7 @@ export const AlertForm = ({ ) : null} {alertTypeNodes} - ) : alertTypesIndex ? ( + ) : ruleTypeIndex ? ( ) : ( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 941d400104082..1daaf3b996126 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -33,7 +33,14 @@ import { import { useHistory } from 'react-router-dom'; import { isEmpty } from 'lodash'; -import { ActionType, Alert, AlertTableItem, AlertTypeIndex, Pagination } from '../../../../types'; +import { + ActionType, + Alert, + AlertTableItem, + AlertType, + RuleTypeIndex, + Pagination, +} from '../../../../types'; import { AlertAdd, AlertEdit } from '../../alert_form'; import { BulkOperationPopover } from '../../common/components/bulk_operation_popover'; import { AlertQuickEditButtonsWithApi as AlertQuickEditButtons } from '../../common/components/alert_quick_edit_buttons'; @@ -74,7 +81,7 @@ const ENTER_KEY = 13; interface AlertTypeState { isLoading: boolean; isInitialized: boolean; - data: AlertTypeIndex; + data: RuleTypeIndex; } interface AlertState { isLoading: boolean; @@ -161,7 +168,7 @@ export const AlertsList: React.FunctionComponent = () => { try { setAlertTypesState({ ...alertTypesState, isLoading: true }); const alertTypes = await loadAlertTypes({ http }); - const index: AlertTypeIndex = new Map(); + const index: RuleTypeIndex = new Map(); for (const alertType of alertTypes) { index.set(alertType.id, alertType); } @@ -895,6 +902,7 @@ export const AlertsList: React.FunctionComponent = () => { }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleTypeIndex={alertTypesState.data} onSave={loadAlertsData} /> )} @@ -906,6 +914,9 @@ export const AlertsList: React.FunctionComponent = () => { }} actionTypeRegistry={actionTypeRegistry} ruleTypeRegistry={ruleTypeRegistry} + ruleType={ + alertTypesState.data.get(currentRuleToEdit.alertTypeId) as AlertType + } onSave={loadAlertsData} /> )} @@ -944,17 +955,17 @@ function filterAlertsById(alerts: Alert[], ids: string[]): Alert[] { function convertAlertsToTableItems( alerts: Alert[], - alertTypesIndex: AlertTypeIndex, + ruleTypeIndex: RuleTypeIndex, canExecuteActions: boolean ) { return alerts.map((alert) => ({ ...alert, actionsCount: alert.actions.length, tagsText: alert.tags.join(', '), - alertType: alertTypesIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, + alertType: ruleTypeIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, isEditable: - hasAllPrivilege(alert, alertTypesIndex.get(alert.alertTypeId)) && + hasAllPrivilege(alert, ruleTypeIndex.get(alert.alertTypeId)) && (canExecuteActions || (!canExecuteActions && !alert.actions.length)), - enabledInLicense: !!alertTypesIndex.get(alert.alertTypeId)?.enabledInLicense, + enabledInLicense: !!ruleTypeIndex.get(alert.alertTypeId)?.enabledInLicense, })); } diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index ae4fd5152794f..2ef20f36b7ca9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -66,7 +66,7 @@ export { }; export type ActionTypeIndex = Record; -export type AlertTypeIndex = Map; +export type RuleTypeIndex = Map; export type ActionTypeRegistryContract< ActionConnector = unknown, ActionParams = unknown @@ -197,6 +197,8 @@ export interface AlertType< | 'minimumLicenseRequired' | 'recoveryActionGroup' | 'defaultActionGroupId' + | 'defaultScheduleInterval' + | 'minimumScheduleInterval' > { actionVariables: ActionVariables; authorizedConsumers: Record; @@ -285,6 +287,7 @@ export interface AlertEditProps> { reloadAlerts?: () => Promise; onSave?: () => Promise; metadata?: MetaData; + ruleType?: AlertType; } export interface AlertAddProps> { @@ -299,4 +302,5 @@ export interface AlertAddProps> { reloadAlerts?: () => Promise; onSave?: () => Promise; metadata?: MetaData; + ruleTypeIndex?: RuleTypeIndex; } From a3895208498e835a692fee918191b4b20802bb59 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 11 Oct 2021 20:27:46 +0200 Subject: [PATCH 028/287] [Utpime] Remove unnecessary usememo in url params hooks (#114252) --- x-pack/plugins/uptime/public/hooks/use_url_params.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts index 329e0ccef4d96..1318b635693c7 100644 --- a/x-pack/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/plugins/uptime/public/hooks/use_url_params.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; @@ -28,7 +28,7 @@ const getParsedParams = (search: string) => { export const useGetUrlParams: GetUrlParams = () => { const { search } = useLocation(); - return useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]); + return getSupportedUrlParams(getParsedParams(search)); }; const getMapFromFilters = (value: any): Map | undefined => { From e8d16cddca6003383c972a01229f9a284799a2ed Mon Sep 17 00:00:00 2001 From: Jonathan Budzenski Date: Mon, 11 Oct 2021 13:30:33 -0500 Subject: [PATCH 029/287] skip flaky suite. #114541, #114542 --- x-pack/test/accessibility/apps/index_lifecycle_management.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/test/accessibility/apps/index_lifecycle_management.ts b/x-pack/test/accessibility/apps/index_lifecycle_management.ts index da56cfd702abf..65faa77fc497b 100644 --- a/x-pack/test/accessibility/apps/index_lifecycle_management.ts +++ b/x-pack/test/accessibility/apps/index_lifecycle_management.ts @@ -57,7 +57,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { throw new Error(`Could not find ${policyName} in policy table`); }; - describe('Index Lifecycle Management', async () => { + // FLAKY + // https://github.com/elastic/kibana/issues/114541 + // https://github.com/elastic/kibana/issues/114542 + describe.skip('Index Lifecycle Management', async () => { before(async () => { await esClient.ilm.putLifecycle({ policy: POLICY_NAME, body: POLICY_ALL_PHASES }); await esClient.indices.putIndexTemplate({ From 328da6f720f6988cd307bccdd4e4be5a0f783ba8 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Mon, 11 Oct 2021 16:18:10 -0400 Subject: [PATCH 030/287] add default sort props (#114294) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../plugins/monitoring/public/application/hooks/use_table.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts index 45d1f717f5d49..be1983c373c27 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_table.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_table.ts @@ -9,6 +9,7 @@ import { useState, useCallback } from 'react'; import { EuiTableSortingType } from '@elastic/eui'; import { euiTableStorageGetter, euiTableStorageSetter } from '../../components/table'; import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; +import { EUI_SORT_ASCENDING } from '../../../common/constants'; interface Pagination { pageSize: number; @@ -77,7 +78,9 @@ export function useTable(storageKey: string) { ); // get initial state from localStorage - const [sorting, setSorting] = useState(storageData.sort || { sort: {} }); + const [sorting, setSorting] = useState( + storageData.sort || { sort: { field: 'name', direction: EUI_SORT_ASCENDING } } + ); const [query, setQuery] = useState(''); From de42e5329f8b76bcc7bb07e0fceb4739a74fe7ea Mon Sep 17 00:00:00 2001 From: Sandra G Date: Mon, 11 Oct 2021 16:18:29 -0400 Subject: [PATCH 031/287] [Stack Monitoring] add test subject to tab (#114319) * pass test subject to advanced link and change name in angular version to be generic * change name in functional test Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/pages/elasticsearch/item_template.tsx | 1 + x-pack/plugins/monitoring/public/directives/main/index.html | 2 +- .../functional/services/monitoring/elasticsearch_node_detail.js | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx index 1f06ba18bf102..05d567a9b3ff7 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/item_template.tsx @@ -25,6 +25,7 @@ export const ItemTemplate: React.FC = (props) => { }, { id: 'advanced', + testSubj: 'esItemDetailAdvancedLink', label: i18n.translate('xpack.monitoring.esItemNavigation.advancedLinkText', { defaultMessage: 'Advanced', }), diff --git a/x-pack/plugins/monitoring/public/directives/main/index.html b/x-pack/plugins/monitoring/public/directives/main/index.html index fb24d9e678d56..558ed5e874cd6 100644 --- a/x-pack/plugins/monitoring/public/directives/main/index.html +++ b/x-pack/plugins/monitoring/public/directives/main/index.html @@ -103,7 +103,7 @@

{{pageTitle || monitoringMain.instance}} Date: Mon, 11 Oct 2021 20:30:54 +0000 Subject: [PATCH 032/287] auto-upgrade polyfills --- renovate.json5 | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/renovate.json5 b/renovate.json5 index dea7d311bae16..b08d7e0bcec1e 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -75,6 +75,16 @@ labels: ['Team:Operations', 'release_note:skip'], enabled: true, }, + { + groupName: 'polyfills', + packageNames: ['core-js'], + matchPackagePatterns: ["polyfill"], + excludePackageNames: ['@loaders.gl/polyfills'], + reviewers: ['team:kibana-operations'], + matchBaseBranches: ['master'], + labels: ['Team:Operations', 'release_note:skip'], + enabled: true, + }, { groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], From a03aa7b7faa0bef0ef4d3c0043c7cdf7f8e3890f Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 11 Oct 2021 13:44:50 +0100 Subject: [PATCH 033/287] skip flaky suites (#114418) --- .../functional/apps/maps/documents_source/docvalue_fields.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index 3479f292374d2..64c6c2b6749e6 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -11,7 +11,8 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const security = getService('security'); - describe('docvalue_fields', () => { + // FLAKY: https://github.com/elastic/kibana/issues/114418 + describe.skip('docvalue_fields', () => { before(async () => { await security.testUser.setRoles(['global_maps_read', 'test_logstash_reader'], false); await PageObjects.maps.loadSavedMap('document example'); From f88901c6ac37440beff1629685055e0bca64221b Mon Sep 17 00:00:00 2001 From: Rudolf Meijering Date: Mon, 11 Oct 2021 23:50:35 +0200 Subject: [PATCH 034/287] Log Unhandled rejection stack (#113614) * Refactor state types * Log the stack trace of unhandled promise rejections * Revert "Refactor state types" This reverts commit 6a5123a1b2d2b40089c94109619c471b3c33acf2. * Fix types * code review feedback --- .../environment/environment_service.test.ts | 26 +++++++++++++++++++ .../server/environment/environment_service.ts | 5 ++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index 34647d090b995..4b074482248b4 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -135,6 +135,32 @@ describe('UuidService', () => { expect(logger.get('process').warn).not.toHaveBeenCalled(); }); }); + + describe('unhandledRejection warnings', () => { + it('logs warn for an unhandeld promise rejected with an Error', async () => { + await service.preboot(); + + const err = new Error('something went wrong'); + process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); + + expect(logger.get('process').warn).toHaveBeenCalledTimes(1); + expect(loggingSystemMock.collect(logger).warn[0][0]).toMatch( + /Detected an unhandled Promise rejection: Error: something went wrong\n.*at / + ); + }); + + it('logs warn for an unhandeld promise rejected with a string', async () => { + await service.preboot(); + + const err = 'something went wrong'; + process.emit('unhandledRejection', err, new Promise((res, rej) => rej(err))); + + expect(logger.get('process').warn).toHaveBeenCalledTimes(1); + expect(loggingSystemMock.collect(logger).warn[0][0]).toMatch( + /Detected an unhandled Promise rejection: "something went wrong"/ + ); + }); + }); }); describe('#setup()', () => { diff --git a/src/core/server/environment/environment_service.ts b/src/core/server/environment/environment_service.ts index f96b616256577..472883c1f482c 100644 --- a/src/core/server/environment/environment_service.ts +++ b/src/core/server/environment/environment_service.ts @@ -54,9 +54,10 @@ export class EnvironmentService { this.configService.atPath(pidConfigDef.path).pipe(take(1)).toPromise(), ]); - // was present in the legacy `pid` file. + // Log unhandled rejections so that we can fix them in preparation for https://github.com/elastic/kibana/issues/77469 process.on('unhandledRejection', (reason) => { - this.log.warn(`Detected an unhandled Promise rejection.\n${reason}`); + const message = (reason as Error)?.stack ?? JSON.stringify(reason); + this.log.warn(`Detected an unhandled Promise rejection: ${message}`); }); process.on('warning', (warning) => { From c6b5136c3a5a95c9027e64c01a03ee52f30d264d Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Mon, 11 Oct 2021 16:26:52 -0600 Subject: [PATCH 035/287] =?UTF-8?q?[Maps]=20fix=20docvalue=5Ffields=C2=B7j?= =?UTF-8?q?s=20functional=20test=20(#114527)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [Maps] fix docvalue_fields·js functional test * remove unused * eslint * remove _type from check list, copied from 7.x branch and is not in 8.0 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../apps/maps/documents_source/docvalue_fields.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index 64c6c2b6749e6..d1da193ac50a5 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -15,7 +15,6 @@ export default function ({ getPageObjects, getService }) { describe.skip('docvalue_fields', () => { before(async () => { await security.testUser.setRoles(['global_maps_read', 'test_logstash_reader'], false); - await PageObjects.maps.loadSavedMap('document example'); }); after(async () => { @@ -46,11 +45,14 @@ export default function ({ getPageObjects, getService }) { it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { await PageObjects.maps.loadSavedMap('document example with data driven styles on date field'); const { rawResponse: response } = await PageObjects.maps.getResponse(); - const firstHit = response.hits.hits[0]; - expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['@timestamp', 'bytes', 'geo.coordinates']); - expect(firstHit.fields['@timestamp']).to.be.an('array'); - expect(firstHit.fields['@timestamp'][0]).to.eql('1442709321445'); + const targetHit = response.hits.hits.find((hit) => { + return hit._id === 'AU_x3_g4GFA8no6QjkSR'; + }); + expect(targetHit).not.to.be(undefined); + expect(targetHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); + expect(targetHit.fields).to.only.have.keys(['@timestamp', 'bytes', 'geo.coordinates']); + expect(targetHit.fields['@timestamp']).to.be.an('array'); + expect(targetHit.fields['@timestamp'][0]).to.eql('1442709321445'); }); }); } From a0b55b316e23a80de5d755864c7acd656b44441f Mon Sep 17 00:00:00 2001 From: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Date: Mon, 11 Oct 2021 18:29:50 -0400 Subject: [PATCH 036/287] [Core][Deprecations] omit deprecationDetails if needed it in the reques (#114399) * omit deprecationDetails if needed it * review I * doc update Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- ...r.deprecationsdetails.correctiveactions.md | 1 + ...-plugin-core-server.deprecationsdetails.md | 2 +- .../deprecations/deprecations_client.test.ts | 33 +++++++++++++++++++ .../deprecations/deprecations_client.ts | 4 +-- src/core/server/deprecations/README.mdx | 2 +- src/core/server/deprecations/types.ts | 2 ++ src/core/server/server.api.md | 1 + 7 files changed, 41 insertions(+), 4 deletions(-) diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md index 657c62a21c581..d7d10651033bf 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md @@ -16,6 +16,7 @@ correctiveActions: { body?: { [key: string]: any; }; + omitContextFromBody?: boolean; }; manualSteps: string[]; }; diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md index 86418a1d0c1c3..2ff9f4b792f5d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -15,7 +15,7 @@ export interface DeprecationsDetails | Property | Type | Description | | --- | --- | --- | -| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps: string[];
} | corrective action needed to fix this deprecation. | +| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
omitContextFromBody?: boolean;
};
manualSteps: string[];
} | corrective action needed to fix this deprecation. | | [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | | [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | (optional) link to the documentation for more details on the deprecation. | | [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | diff --git a/src/core/public/deprecations/deprecations_client.test.ts b/src/core/public/deprecations/deprecations_client.test.ts index cca81f4687a97..3c45b76cfecd2 100644 --- a/src/core/public/deprecations/deprecations_client.test.ts +++ b/src/core/public/deprecations/deprecations_client.test.ts @@ -197,5 +197,38 @@ describe('DeprecationsClient', () => { expect(result).toEqual({ status: 'fail', reason: mockResponse }); }); + + it('omit deprecationDetails in the request of the body', async () => { + const deprecationsClient = new DeprecationsClient({ http }); + const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', + domainId: 'testPluginId-1', + message: 'some-message', + level: 'warning', + correctiveActions: { + api: { + path: 'some-path', + method: 'POST', + body: { + extra_param: 123, + }, + omitContextFromBody: true, + }, + manualSteps: ['manual-step'], + }, + }; + const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); + + expect(http.fetch).toBeCalledTimes(1); + expect(http.fetch).toBeCalledWith({ + path: 'some-path', + method: 'POST', + asSystemRequest: true, + body: JSON.stringify({ + extra_param: 123, + }), + }); + expect(result).toEqual({ status: 'ok' }); + }); }); }); diff --git a/src/core/public/deprecations/deprecations_client.ts b/src/core/public/deprecations/deprecations_client.ts index 4b9cfca1986ba..01906c930d730 100644 --- a/src/core/public/deprecations/deprecations_client.ts +++ b/src/core/public/deprecations/deprecations_client.ts @@ -59,7 +59,7 @@ export class DeprecationsClient { }; } - const { body, method, path } = correctiveActions.api; + const { body, method, path, omitContextFromBody = false } = correctiveActions.api; try { await this.http.fetch({ path, @@ -67,7 +67,7 @@ export class DeprecationsClient { asSystemRequest: true, body: JSON.stringify({ ...body, - deprecationDetails: { domainId }, + ...(omitContextFromBody ? {} : { deprecationDetails: { domainId } }), }), }); return { status: 'ok' }; diff --git a/src/core/server/deprecations/README.mdx b/src/core/server/deprecations/README.mdx index 82a01995502e2..ed542610e753f 100644 --- a/src/core/server/deprecations/README.mdx +++ b/src/core/server/deprecations/README.mdx @@ -212,7 +212,7 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations The deprecations API allows plugins to provide an API call that can be used to automatically fix specific deprecations. To do so create a `PUT` or `POST` route in your plugin and specify data you want to be passed in the payload for the deprecation. -In the example above, `/internal/security/users/test_dashboard_user` will be called when users click on `Quick Resolve` in the UA. The service will automatically pass the body provided in the api corrective action to provide context to the route for fixing the deprecation. +In the example above, `/internal/security/users/test_dashboard_user` will be called when users click on `Quick Resolve` in the UA. The service will automatically pass the body provided in the api corrective action to provide context to the route for fixing the deprecation. If you need to omit the deprecation details context in the request of the body, you can use the property `omitContextFromBody`. The deprecations service expects a `200` status code to recognize the corrective action as a success. diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts index 7e276514a64d3..e24c6a13fceea 100644 --- a/src/core/server/deprecations/types.ts +++ b/src/core/server/deprecations/types.ts @@ -69,6 +69,8 @@ export interface DeprecationsDetails { body?: { [key: string]: any; }; + /* Allow to omit context in the request of the body */ + omitContextFromBody?: boolean; }; /** * Specify a list of manual steps users need to follow to diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index c92f767ce891d..a1fd69f5e1c7e 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -812,6 +812,7 @@ export interface DeprecationsDetails { body?: { [key: string]: any; }; + omitContextFromBody?: boolean; }; manualSteps: string[]; }; From 912eb0d9373ef17c27c2a1308ff8151dd8382e5a Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 11 Oct 2021 18:29:10 -0500 Subject: [PATCH 037/287] [ftr] ensure indentation is reset between configs (#114359) Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../src/tooling_log/tooling_log.test.ts | 28 ++++++++++ .../src/tooling_log/tooling_log.ts | 37 +++++++++++-- packages/kbn-pm/dist/index.js | 40 ++++++++++++-- .../src/functional_test_runner/cli.ts | 2 +- .../kbn-test/src/functional_tests/tasks.ts | 54 +++++++++---------- 5 files changed, 121 insertions(+), 40 deletions(-) diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts index 2a1099587d0c4..ec63a9fb7e6f2 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.test.ts @@ -12,6 +12,10 @@ import { toArray, takeUntil } from 'rxjs/operators'; import { ToolingLog } from './tooling_log'; import { Writer } from './writer'; import { ToolingLogTextWriter } from './tooling_log_text_writer'; +import { ToolingLogCollectingWriter } from './tooling_log_collecting_writer'; +import { createStripAnsiSerializer } from '../serializers/strip_ansi_serializer'; + +expect.addSnapshotSerializer(createStripAnsiSerializer()); it('creates zero writers without a config', () => { const log = new ToolingLog(); @@ -67,6 +71,30 @@ describe('#indent()', () => { expect(write.mock.calls).toMatchSnapshot(); }); + + it('resets the indentation after block executes and promise resolves', async () => { + const log = new ToolingLog(); + const writer = new ToolingLogCollectingWriter(); + log.setWriters([writer]); + + log.info('base'); + await log.indent(2, async () => { + log.indent(2); + log.info('hello'); + log.indent(2); + log.info('world'); + }); + log.info('back to base'); + + expect(writer.messages).toMatchInlineSnapshot(` + Array [ + " info base", + " │ info hello", + " │ info world", + " info back to base", + ] + `); + }); }); (['verbose', 'debug', 'info', 'success', 'warning', 'error', 'write'] as const).forEach( diff --git a/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts b/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts index d60bf9bcb1d70..e9fd15afefe4e 100644 --- a/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts +++ b/packages/kbn-dev-utils/src/tooling_log/tooling_log.ts @@ -13,7 +13,7 @@ import { Writer } from './writer'; import { Message, MessageTypes } from './message'; export class ToolingLog { - private identWidth = 0; + private indentWidth = 0; private writers: Writer[]; private readonly written$: Rx.Subject; @@ -22,9 +22,36 @@ export class ToolingLog { this.written$ = new Rx.Subject(); } - public indent(delta = 0) { - this.identWidth = Math.max(this.identWidth + delta, 0); - return this.identWidth; + /** + * Get the current indentation level of the ToolingLog + */ + public getIndent() { + return this.indentWidth; + } + + /** + * Indent the output of the ToolingLog by some character (4 is a good choice usually). + * + * If provided, the `block` function will be executed and once it's promise is resolved + * or rejected the indentation will be reset to its original state. + * + * @param delta the number of spaces to increase/decrease the indentation + * @param block a function to run and reset any indentation changes after + */ + public indent(delta = 0, block?: () => Promise) { + const originalWidth = this.indentWidth; + this.indentWidth = Math.max(this.indentWidth + delta, 0); + if (!block) { + return; + } + + return (async () => { + try { + return await block(); + } finally { + this.indentWidth = originalWidth; + } + })(); } public verbose(...args: any[]) { @@ -70,7 +97,7 @@ export class ToolingLog { private sendToWriters(type: MessageTypes, args: any[]) { const msg = { type, - indent: this.identWidth, + indent: this.indentWidth, args, }; diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index f395636379141..a231fe21ea838 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -626,16 +626,46 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && */ class ToolingLog { constructor(writerConfig) { - (0, _defineProperty2.default)(this, "identWidth", 0); + (0, _defineProperty2.default)(this, "indentWidth", 0); (0, _defineProperty2.default)(this, "writers", void 0); (0, _defineProperty2.default)(this, "written$", void 0); this.writers = writerConfig ? [new _tooling_log_text_writer.ToolingLogTextWriter(writerConfig)] : []; this.written$ = new Rx.Subject(); } + /** + * Get the current indentation level of the ToolingLog + */ + + + getIndent() { + return this.indentWidth; + } + /** + * Indent the output of the ToolingLog by some character (4 is a good choice usually). + * + * If provided, the `block` function will be executed and once it's promise is resolved + * or rejected the indentation will be reset to its original state. + * + * @param delta the number of spaces to increase/decrease the indentation + * @param block a function to run and reset any indentation changes after + */ - indent(delta = 0) { - this.identWidth = Math.max(this.identWidth + delta, 0); - return this.identWidth; + + indent(delta = 0, block) { + const originalWidth = this.indentWidth; + this.indentWidth = Math.max(this.indentWidth + delta, 0); + + if (!block) { + return; + } + + return (async () => { + try { + return await block(); + } finally { + this.indentWidth = originalWidth; + } + })(); } verbose(...args) { @@ -681,7 +711,7 @@ class ToolingLog { sendToWriters(type, args) { const msg = { type, - indent: this.identWidth, + indent: this.indentWidth, args }; let written = false; diff --git a/packages/kbn-test/src/functional_test_runner/cli.ts b/packages/kbn-test/src/functional_test_runner/cli.ts index 3ad365a028b65..d9938bebea5bb 100644 --- a/packages/kbn-test/src/functional_test_runner/cli.ts +++ b/packages/kbn-test/src/functional_test_runner/cli.ts @@ -79,7 +79,7 @@ export function runFtrCli() { err: err.message, ...flags, }); - log.indent(-log.indent()); + log.indent(-log.getIndent()); log.error(err); process.exitCode = 1; } else { diff --git a/packages/kbn-test/src/functional_tests/tasks.ts b/packages/kbn-test/src/functional_tests/tasks.ts index 3bc697c143f40..c8265c032cbcc 100644 --- a/packages/kbn-test/src/functional_tests/tasks.ts +++ b/packages/kbn-test/src/functional_tests/tasks.ts @@ -78,12 +78,9 @@ export async function runTests(options: RunTestsParams) { log.write('--- asserting that all tests belong to a ciGroup'); for (const configPath of options.configs) { log.info('loading', configPath); - log.indent(4); - try { + await log.indent(4, async () => { await assertNoneExcluded({ configPath, options: { ...options, log } }); - } finally { - log.indent(-4); - } + }); continue; } @@ -94,42 +91,41 @@ export async function runTests(options: RunTestsParams) { const configPathsWithTests: string[] = []; for (const configPath of options.configs) { log.info('testing', configPath); - log.indent(4); - try { + await log.indent(4, async () => { if (await hasTests({ configPath, options: { ...options, log } })) { configPathsWithTests.push(configPath); } - } finally { - log.indent(-4); - } + }); } for (const configPath of configPathsWithTests) { - log.write(`--- Running ${relative(REPO_ROOT, configPath)}`); + await log.indent(0, async () => { + log.write(`--- Running ${relative(REPO_ROOT, configPath)}`); - await withProcRunner(log, async (procs) => { - const config = await readConfigFile(log, configPath); + await withProcRunner(log, async (procs) => { + const config = await readConfigFile(log, configPath); - let es; - try { - es = await runElasticsearch({ config, options: { ...options, log } }); - await runKibanaServer({ procs, config, options }); - await runFtr({ configPath, options: { ...options, log } }); - } finally { + let es; try { - const delay = config.get('kbnTestServer.delayShutdown'); - if (typeof delay === 'number') { - log.info('Delaying shutdown of Kibana for', delay, 'ms'); - await new Promise((r) => setTimeout(r, delay)); - } - - await procs.stop('kibana'); + es = await runElasticsearch({ config, options: { ...options, log } }); + await runKibanaServer({ procs, config, options }); + await runFtr({ configPath, options: { ...options, log } }); } finally { - if (es) { - await es.cleanup(); + try { + const delay = config.get('kbnTestServer.delayShutdown'); + if (typeof delay === 'number') { + log.info('Delaying shutdown of Kibana for', delay, 'ms'); + await new Promise((r) => setTimeout(r, delay)); + } + + await procs.stop('kibana'); + } finally { + if (es) { + await es.cleanup(); + } } } - } + }); }); } } From 8e72e17648dbfd4d6c3af527edc0daea9e6c6b36 Mon Sep 17 00:00:00 2001 From: Stacey Gammon Date: Mon, 11 Oct 2021 19:46:01 -0400 Subject: [PATCH 038/287] api docs (#114565) --- api_docs/apm.json | 290 +++--- api_docs/apm.mdx | 2 +- api_docs/charts.json | 945 +++++++++++++++++- api_docs/charts.mdx | 8 +- api_docs/core.json | 262 ++++- api_docs/core.mdx | 2 +- api_docs/core_application.json | 2 +- api_docs/core_application.mdx | 2 +- api_docs/core_chrome.mdx | 2 +- api_docs/core_http.json | 2 +- api_docs/core_http.mdx | 2 +- api_docs/core_saved_objects.json | 8 +- api_docs/core_saved_objects.mdx | 2 +- api_docs/dashboard.json | 6 +- api_docs/dashboard_enhanced.json | 10 +- api_docs/data.json | 735 ++++++++------ api_docs/data.mdx | 2 +- api_docs/data_autocomplete.mdx | 2 +- api_docs/data_query.mdx | 2 +- api_docs/data_search.json | 10 +- api_docs/data_search.mdx | 2 +- api_docs/data_ui.mdx | 2 +- api_docs/data_views.json | 345 +++++-- api_docs/data_views.mdx | 2 +- api_docs/deprecations_by_api.mdx | 4 +- api_docs/deprecations_by_plugin.mdx | 28 +- api_docs/elastic_apm_generator.json | 257 +++++ api_docs/elastic_apm_generator.mdx | 27 + api_docs/event_log.json | 22 +- api_docs/expression_metric_vis.json | 837 ++++++++++++++++ api_docs/expression_metric_vis.mdx | 33 + api_docs/fleet.json | 816 +++++++++++++-- api_docs/fleet.mdx | 2 +- api_docs/interactive_setup.json | 98 ++ api_docs/interactive_setup.mdx | 2 +- api_docs/kbn_config.json | 212 +++- api_docs/kbn_config.mdx | 2 +- api_docs/kbn_es_query.json | 370 ++++--- api_docs/kbn_es_query.mdx | 2 +- api_docs/kbn_logging.json | 2 +- api_docs/kbn_monaco.json | 166 ++- api_docs/kbn_monaco.mdx | 2 +- api_docs/kbn_optimizer.json | 55 + api_docs/kbn_optimizer.mdx | 2 +- api_docs/kbn_securitysolution_list_utils.json | 4 +- api_docs/kbn_typed_react_router_config.json | 125 ++- api_docs/kbn_typed_react_router_config.mdx | 2 +- api_docs/kibana_utils.json | 48 +- api_docs/kibana_utils.mdx | 2 +- api_docs/lens.json | 13 + api_docs/lens.mdx | 2 +- api_docs/maps.json | 4 +- api_docs/observability.json | 238 ++++- api_docs/observability.mdx | 2 +- api_docs/plugin_directory.mdx | 47 +- api_docs/saved_objects.json | 8 - api_docs/security_solution.json | 269 +++-- api_docs/security_solution.mdx | 2 +- api_docs/share.json | 111 +- api_docs/share.mdx | 2 +- api_docs/spaces.json | 91 +- api_docs/spaces.mdx | 2 +- api_docs/timelines.json | 106 +- api_docs/transform.json | 101 ++ api_docs/transform.mdx | 37 + api_docs/vis_default_editor.json | 8 +- api_docs/vis_type_vislib.json | 4 +- api_docs/vis_type_xy.json | 8 +- api_docs/visualizations.json | 500 ++++++++- api_docs/visualizations.mdx | 2 +- .../tests/snapshots/plugin_a.foo.json | 76 -- .../api_docs/tests/snapshots/plugin_a.foo.mdx | 35 - 72 files changed, 6206 insertions(+), 1229 deletions(-) create mode 100644 api_docs/elastic_apm_generator.json create mode 100644 api_docs/elastic_apm_generator.mdx create mode 100644 api_docs/expression_metric_vis.json create mode 100644 api_docs/expression_metric_vis.mdx create mode 100644 api_docs/transform.json create mode 100644 api_docs/transform.mdx delete mode 100644 packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.json delete mode 100644 packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.mdx diff --git a/api_docs/apm.json b/api_docs/apm.json index c2300232f9b8f..8b43370097091 100644 --- a/api_docs/apm.json +++ b/api_docs/apm.json @@ -184,7 +184,7 @@ "APMPluginStartDependencies", ", unknown>, plugins: Pick<", "APMPluginSetupDependencies", - ", \"data\" | \"cloud\" | \"security\" | \"home\" | \"features\" | \"fleet\" | \"ml\" | \"actions\" | \"usageCollection\" | \"apmOss\" | \"licensing\" | \"observability\" | \"ruleRegistry\" | \"spaces\" | \"taskManager\" | \"alerting\">) => { config$: ", + ", \"data\" | \"cloud\" | \"security\" | \"home\" | \"features\" | \"fleet\" | \"ml\" | \"actions\" | \"usageCollection\" | \"spaces\" | \"apmOss\" | \"licensing\" | \"observability\" | \"ruleRegistry\" | \"taskManager\" | \"alerting\">) => { config$: ", "Observable", "<{ 'apm_oss.transactionIndices': string; 'apm_oss.spanIndices': string; 'apm_oss.errorIndices': string; 'apm_oss.metricsIndices': string; 'apm_oss.sourcemapIndices': string; 'apm_oss.onboardingIndices': string; 'xpack.apm.serviceMapEnabled': boolean; 'xpack.apm.serviceMapFingerprintBucketSize': number; 'xpack.apm.serviceMapTraceIdBucketSize': number; 'xpack.apm.serviceMapFingerprintGlobalBucketSize': number; 'xpack.apm.serviceMapTraceIdGlobalBucketSize': number; 'xpack.apm.serviceMapMaxTracesPerRequest': number; 'xpack.apm.ui.enabled': boolean; 'xpack.apm.ui.maxTraceItems': number; 'xpack.apm.ui.transactionGroupBucketSize': number; 'xpack.apm.autocreateApmIndexPattern': boolean; 'xpack.apm.telemetryCollectionEnabled': boolean; 'xpack.apm.searchAggregatedTransactions': ", "SearchAggregatedTransactionSetting", @@ -248,7 +248,7 @@ "signature": [ "Pick<", "APMPluginSetupDependencies", - ", \"data\" | \"cloud\" | \"security\" | \"home\" | \"features\" | \"fleet\" | \"ml\" | \"actions\" | \"usageCollection\" | \"apmOss\" | \"licensing\" | \"observability\" | \"ruleRegistry\" | \"spaces\" | \"taskManager\" | \"alerting\">" + ", \"data\" | \"cloud\" | \"security\" | \"home\" | \"features\" | \"fleet\" | \"ml\" | \"actions\" | \"usageCollection\" | \"spaces\" | \"apmOss\" | \"licensing\" | \"observability\" | \"ruleRegistry\" | \"taskManager\" | \"alerting\">" ], "path": "x-pack/plugins/apm/server/plugin.ts", "deprecated": false, @@ -797,7 +797,7 @@ "label": "APIEndpoint", "description": [], "signature": [ - "\"POST /api/apm/index_pattern/static\" | \"GET /api/apm/index_pattern/dynamic\" | \"GET /api/apm/environments\" | \"GET /api/apm/services/{serviceName}/errors\" | \"GET /api/apm/services/{serviceName}/errors/{groupId}\" | \"GET /api/apm/services/{serviceName}/errors/distribution\" | \"GET /api/apm/services/{serviceName}/metrics/charts\" | \"GET /api/apm/observability_overview\" | \"GET /api/apm/observability_overview/has_data\" | \"GET /api/apm/rum/client-metrics\" | \"GET /api/apm/rum-client/page-load-distribution\" | \"GET /api/apm/rum-client/page-load-distribution/breakdown\" | \"GET /api/apm/rum-client/page-view-trends\" | \"GET /api/apm/rum-client/services\" | \"GET /api/apm/rum-client/visitor-breakdown\" | \"GET /api/apm/rum-client/web-core-vitals\" | \"GET /api/apm/rum-client/long-task-metrics\" | \"GET /api/apm/rum-client/url-search\" | \"GET /api/apm/rum-client/js-errors\" | \"GET /api/apm/observability_overview/has_rum_data\" | \"GET /api/apm/service-map\" | \"GET /api/apm/service-map/service/{serviceName}\" | \"GET /api/apm/service-map/backend/{backendName}\" | \"GET /api/apm/services/{serviceName}/serviceNodes\" | \"GET /api/apm/services\" | \"GET /api/apm/services/detailed_statistics\" | \"GET /api/apm/services/{serviceName}/metadata/details\" | \"GET /api/apm/services/{serviceName}/metadata/icons\" | \"GET /api/apm/services/{serviceName}/agent\" | \"GET /api/apm/services/{serviceName}/transaction_types\" | \"GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /api/apm/services/{serviceName}/error_groups/main_statistics\" | \"GET /api/apm/services/{serviceName}/error_groups/detailed_statistics\" | \"GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /api/apm/services/{serviceName}/throughput\" | \"GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /api/apm/services/{serviceName}/dependencies\" | \"GET /api/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /api/apm/services/{serviceName}/profiling/timeline\" | \"GET /api/apm/services/{serviceName}/profiling/statistics\" | \"GET /api/apm/services/{serviceName}/alerts\" | \"GET /api/apm/services/{serviceName}/infrastructure\" | \"GET /internal/apm/suggestions\" | \"GET /api/apm/traces/{traceId}\" | \"GET /api/apm/traces\" | \"GET /api/apm/traces/{traceId}/root_transaction\" | \"GET /api/apm/transactions/{transactionId}\" | \"GET /api/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /api/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /api/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /api/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /api/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /api/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /api/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /api/apm/alerts/chart_preview/transaction_duration\" | \"GET /api/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/services\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /api/apm/settings/anomaly-detection/jobs\" | \"POST /api/apm/settings/anomaly-detection/jobs\" | \"GET /api/apm/settings/anomaly-detection/environments\" | \"GET /api/apm/settings/apm-index-settings\" | \"GET /api/apm/settings/apm-indices\" | \"POST /api/apm/settings/apm-indices/save\" | \"GET /api/apm/settings/custom_links/transaction\" | \"GET /api/apm/settings/custom_links\" | \"POST /api/apm/settings/custom_links\" | \"PUT /api/apm/settings/custom_links/{id}\" | \"DELETE /api/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /api/apm/fleet/has_data\" | \"GET /api/apm/fleet/agents\" | \"POST /api/apm/fleet/apm_server_schema\" | \"GET /api/apm/fleet/apm_server_schema/unsupported\" | \"GET /api/apm/fleet/migration_check\" | \"POST /api/apm/fleet/cloud_apm_package_policy\" | \"GET /api/apm/backends/top_backends\" | \"GET /api/apm/backends/{backendName}/upstream_services\" | \"GET /api/apm/backends/{backendName}/metadata\" | \"GET /api/apm/backends/{backendName}/charts/latency\" | \"GET /api/apm/backends/{backendName}/charts/throughput\" | \"GET /api/apm/backends/{backendName}/charts/error_rate\" | \"GET /api/apm/fallback_to_transactions\" | \"GET /api/apm/has_data\" | \"GET /api/apm/event_metadata/{processorEvent}/{id}\"" + "\"POST /internal/apm/index_pattern/static\" | \"GET /internal/apm/index_pattern/dynamic\" | \"GET /internal/apm/environments\" | \"GET /internal/apm/services/{serviceName}/errors\" | \"GET /internal/apm/services/{serviceName}/errors/{groupId}\" | \"GET /internal/apm/services/{serviceName}/errors/distribution\" | \"GET /internal/apm/services/{serviceName}/metrics/charts\" | \"GET /internal/apm/observability_overview\" | \"GET /internal/apm/observability_overview/has_data\" | \"GET /api/apm/rum/client-metrics\" | \"GET /api/apm/rum-client/page-load-distribution\" | \"GET /api/apm/rum-client/page-load-distribution/breakdown\" | \"GET /api/apm/rum-client/page-view-trends\" | \"GET /api/apm/rum-client/services\" | \"GET /api/apm/rum-client/visitor-breakdown\" | \"GET /api/apm/rum-client/web-core-vitals\" | \"GET /api/apm/rum-client/long-task-metrics\" | \"GET /api/apm/rum-client/url-search\" | \"GET /api/apm/rum-client/js-errors\" | \"GET /api/apm/observability_overview/has_rum_data\" | \"GET /internal/apm/service-map\" | \"GET /internal/apm/service-map/service/{serviceName}\" | \"GET /internal/apm/service-map/backend/{backendName}\" | \"GET /internal/apm/services/{serviceName}/serviceNodes\" | \"GET /internal/apm/services\" | \"GET /internal/apm/services/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/metadata/details\" | \"GET /internal/apm/services/{serviceName}/metadata/icons\" | \"GET /internal/apm/services/{serviceName}/agent\" | \"GET /internal/apm/services/{serviceName}/transaction_types\" | \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\" | \"GET /api/apm/services/{serviceName}/annotation/search\" | \"POST /api/apm/services/{serviceName}/annotation\" | \"GET /internal/apm/services/{serviceName}/error_groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\" | \"GET /internal/apm/services/{serviceName}/throughput\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\" | \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/dependencies\" | \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\" | \"GET /internal/apm/services/{serviceName}/profiling/timeline\" | \"GET /internal/apm/services/{serviceName}/profiling/statistics\" | \"GET /internal/apm/services/{serviceName}/alerts\" | \"GET /internal/apm/services/{serviceName}/infrastructure\" | \"GET /internal/apm/suggestions\" | \"GET /internal/apm/traces/{traceId}\" | \"GET /internal/apm/traces\" | \"GET /internal/apm/traces/{traceId}/root_transaction\" | \"GET /internal/apm/transactions/{transactionId}\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\" | \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\" | \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\" | \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\" | \"GET /internal/apm/alerts/chart_preview/transaction_duration\" | \"GET /internal/apm/alerts/chart_preview/transaction_error_count\" | \"GET /api/apm/settings/agent-configuration\" | \"GET /api/apm/settings/agent-configuration/view\" | \"DELETE /api/apm/settings/agent-configuration\" | \"PUT /api/apm/settings/agent-configuration\" | \"POST /api/apm/settings/agent-configuration/search\" | \"GET /api/apm/settings/agent-configuration/services\" | \"GET /api/apm/settings/agent-configuration/environments\" | \"GET /api/apm/settings/agent-configuration/agent_name\" | \"GET /internal/apm/settings/anomaly-detection/jobs\" | \"POST /internal/apm/settings/anomaly-detection/jobs\" | \"GET /internal/apm/settings/anomaly-detection/environments\" | \"GET /internal/apm/settings/apm-index-settings\" | \"GET /internal/apm/settings/apm-indices\" | \"POST /internal/apm/settings/apm-indices/save\" | \"GET /internal/apm/settings/custom_links/transaction\" | \"GET /internal/apm/settings/custom_links\" | \"POST /internal/apm/settings/custom_links\" | \"PUT /internal/apm/settings/custom_links/{id}\" | \"DELETE /internal/apm/settings/custom_links/{id}\" | \"GET /api/apm/sourcemaps\" | \"POST /api/apm/sourcemaps\" | \"DELETE /api/apm/sourcemaps/{id}\" | \"GET /internal/apm/fleet/has_data\" | \"GET /internal/apm/fleet/agents\" | \"POST /internal/apm/fleet/apm_server_schema\" | \"GET /internal/apm/fleet/apm_server_schema/unsupported\" | \"GET /internal/apm/fleet/migration_check\" | \"POST /internal/apm/fleet/cloud_apm_package_policy\" | \"GET /internal/apm/backends/top_backends\" | \"GET /internal/apm/backends/{backendName}/upstream_services\" | \"GET /internal/apm/backends/{backendName}/metadata\" | \"GET /internal/apm/backends/{backendName}/charts/latency\" | \"GET /internal/apm/backends/{backendName}/charts/throughput\" | \"GET /internal/apm/backends/{backendName}/charts/error_rate\" | \"GET /internal/apm/fallback_to_transactions\" | \"GET /internal/apm/has_data\" | \"GET /internal/apm/event_metadata/{processorEvent}/{id}\"" ], "path": "x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts", "deprecated": false, @@ -858,7 +858,7 @@ }, ", ", "APMRouteCreateOptions", - ", { \"POST /api/apm/index_pattern/static\": ", + ", { \"POST /internal/apm/index_pattern/static\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -866,7 +866,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/index_pattern/static\", undefined, ", + "<\"POST /internal/apm/index_pattern/static\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -876,7 +876,7 @@ }, ", { created: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/index_pattern/dynamic\": ", + ">; } & { \"GET /internal/apm/index_pattern/dynamic\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -884,7 +884,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/index_pattern/dynamic\", undefined, ", + "<\"GET /internal/apm/index_pattern/dynamic\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -896,7 +896,7 @@ "IndexPatternTitleAndFields", " | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/environments\": ", + ">; } & { \"GET /internal/apm/environments\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -904,7 +904,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/environments\", ", + "<\"GET /internal/apm/environments\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -928,7 +928,7 @@ }, ", { environments: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/errors\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/errors\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -936,7 +936,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/errors\", ", + "<\"GET /internal/apm/services/{serviceName}/errors\", ", "TypeC", "<{ path: ", "TypeC", @@ -988,7 +988,7 @@ }, ", { errorGroups: { message: string; occurrenceCount: number; culprit: string | undefined; groupId: string; latestOccurrenceAt: string; handled: boolean | undefined; type: string | undefined; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/errors/{groupId}\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/errors/{groupId}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -996,7 +996,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/errors/{groupId}\", ", + "<\"GET /internal/apm/services/{serviceName}/errors/{groupId}\", ", "TypeC", "<{ path: ", "TypeC", @@ -1044,7 +1044,7 @@ "APMError", "; occurrencesCount: number; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/errors/distribution\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/errors/distribution\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1052,7 +1052,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/errors/distribution\", ", + "<\"GET /internal/apm/services/{serviceName}/errors/distribution\", ", "TypeC", "<{ path: ", "TypeC", @@ -1098,7 +1098,7 @@ }, ", { noHits: boolean; buckets: { key: number; count: number; }[]; bucketSize: number; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/metrics/charts\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/metrics/charts\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1106,7 +1106,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/metrics/charts\", ", + "<\"GET /internal/apm/services/{serviceName}/metrics/charts\", ", "TypeC", "<{ path: ", "TypeC", @@ -1158,7 +1158,7 @@ "MetricsChartsByAgentAPIResponse", ", ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/observability_overview\": ", + ">; } & { \"GET /internal/apm/observability_overview\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1166,7 +1166,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/observability_overview\", ", + "<\"GET /internal/apm/observability_overview\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1190,7 +1190,7 @@ }, ", { serviceCount: number; transactionPerMinute: { value: undefined; timeseries: never[]; } | { value: number; timeseries: { x: number; y: number | null; }[]; }; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/observability_overview/has_data\": ", + ">; } & { \"GET /internal/apm/observability_overview/has_data\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1198,7 +1198,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/observability_overview/has_data\", undefined, ", + "<\"GET /internal/apm/observability_overview/has_data\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -1642,7 +1642,7 @@ }, ", { indices: string; hasData: boolean; serviceName: string | number | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/service-map\": ", + ">; } & { \"GET /internal/apm/service-map\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1650,7 +1650,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/service-map\", ", + "<\"GET /internal/apm/service-map\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1692,7 +1692,7 @@ "ServiceAnomalyStats", " | undefined; label: string | undefined; id?: string | undefined; parent?: string | undefined; position?: cytoscape.Position | undefined; } | { 'span.destination.service.resource': string; 'span.type': string; 'span.subtype': string; label: string | undefined; id?: string | undefined; parent?: string | undefined; position?: cytoscape.Position | undefined; } | { id: string; source: string | undefined; target: string | undefined; label: string | undefined; bidirectional?: boolean | undefined; isInverseEdge?: boolean | undefined; } | undefined)[]; }; } | { data: { id: string; source: string; target: string; }; })[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/service-map/service/{serviceName}\": ", + ">; } & { \"GET /internal/apm/service-map/service/{serviceName}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1700,7 +1700,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/service-map/service/{serviceName}\", ", + "<\"GET /internal/apm/service-map/service/{serviceName}\", ", "TypeC", "<{ path: ", "TypeC", @@ -1738,7 +1738,7 @@ }, ", { avgMemoryUsage: number | null; avgCpuUsage: number | null; transactionStats: { avgTransactionDuration: number | null; avgRequestsPerMinute: number | null; }; avgErrorRate: number | null; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/service-map/backend/{backendName}\": ", + ">; } & { \"GET /internal/apm/service-map/backend/{backendName}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1746,7 +1746,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/service-map/backend/{backendName}\", ", + "<\"GET /internal/apm/service-map/backend/{backendName}\", ", "TypeC", "<{ path: ", "TypeC", @@ -1784,7 +1784,7 @@ }, ", { avgErrorRate: null; transactionStats: { avgRequestsPerMinute: null; avgTransactionDuration: null; }; } | { avgErrorRate: number; transactionStats: { avgRequestsPerMinute: number; avgTransactionDuration: number; }; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/serviceNodes\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/serviceNodes\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1792,7 +1792,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/serviceNodes\", ", + "<\"GET /internal/apm/services/{serviceName}/serviceNodes\", ", "TypeC", "<{ path: ", "TypeC", @@ -1834,7 +1834,7 @@ }, ", { serviceNodes: { name: string; cpu: number | null; heapMemory: number | null; hostName: string | null | undefined; nonHeapMemory: number | null; threadCount: number | null; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services\": ", + ">; } & { \"GET /internal/apm/services\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1842,7 +1842,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services\", ", + "<\"GET /internal/apm/services\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1892,7 +1892,7 @@ "ServiceHealthStatus", "; }>; hasLegacyData: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/detailed_statistics\": ", + ">; } & { \"GET /internal/apm/services/detailed_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1900,7 +1900,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/detailed_statistics\", ", + "<\"GET /internal/apm/services/detailed_statistics\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -1946,7 +1946,7 @@ }, ", { currentPeriod: _.Dictionary<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; previousPeriod: _.Dictionary<{ serviceName: string; latency: { x: number; y: number | null; }[]; transactionErrorRate: { x: number; y: number; }[]; throughput: { x: number; y: number; }[]; }>; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/metadata/details\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/metadata/details\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1954,7 +1954,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/metadata/details\", ", + "<\"GET /internal/apm/services/{serviceName}/metadata/details\", ", "TypeC", "<{ path: ", "TypeC", @@ -1978,7 +1978,7 @@ "ServiceMetadataDetails", ", ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/metadata/icons\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/metadata/icons\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -1986,7 +1986,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/metadata/icons\", ", + "<\"GET /internal/apm/services/{serviceName}/metadata/icons\", ", "TypeC", "<{ path: ", "TypeC", @@ -2010,7 +2010,7 @@ "ServiceMetadataIcons", ", ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/agent\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/agent\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2018,7 +2018,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/agent\", ", + "<\"GET /internal/apm/services/{serviceName}/agent\", ", "TypeC", "<{ path: ", "TypeC", @@ -2040,7 +2040,7 @@ }, ", { agentName?: undefined; runtimeName?: undefined; } | { agentName: string | undefined; runtimeName: string | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transaction_types\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transaction_types\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2048,7 +2048,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transaction_types\", ", + "<\"GET /internal/apm/services/{serviceName}/transaction_types\", ", "TypeC", "<{ path: ", "TypeC", @@ -2070,7 +2070,7 @@ }, ", { transactionTypes: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2078,7 +2078,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/node/{serviceNodeName}/metadata\", ", + "<\"GET /internal/apm/services/{serviceName}/node/{serviceNodeName}/metadata\", ", "TypeC", "<{ path: ", "TypeC", @@ -2206,7 +2206,7 @@ "Annotation", "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/error_groups/main_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/error_groups/main_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2214,7 +2214,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/error_groups/main_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/error_groups/main_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -2260,7 +2260,7 @@ }, ", { is_aggregation_accurate: boolean; error_groups: { group_id: string; name: string; lastSeen: number; occurrences: number; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/error_groups/detailed_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2268,7 +2268,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/error_groups/detailed_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/error_groups/detailed_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -2326,7 +2326,7 @@ "Coordinate", "[]; }>; previousPeriod: _.Dictionary<{ timeseries: { x: number; y: number | null | undefined; }[]; groupId: string; }>; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2334,7 +2334,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\", ", + "<\"GET /internal/apm/services/{serviceName}/service_overview_instances/details/{serviceNodeName}\", ", "TypeC", "<{ path: ", "TypeC", @@ -2380,7 +2380,7 @@ "Cloud", " | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/throughput\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/throughput\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2388,7 +2388,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/throughput\", ", + "<\"GET /internal/apm/services/{serviceName}/throughput\", ", "TypeC", "<{ path: ", "TypeC", @@ -2448,7 +2448,7 @@ "ThroughputUnit", "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2456,7 +2456,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/service_overview_instances/main_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -2522,7 +2522,7 @@ }, ", { currentPeriod: { serviceNodeName: string; errorRate?: number | undefined; latency?: number | undefined; throughput?: number | undefined; cpuUsage?: number | null | undefined; memoryUsage?: number | null | undefined; }[]; previousPeriod: { serviceNodeName: string; errorRate?: number | undefined; latency?: number | undefined; throughput?: number | undefined; cpuUsage?: number | null | undefined; memoryUsage?: number | null | undefined; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2530,7 +2530,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/service_overview_instances/detailed_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/service_overview_instances/detailed_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -2610,7 +2610,7 @@ "Coordinate", "[] | undefined; }>; previousPeriod: _.Dictionary<{ cpuUsage: { x: number; y: number | null | undefined; }[]; errorRate: { x: number; y: number | null | undefined; }[]; latency: { x: number; y: number | null | undefined; }[]; memoryUsage: { x: number; y: number | null | undefined; }[]; throughput: { x: number; y: number | null | undefined; }[]; serviceNodeName: string; }>; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/dependencies\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/dependencies\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2618,7 +2618,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/dependencies\", ", + "<\"GET /internal/apm/services/{serviceName}/dependencies\", ", "TypeC", "<{ path: ", "TypeC", @@ -2682,7 +2682,7 @@ "Node", "; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/dependencies/breakdown\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/dependencies/breakdown\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2690,7 +2690,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/dependencies/breakdown\", ", + "<\"GET /internal/apm/services/{serviceName}/dependencies/breakdown\", ", "TypeC", "<{ path: ", "TypeC", @@ -2732,7 +2732,7 @@ }, ", { breakdown: { title: string; data: { x: number; y: number; }[]; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/profiling/timeline\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/profiling/timeline\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2740,7 +2740,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/profiling/timeline\", ", + "<\"GET /internal/apm/services/{serviceName}/profiling/timeline\", ", "TypeC", "<{ path: ", "TypeC", @@ -2782,7 +2782,7 @@ }, ", { profilingTimeline: { x: number; valueTypes: { wall_time: number; cpu_time: number; samples: number; alloc_objects: number; alloc_space: number; inuse_objects: number; inuse_space: number; unknown: number; }; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/profiling/statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/profiling/statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2790,7 +2790,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/profiling/statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/profiling/statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -2866,7 +2866,7 @@ "ProfileNode", ">; rootNodes: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/alerts\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/alerts\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2874,7 +2874,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/alerts\", ", + "<\"GET /internal/apm/services/{serviceName}/alerts\", ", "TypeC", "<{ path: ", "TypeC", @@ -2916,7 +2916,7 @@ }, ", { alerts: Partial>[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/infrastructure\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/infrastructure\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -2924,7 +2924,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/infrastructure\", ", + "<\"GET /internal/apm/services/{serviceName}/infrastructure\", ", "TypeC", "<{ path: ", "TypeC", @@ -2992,7 +2992,7 @@ }, ", { terms: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/traces/{traceId}\": ", + ">; } & { \"GET /internal/apm/traces/{traceId}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3000,7 +3000,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/traces/{traceId}\", ", + "<\"GET /internal/apm/traces/{traceId}\", ", "TypeC", "<{ path: ", "TypeC", @@ -3028,7 +3028,7 @@ "APMError", "[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/traces\": ", + ">; } & { \"GET /internal/apm/traces\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3036,7 +3036,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/traces\", ", + "<\"GET /internal/apm/traces\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -3076,7 +3076,7 @@ "TransactionGroup", "[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/traces/{traceId}/root_transaction\": ", + ">; } & { \"GET /internal/apm/traces/{traceId}/root_transaction\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3084,7 +3084,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/traces/{traceId}/root_transaction\", ", + "<\"GET /internal/apm/traces/{traceId}/root_transaction\", ", "TypeC", "<{ path: ", "TypeC", @@ -3102,7 +3102,7 @@ "Transaction", "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/transactions/{transactionId}\": ", + ">; } & { \"GET /internal/apm/transactions/{transactionId}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3110,7 +3110,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/transactions/{transactionId}\", ", + "<\"GET /internal/apm/transactions/{transactionId}\", ", "TypeC", "<{ path: ", "TypeC", @@ -3128,7 +3128,7 @@ "Transaction", "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transactions/groups/main_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3136,7 +3136,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transactions/groups/main_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -3196,7 +3196,7 @@ }, ", { transactionGroups: { transactionType: string; name: string; latency: number | null; throughput: number; errorRate: number; impact: number; }[]; isAggregationAccurate: boolean; bucketSize: number; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transactions/groups/detailed_statistics\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3204,7 +3204,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transactions/groups/detailed_statistics\", ", + "<\"GET /internal/apm/services/{serviceName}/transactions/groups/detailed_statistics\", ", "TypeC", "<{ path: ", "TypeC", @@ -3280,7 +3280,7 @@ "Coordinate", "[]; impact: number; }>; previousPeriod: _.Dictionary<{ errorRate: { x: number; y: number | null | undefined; }[]; throughput: { x: number; y: number | null | undefined; }[]; latency: { x: number; y: number | null | undefined; }[]; transactionName: string; impact: number; }>; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transactions/charts/latency\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transactions/charts/latency\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3288,7 +3288,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transactions/charts/latency\", ", + "<\"GET /internal/apm/services/{serviceName}/transactions/charts/latency\", ", "TypeC", "<{ path: ", "TypeC", @@ -3360,7 +3360,7 @@ }, ", { currentPeriod: { overallAvgDuration: number | null; latencyTimeseries: { x: number; y: number | null; }[]; }; previousPeriod: { latencyTimeseries: { x: number; y: number | null | undefined; }[]; overallAvgDuration: number | null; }; anomalyTimeseries: { jobId: string; anomalyScore: { x0: number; x: number; y: number; }[]; anomalyBoundaries: { x: number; y0: number; y: number; }[]; } | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transactions/traces/samples\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transactions/traces/samples\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3368,7 +3368,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transactions/traces/samples\", ", + "<\"GET /internal/apm/services/{serviceName}/transactions/traces/samples\", ", "TypeC", "<{ path: ", "TypeC", @@ -3426,7 +3426,7 @@ }, ", { noHits: boolean; traceSamples: { transactionId: string; traceId: string; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transaction/charts/breakdown\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3434,7 +3434,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transaction/charts/breakdown\", ", + "<\"GET /internal/apm/services/{serviceName}/transaction/charts/breakdown\", ", "TypeC", "<{ path: ", "TypeC", @@ -3484,7 +3484,7 @@ }, ", { timeseries: { title: string; color: string; type: string; data: { x: number; y: number | null; }[]; hideLegend: boolean; legendValue: string; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/services/{serviceName}/transactions/charts/error_rate\": ", + ">; } & { \"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3492,7 +3492,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/services/{serviceName}/transactions/charts/error_rate\", ", + "<\"GET /internal/apm/services/{serviceName}/transactions/charts/error_rate\", ", "TypeC", "<{ path: ", "TypeC", @@ -3552,7 +3552,7 @@ "Coordinate", "[]; average: number | null; }; previousPeriod: { transactionErrorRate: { x: number; y: number | null | undefined; }[]; noHits: boolean; average: number | null; }; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/alerts/chart_preview/transaction_error_rate\": ", + ">; } & { \"GET /internal/apm/alerts/chart_preview/transaction_error_rate\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3560,7 +3560,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/alerts/chart_preview/transaction_error_rate\", ", + "<\"GET /internal/apm/alerts/chart_preview/transaction_error_rate\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -3612,7 +3612,7 @@ }, ", { errorRateChartPreview: { x: number; y: number; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/alerts/chart_preview/transaction_duration\": ", + ">; } & { \"GET /internal/apm/alerts/chart_preview/transaction_duration\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3620,7 +3620,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/alerts/chart_preview/transaction_duration\", ", + "<\"GET /internal/apm/alerts/chart_preview/transaction_duration\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -3672,7 +3672,7 @@ }, ", { latencyChartPreview: { x: number; y: number | null; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/alerts/chart_preview/transaction_error_count\": ", + ">; } & { \"GET /internal/apm/alerts/chart_preview/transaction_error_count\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3680,7 +3680,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/alerts/chart_preview/transaction_error_count\", ", + "<\"GET /internal/apm/alerts/chart_preview/transaction_error_count\", ", "TypeC", "<{ query: ", "IntersectionC", @@ -3968,7 +3968,7 @@ }, ", { agentName: string | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/anomaly-detection/jobs\": ", + ">; } & { \"GET /internal/apm/settings/anomaly-detection/jobs\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3976,7 +3976,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/anomaly-detection/jobs\", undefined, ", + "<\"GET /internal/apm/settings/anomaly-detection/jobs\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -3986,7 +3986,7 @@ }, ", { jobs: { job_id: string; environment: string; }[]; hasLegacyJobs: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"POST /api/apm/settings/anomaly-detection/jobs\": ", + ">; } & { \"POST /internal/apm/settings/anomaly-detection/jobs\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -3994,7 +3994,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/settings/anomaly-detection/jobs\", ", + "<\"POST /internal/apm/settings/anomaly-detection/jobs\", ", "TypeC", "<{ body: ", "TypeC", @@ -4012,7 +4012,7 @@ }, ", { jobCreated: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/anomaly-detection/environments\": ", + ">; } & { \"GET /internal/apm/settings/anomaly-detection/environments\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4020,7 +4020,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/anomaly-detection/environments\", undefined, ", + "<\"GET /internal/apm/settings/anomaly-detection/environments\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4030,7 +4030,7 @@ }, ", { environments: string[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/apm-index-settings\": ", + ">; } & { \"GET /internal/apm/settings/apm-index-settings\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4038,7 +4038,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/apm-index-settings\", undefined, ", + "<\"GET /internal/apm/settings/apm-index-settings\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4048,7 +4048,7 @@ }, ", { apmIndexSettings: { configurationName: \"apm_oss.sourcemapIndices\" | \"apm_oss.errorIndices\" | \"apm_oss.onboardingIndices\" | \"apm_oss.spanIndices\" | \"apm_oss.transactionIndices\" | \"apm_oss.metricsIndices\" | \"apmAgentConfigurationIndex\" | \"apmCustomLinkIndex\"; defaultValue: string; savedValue: string | undefined; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/apm-indices\": ", + ">; } & { \"GET /internal/apm/settings/apm-indices\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4056,7 +4056,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/apm-indices\", undefined, ", + "<\"GET /internal/apm/settings/apm-indices\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4068,7 +4068,7 @@ "ApmIndicesConfig", ", ", "APMRouteCreateOptions", - ">; } & { \"POST /api/apm/settings/apm-indices/save\": ", + ">; } & { \"POST /internal/apm/settings/apm-indices/save\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4076,7 +4076,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/settings/apm-indices/save\", ", + "<\"POST /internal/apm/settings/apm-indices/save\", ", "TypeC", "<{ body: ", "PartialC", @@ -4104,7 +4104,7 @@ "SavedObject", "<{}>, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/custom_links/transaction\": ", + ">; } & { \"GET /internal/apm/settings/custom_links/transaction\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4112,7 +4112,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/custom_links/transaction\", ", + "<\"GET /internal/apm/settings/custom_links/transaction\", ", "PartialC", "<{ query: ", "PartialC", @@ -4136,7 +4136,7 @@ "Transaction", ", ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/settings/custom_links\": ", + ">; } & { \"GET /internal/apm/settings/custom_links\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4144,7 +4144,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/settings/custom_links\", ", + "<\"GET /internal/apm/settings/custom_links\", ", "PartialC", "<{ query: ", "PartialC", @@ -4168,7 +4168,7 @@ "CustomLink", "[]; }, ", "APMRouteCreateOptions", - ">; } & { \"POST /api/apm/settings/custom_links\": ", + ">; } & { \"POST /internal/apm/settings/custom_links\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4176,7 +4176,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/settings/custom_links\", ", + "<\"POST /internal/apm/settings/custom_links\", ", "TypeC", "<{ body: ", "IntersectionC", @@ -4220,7 +4220,7 @@ }, ", void, ", "APMRouteCreateOptions", - ">; } & { \"PUT /api/apm/settings/custom_links/{id}\": ", + ">; } & { \"PUT /internal/apm/settings/custom_links/{id}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4228,7 +4228,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"PUT /api/apm/settings/custom_links/{id}\", ", + "<\"PUT /internal/apm/settings/custom_links/{id}\", ", "TypeC", "<{ path: ", "TypeC", @@ -4276,7 +4276,7 @@ }, ", void, ", "APMRouteCreateOptions", - ">; } & { \"DELETE /api/apm/settings/custom_links/{id}\": ", + ">; } & { \"DELETE /internal/apm/settings/custom_links/{id}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4284,7 +4284,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"DELETE /api/apm/settings/custom_links/{id}\", ", + "<\"DELETE /internal/apm/settings/custom_links/{id}\", ", "TypeC", "<{ path: ", "TypeC", @@ -4382,7 +4382,7 @@ }, ", void, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/fleet/has_data\": ", + ">; } & { \"GET /internal/apm/fleet/has_data\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4390,7 +4390,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/fleet/has_data\", undefined, ", + "<\"GET /internal/apm/fleet/has_data\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4400,7 +4400,7 @@ }, ", { hasData: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/fleet/agents\": ", + ">; } & { \"GET /internal/apm/fleet/agents\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4408,7 +4408,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/fleet/agents\", undefined, ", + "<\"GET /internal/apm/fleet/agents\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4418,7 +4418,7 @@ }, ", { cloudStandaloneSetup: { apmServerUrl: string | undefined; secretToken: string | undefined; } | undefined; isFleetEnabled: boolean; fleetAgents: { id: string; name: string; apmServerUrl: any; secretToken: any; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"POST /api/apm/fleet/apm_server_schema\": ", + ">; } & { \"POST /internal/apm/fleet/apm_server_schema\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4426,7 +4426,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/fleet/apm_server_schema\", ", + "<\"POST /internal/apm/fleet/apm_server_schema\", ", "TypeC", "<{ body: ", "TypeC", @@ -4446,7 +4446,7 @@ }, ", void, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/fleet/apm_server_schema/unsupported\": ", + ">; } & { \"GET /internal/apm/fleet/apm_server_schema/unsupported\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4454,7 +4454,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/fleet/apm_server_schema/unsupported\", undefined, ", + "<\"GET /internal/apm/fleet/apm_server_schema/unsupported\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4464,7 +4464,7 @@ }, ", { unsupported: { key: string; value: any; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/fleet/migration_check\": ", + ">; } & { \"GET /internal/apm/fleet/migration_check\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4472,7 +4472,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/fleet/migration_check\", undefined, ", + "<\"GET /internal/apm/fleet/migration_check\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4482,7 +4482,7 @@ }, ", { has_cloud_agent_policy: boolean; has_cloud_apm_package_policy: boolean; cloud_apm_migration_enabled: boolean; has_required_role: boolean | undefined; }, ", "APMRouteCreateOptions", - ">; } & { \"POST /api/apm/fleet/cloud_apm_package_policy\": ", + ">; } & { \"POST /internal/apm/fleet/cloud_apm_package_policy\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4490,7 +4490,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"POST /api/apm/fleet/cloud_apm_package_policy\", undefined, ", + "<\"POST /internal/apm/fleet/cloud_apm_package_policy\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4508,7 +4508,7 @@ }, "; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/top_backends\": ", + ">; } & { \"GET /internal/apm/backends/top_backends\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4516,7 +4516,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/top_backends\", ", + "<\"GET /internal/apm/backends/top_backends\", ", "IntersectionC", "<[", "TypeC", @@ -4584,7 +4584,7 @@ "Node", "; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/{backendName}/upstream_services\": ", + ">; } & { \"GET /internal/apm/backends/{backendName}/upstream_services\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4592,7 +4592,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/{backendName}/upstream_services\", ", + "<\"GET /internal/apm/backends/{backendName}/upstream_services\", ", "IntersectionC", "<[", "TypeC", @@ -4666,7 +4666,7 @@ "Node", "; }[]; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/{backendName}/metadata\": ", + ">; } & { \"GET /internal/apm/backends/{backendName}/metadata\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4674,7 +4674,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/{backendName}/metadata\", ", + "<\"GET /internal/apm/backends/{backendName}/metadata\", ", "TypeC", "<{ path: ", "TypeC", @@ -4696,7 +4696,7 @@ }, ", { metadata: { spanType: string | undefined; spanSubtype: string | undefined; }; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/{backendName}/charts/latency\": ", + ">; } & { \"GET /internal/apm/backends/{backendName}/charts/latency\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4704,7 +4704,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/{backendName}/charts/latency\", ", + "<\"GET /internal/apm/backends/{backendName}/charts/latency\", ", "TypeC", "<{ path: ", "TypeC", @@ -4750,7 +4750,7 @@ }, ", { currentTimeseries: { x: number; y: number; }[]; comparisonTimeseries: { x: number; y: number; }[] | null; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/{backendName}/charts/throughput\": ", + ">; } & { \"GET /internal/apm/backends/{backendName}/charts/throughput\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4758,7 +4758,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/{backendName}/charts/throughput\", ", + "<\"GET /internal/apm/backends/{backendName}/charts/throughput\", ", "TypeC", "<{ path: ", "TypeC", @@ -4804,7 +4804,7 @@ }, ", { currentTimeseries: { x: number; y: number | null; }[]; comparisonTimeseries: { x: number; y: number | null; }[] | null; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/backends/{backendName}/charts/error_rate\": ", + ">; } & { \"GET /internal/apm/backends/{backendName}/charts/error_rate\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4812,7 +4812,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/backends/{backendName}/charts/error_rate\", ", + "<\"GET /internal/apm/backends/{backendName}/charts/error_rate\", ", "TypeC", "<{ path: ", "TypeC", @@ -4858,7 +4858,7 @@ }, ", { currentTimeseries: { x: number; y: number; }[]; comparisonTimeseries: { x: number; y: number; }[] | null; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/fallback_to_transactions\": ", + ">; } & { \"GET /internal/apm/fallback_to_transactions\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4866,7 +4866,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/fallback_to_transactions\", ", + "<\"GET /internal/apm/fallback_to_transactions\", ", "PartialC", "<{ query: ", "IntersectionC", @@ -4890,7 +4890,7 @@ }, ", { fallbackToTransactions: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/has_data\": ", + ">; } & { \"GET /internal/apm/has_data\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4898,7 +4898,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/has_data\", undefined, ", + "<\"GET /internal/apm/has_data\", undefined, ", { "pluginId": "apm", "scope": "server", @@ -4908,7 +4908,7 @@ }, ", { hasData: boolean; }, ", "APMRouteCreateOptions", - ">; } & { \"GET /api/apm/event_metadata/{processorEvent}/{id}\": ", + ">; } & { \"GET /internal/apm/event_metadata/{processorEvent}/{id}\": ", { "pluginId": "@kbn/server-route-repository", "scope": "server", @@ -4916,7 +4916,7 @@ "section": "def-server.ServerRoute", "text": "ServerRoute" }, - "<\"GET /api/apm/event_metadata/{processorEvent}/{id}\", ", + "<\"GET /internal/apm/event_metadata/{processorEvent}/{id}\", ", "TypeC", "<{ path: ", "TypeC", diff --git a/api_docs/apm.mdx b/api_docs/apm.mdx index a3fb1df512fa5..d9e53e6ec9df8 100644 --- a/api_docs/apm.mdx +++ b/api_docs/apm.mdx @@ -10,7 +10,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex --- import apmObj from './apm.json'; - +The user interface for Elastic APM Contact [APM UI](https://github.com/orgs/elastic/teams/apm-ui) for questions regarding this plugin. diff --git a/api_docs/charts.json b/api_docs/charts.json index 9f6d07287eba1..5d4f047a247e2 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -975,9 +975,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" } ], @@ -1004,7 +1004,7 @@ "tags": [], "label": "ColorSchemaParams", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false, "children": [ { @@ -1017,13 +1017,13 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" } ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1033,7 +1033,7 @@ "tags": [], "label": "invertColors", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false } ], @@ -1255,7 +1255,7 @@ "tags": [], "label": "Labels", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false, "children": [ { @@ -1268,7 +1268,7 @@ "signature": [ "string | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1281,7 +1281,7 @@ "signature": [ "boolean | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1294,7 +1294,7 @@ "signature": [ "boolean | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1307,7 +1307,7 @@ "signature": [ "number | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1320,7 +1320,7 @@ "signature": [ "boolean | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1333,7 +1333,7 @@ "signature": [ "number | null | undefined" ], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false } ], @@ -1830,9 +1830,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" } ], @@ -1923,7 +1923,7 @@ "tags": [], "label": "Style", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false, "children": [ { @@ -1933,7 +1933,7 @@ "tags": [], "label": "bgFill", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1943,7 +1943,7 @@ "tags": [], "label": "bgColor", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1953,7 +1953,7 @@ "tags": [], "label": "labelColor", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1963,7 +1963,7 @@ "tags": [], "label": "subText", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false }, { @@ -1973,7 +1973,7 @@ "tags": [], "label": "fontSize", "description": [], - "path": "src/plugins/charts/public/static/components/types.ts", + "path": "src/plugins/charts/common/types.ts", "deprecated": false } ], @@ -2041,9 +2041,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchema", + "section": "def-common.ColorSchema", "text": "ColorSchema" }, "[]" @@ -2117,9 +2117,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchema", + "section": "def-common.ColorSchema", "text": "ColorSchema" }, "[]" @@ -2202,9 +2202,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".Blues" @@ -2257,9 +2257,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".Greens" @@ -2312,9 +2312,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".Greys" @@ -2367,9 +2367,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".Reds" @@ -2422,9 +2422,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".YellowToRed" @@ -2477,9 +2477,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemas", + "section": "def-common.ColorSchemas", "text": "ColorSchemas" }, ".GreenToRed" @@ -2946,6 +2946,51 @@ "common": { "classes": [], "functions": [ + { + "parentPluginId": "charts", + "id": "def-common.getHeatmapColors", + "type": "Function", + "tags": [], + "label": "getHeatmapColors", + "description": [], + "signature": [ + "(value: any, colorSchemaName: string) => string" + ], + "path": "src/plugins/charts/common/static/color_maps/heatmap_color.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.getHeatmapColors.$1", + "type": "Any", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "any" + ], + "path": "src/plugins/charts/common/static/color_maps/heatmap_color.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "charts", + "id": "def-common.getHeatmapColors.$2", + "type": "string", + "tags": [], + "label": "colorSchemaName", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/charts/common/static/color_maps/heatmap_color.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "charts", "id": "def-common.palette", @@ -3084,6 +3129,116 @@ } ], "interfaces": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorMap", + "type": "Interface", + "tags": [], + "label": "ColorMap", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorMap.Unnamed", + "type": "Any", + "tags": [], + "label": "Unnamed", + "description": [], + "signature": [ + "any" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.ColorSchema", + "type": "Interface", + "tags": [], + "label": "ColorSchema", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorSchema.value", + "type": "Enum", + "tags": [], + "label": "value", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + } + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.ColorSchema.text", + "type": "string", + "tags": [], + "label": "text", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.ColorSchemaParams", + "type": "Interface", + "tags": [], + "label": "ColorSchemaParams", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorSchemaParams.colorSchema", + "type": "Enum", + "tags": [], + "label": "colorSchema", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + } + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.ColorSchemaParams.invertColors", + "type": "boolean", + "tags": [], + "label": "invertColors", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "charts", "id": "def-common.CustomPaletteArguments", @@ -3293,6 +3448,97 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "charts", + "id": "def-common.Labels", + "type": "Interface", + "tags": [], + "label": "Labels", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.Labels.color", + "type": "string", + "tags": [], + "label": "color", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Labels.filter", + "type": "CompoundType", + "tags": [], + "label": "filter", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Labels.overwriteColor", + "type": "CompoundType", + "tags": [], + "label": "overwriteColor", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Labels.rotate", + "type": "number", + "tags": [], + "label": "rotate", + "description": [], + "signature": [ + "number | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Labels.show", + "type": "CompoundType", + "tags": [], + "label": "show", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Labels.truncate", + "type": "CompoundType", + "tags": [], + "label": "truncate", + "description": [], + "signature": [ + "number | null | undefined" + ], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "charts", "id": "def-common.PaletteOutput", @@ -3354,29 +3600,159 @@ }, { "parentPluginId": "charts", - "id": "def-common.SystemPaletteArguments", + "id": "def-common.RawColorSchema", "type": "Interface", "tags": [], - "label": "SystemPaletteArguments", + "label": "RawColorSchema", "description": [], - "path": "src/plugins/charts/common/palette.ts", + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", "deprecated": false, "children": [ { "parentPluginId": "charts", - "id": "def-common.SystemPaletteArguments.name", - "type": "string", + "id": "def-common.RawColorSchema.id", + "type": "Enum", "tags": [], - "label": "name", + "label": "id", "description": [], - "path": "src/plugins/charts/common/palette.ts", + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + } + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", "deprecated": false - } - ], - "initialIsOpen": false - } + }, + { + "parentPluginId": "charts", + "id": "def-common.RawColorSchema.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.RawColorSchema.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Style", + "type": "Interface", + "tags": [], + "label": "Style", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.Style.bgFill", + "type": "string", + "tags": [], + "label": "bgFill", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Style.bgColor", + "type": "boolean", + "tags": [], + "label": "bgColor", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Style.labelColor", + "type": "boolean", + "tags": [], + "label": "labelColor", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Style.subText", + "type": "string", + "tags": [], + "label": "subText", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.Style.fontSize", + "type": "number", + "tags": [], + "label": "fontSize", + "description": [], + "path": "src/plugins/charts/common/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.SystemPaletteArguments", + "type": "Interface", + "tags": [], + "label": "SystemPaletteArguments", + "description": [], + "path": "src/plugins/charts/common/palette.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.SystemPaletteArguments.name", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "path": "src/plugins/charts/common/palette.ts", + "deprecated": false + } + ], + "initialIsOpen": false + } + ], + "enums": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorSchemas", + "type": "Enum", + "tags": [], + "label": "ColorSchemas", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "initialIsOpen": false + } ], - "enums": [], "misc": [ { "parentPluginId": "charts", @@ -3392,6 +3768,52 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "charts", + "id": "def-common.ColorMode", + "type": "Type", + "tags": [], + "label": "ColorMode", + "description": [], + "signature": [ + "\"Background\" | \"Labels\" | \"None\"" + ], + "path": "src/plugins/charts/common/static/components/collections.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.colorSchemas", + "type": "Array", + "tags": [], + "label": "colorSchemas", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchema", + "text": "ColorSchema" + }, + "[]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.defaultCountLabel", + "type": "string", + "tags": [], + "label": "defaultCountLabel", + "description": [], + "path": "src/plugins/charts/common/static/components/collections.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "charts", "id": "def-common.defaultCustomColors", @@ -3406,6 +3828,20 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "charts", + "id": "def-common.LabelRotation", + "type": "Type", + "tags": [], + "label": "LabelRotation", + "description": [], + "signature": [ + "number" + ], + "path": "src/plugins/charts/common/static/components/collections.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "charts", "id": "def-common.paletteIds", @@ -3419,8 +3855,415 @@ "path": "src/plugins/charts/common/constants.ts", "deprecated": false, "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.truncatedColorSchemas", + "type": "Array", + "tags": [], + "label": "truncatedColorSchemas", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchema", + "text": "ColorSchema" + }, + "[]" + ], + "path": "src/plugins/charts/common/static/color_maps/truncated_color_maps.ts", + "deprecated": false, + "initialIsOpen": false } ], - "objects": [] + "objects": [ + { + "parentPluginId": "charts", + "id": "def-common.ColorMode", + "type": "Object", + "tags": [], + "label": "ColorMode", + "description": [], + "signature": [ + "{ readonly Background: \"Background\"; readonly Labels: \"Labels\"; readonly None: \"None\"; }" + ], + "path": "src/plugins/charts/common/static/components/collections.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.LabelRotation", + "type": "Object", + "tags": [], + "label": "LabelRotation", + "description": [], + "signature": [ + "{ readonly Horizontal: number; readonly Vertical: number; readonly Angled: number; }" + ], + "path": "src/plugins/charts/common/static/components/collections.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.truncatedColorMaps", + "type": "Object", + "tags": [], + "label": "truncatedColorMaps", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/truncated_color_maps.ts", + "deprecated": false, + "children": [], + "initialIsOpen": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps", + "type": "Object", + "tags": [], + "label": "vislibColorMaps", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Blues", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.Blues]", + "description": [ + "// Sequential" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Blues.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".Blues" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Blues.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Blues.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greens", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.Greens]", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greens.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".Greens" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greens.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greens.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greys", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.Greys]", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greys.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".Greys" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greys.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Greys.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Reds", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.Reds]", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Reds.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".Reds" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Reds.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.Reds.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.YellowToRed", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.YellowToRed]", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.YellowToRed.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".YellowToRed" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.YellowToRed.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.YellowToRed.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.GreenToRed", + "type": "Object", + "tags": [], + "label": "[ColorSchemas.GreenToRed]", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.GreenToRed.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + }, + ".GreenToRed" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.GreenToRed.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + }, + { + "parentPluginId": "charts", + "id": "def-common.vislibColorMaps.ColorSchemas.GreenToRed.value", + "type": "Array", + "tags": [], + "label": "value", + "description": [], + "signature": [ + "[number, number[]][]" + ], + "path": "src/plugins/charts/common/static/color_maps/color_maps.ts", + "deprecated": false + } + ] + } + ], + "initialIsOpen": false + } + ] } } \ No newline at end of file diff --git a/api_docs/charts.mdx b/api_docs/charts.mdx index c8159a3bc0dfa..8d31f6ad1d640 100644 --- a/api_docs/charts.mdx +++ b/api_docs/charts.mdx @@ -18,7 +18,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 223 | 2 | 192 | 3 | +| 285 | 4 | 253 | 3 | ## Client @@ -53,12 +53,18 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) ## Common +### Objects + + ### Functions ### Interfaces +### Enums + + ### Consts, variables and types diff --git a/api_docs/core.json b/api_docs/core.json index 8b64ef86dbf16..c288037b4486d 100644 --- a/api_docs/core.json +++ b/api_docs/core.json @@ -1603,7 +1603,7 @@ "label": "links", "description": [], "signature": [ - "{ readonly settings: string; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; readonly histogram: string; readonly ip_range: string; readonly range: string; readonly significant_terms: string; readonly terms: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; readonly min_bucket: string; readonly sum_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative_sum: string; readonly derivative: string; readonly geo_bounds: string; readonly geo_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving_avg: string; readonly percentile_ranks: string; readonly serial_diff: string; readonly std_dev: string; readonly sum: string; readonly top_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; readonly autocompleteChanges: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; readonly transforms: Record; readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly watcher: Record; readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; }>; readonly ecs: { readonly guide: string; }; }" + "{ readonly settings: string; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly enterpriseSearch: { readonly base: string; readonly appSearchBase: string; readonly workplaceSearchBase: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite_missing_bucket: string; readonly date_histogram: string; readonly date_range: string; readonly date_format_pattern: string; readonly filter: string; readonly filters: string; readonly geohash_grid: string; readonly histogram: string; readonly ip_range: string; readonly range: string; readonly significant_terms: string; readonly terms: string; readonly avg: string; readonly avg_bucket: string; readonly max_bucket: string; readonly min_bucket: string; readonly sum_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative_sum: string; readonly derivative: string; readonly geo_bounds: string; readonly geo_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving_avg: string; readonly percentile_ranks: string; readonly serial_diff: string; readonly std_dev: string; readonly sum: string; readonly top_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: string; readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; readonly autocompleteChanges: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record; readonly ml: Record; readonly transforms: Record; readonly visualize: Record; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record; readonly maps: Record; readonly monitoring: Record; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record; readonly ccs: Record; readonly plugins: Record; readonly snapshotRestore: Record; readonly ingest: Record; readonly fleet: Readonly<{ guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; }" ], "path": "src/core/public/doc_links/doc_links_service.ts", "deprecated": false @@ -1611,40 +1611,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "core", - "id": "def-public.DomainDeprecationDetails", - "type": "Interface", - "tags": [], - "label": "DomainDeprecationDetails", - "description": [], - "signature": [ - "DomainDeprecationDetails", - " extends ", - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.DeprecationsDetails", - "text": "DeprecationsDetails" - } - ], - "path": "src/core/server/deprecations/types.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "core", - "id": "def-public.DomainDeprecationDetails.domainId", - "type": "string", - "tags": [], - "label": "domainId", - "description": [], - "path": "src/core/server/deprecations/types.ts", - "deprecated": false - } - ], - "initialIsOpen": false - }, { "parentPluginId": "core", "id": "def-public.EnvironmentMode", @@ -8281,6 +8247,45 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecationContext", + "type": "Interface", + "tags": [], + "label": "ConfigDeprecationContext", + "description": [ + "\nDeprecation context provided to {@link ConfigDeprecation | config deprecations}\n" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecationContext.version", + "type": "string", + "tags": [], + "label": "version", + "description": [ + "The current Kibana version, e.g `7.16.1`, `8.0.0`" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecationContext.branch", + "type": "string", + "tags": [], + "label": "branch", + "description": [ + "The current Kibana branch, e.g `7.x`, `7.16`, `master`" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.ConfigDeprecationFactory", @@ -8306,7 +8311,13 @@ "(deprecatedKey: string, removeBy: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -8371,7 +8382,13 @@ "(deprecatedKey: string, removeBy: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -8436,7 +8453,13 @@ "(oldKey: string, newKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -8501,7 +8524,13 @@ "(oldKey: string, newKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -8566,7 +8595,13 @@ "(unusedKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -8617,7 +8652,13 @@ "(unusedKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", "deprecated": false, @@ -9552,7 +9593,9 @@ "type": "string", "tags": [], "label": "documentationUrl", - "description": [], + "description": [ + "(optional) link to the documentation for more details on the deprecation." + ], "signature": [ "string | undefined" ], @@ -9565,7 +9608,9 @@ "type": "CompoundType", "tags": [], "label": "requireRestart", - "description": [], + "description": [ + "(optional) specify the fix for this deprecation requires a full kibana restart." + ], "signature": [ "boolean | undefined" ], @@ -9578,7 +9623,9 @@ "type": "Object", "tags": [], "label": "correctiveActions", - "description": [], + "description": [ + "corrective action needed to fix this deprecation." + ], "signature": [ "{ api?: { path: string; method: \"PUT\" | \"POST\"; body?: { [key: string]: any; } | undefined; } | undefined; manualSteps: string[]; }" ], @@ -16830,6 +16877,123 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation", + "type": "Type", + "tags": [], + "label": "ConfigDeprecation", + "description": [ + "\nConfiguration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration.\n" + ], + "signature": [ + "(config: Readonly<{ [x: string]: any; }>, fromPath: string, addDeprecation: ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.AddConfigDeprecation", + "text": "AddConfigDeprecation" + }, + ", context: ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationContext", + "text": "ConfigDeprecationContext" + }, + ") => void | ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationCommand", + "text": "ConfigDeprecationCommand" + } + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation.$1", + "type": "Object", + "tags": [], + "label": "config", + "description": [ + "must not be mutated, return {@link ConfigDeprecationCommand} to change config shape." + ], + "signature": [ + "{ readonly [x: string]: any; }" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation.$2", + "type": "string", + "tags": [], + "label": "fromPath", + "description": [], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation.$3", + "type": "Function", + "tags": [], + "label": "addDeprecation", + "description": [], + "signature": [ + "(details: ", + "DeprecatedConfigDetails", + ") => void" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation.$3.$1", + "type": "Object", + "tags": [], + "label": "details", + "description": [], + "signature": [ + "DeprecatedConfigDetails" + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "core", + "id": "def-server.ConfigDeprecation.$4", + "type": "Object", + "tags": [], + "label": "context", + "description": [], + "signature": [ + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationContext", + "text": "ConfigDeprecationContext" + } + ], + "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "core", "id": "def-server.ConfigDeprecationProvider", @@ -16849,7 +17013,13 @@ "text": "ConfigDeprecationFactory" }, ") => ", - "ConfigDeprecation", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + }, "[]" ], "path": "node_modules/@kbn/config/target_types/deprecation/types.d.ts", @@ -17974,7 +18144,7 @@ "EcsHttp", " | undefined; log?: Pick<", "EcsLog", - ", \"origin\" | \"original\" | \"file\" | \"syslog\"> | undefined; network?: ", + ", \"origin\" | \"file\" | \"syslog\"> | undefined; network?: ", "EcsNetwork", " | undefined; observer?: ", "EcsObserver", diff --git a/api_docs/core.mdx b/api_docs/core.mdx index 856db3cf8871b..018a9f1beda6c 100644 --- a/api_docs/core.mdx +++ b/api_docs/core.mdx @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2293 | 27 | 1020 | 29 | +| 2300 | 27 | 1019 | 29 | ## Client diff --git a/api_docs/core_application.json b/api_docs/core_application.json index 71e4244af778b..52f50c364fdba 100644 --- a/api_docs/core_application.json +++ b/api_docs/core_application.json @@ -1664,7 +1664,7 @@ "tags": [], "label": "euiIconType", "description": [ - "\nA EUI iconType that will be used for the app's icon. This icon\ntakes precendence over the `icon` property." + "\nA EUI iconType that will be used for the app's icon. This icon\ntakes precedence over the `icon` property." ], "signature": [ "string | undefined" diff --git a/api_docs/core_application.mdx b/api_docs/core_application.mdx index bf9e610a24b78..1c01073421f69 100644 --- a/api_docs/core_application.mdx +++ b/api_docs/core_application.mdx @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2293 | 27 | 1020 | 29 | +| 2300 | 27 | 1019 | 29 | ## Client diff --git a/api_docs/core_chrome.mdx b/api_docs/core_chrome.mdx index c217d2ae66f73..18244445385ca 100644 --- a/api_docs/core_chrome.mdx +++ b/api_docs/core_chrome.mdx @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2293 | 27 | 1020 | 29 | +| 2300 | 27 | 1019 | 29 | ## Client diff --git a/api_docs/core_http.json b/api_docs/core_http.json index f6345d6f6e933..94ee961f265b7 100644 --- a/api_docs/core_http.json +++ b/api_docs/core_http.json @@ -1249,7 +1249,7 @@ "tags": [], "label": "fetch", "description": [ - "Makes an HTTP request. Defaults to a GET request unless overriden. See {@link HttpHandler} for options." + "Makes an HTTP request. Defaults to a GET request unless overridden. See {@link HttpHandler} for options." ], "signature": [ { diff --git a/api_docs/core_http.mdx b/api_docs/core_http.mdx index 739916c56ecfc..ae5747c711b97 100644 --- a/api_docs/core_http.mdx +++ b/api_docs/core_http.mdx @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2293 | 27 | 1020 | 29 | +| 2300 | 27 | 1019 | 29 | ## Client diff --git a/api_docs/core_saved_objects.json b/api_docs/core_saved_objects.json index f5b601e346dec..91af1d2465c3d 100644 --- a/api_docs/core_saved_objects.json +++ b/api_docs/core_saved_objects.json @@ -5600,7 +5600,7 @@ "tags": [], "label": "objectTransformError", "description": [ - "\nError returned when a {@link SavedObjectsExportTransform | export tranform} threw an error" + "\nError returned when a {@link SavedObjectsExportTransform | export transform} threw an error" ], "signature": [ "(objects: ", @@ -5656,7 +5656,7 @@ "tags": [], "label": "invalidTransformError", "description": [ - "\nError returned when a {@link SavedObjectsExportTransform | export tranform} performed an invalid operation\nduring the transform, such as removing objects from the export, or changing an object's type or id." + "\nError returned when a {@link SavedObjectsExportTransform | export transform} performed an invalid operation\nduring the transform, such as removing objects from the export, or changing an object's type or id." ], "signature": [ "(objectKeys: string[]) => ", @@ -6599,7 +6599,7 @@ ], "label": "resolveImportErrors", "description": [ - "\nResolve and return saved object import errors.\nSee the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed informations.\n" + "\nResolve and return saved object import errors.\nSee the {@link SavedObjectsResolveImportErrorsOptions | options} for more detailed information.\n" ], "signature": [ "({ readStream, createNewCopies, namespace, retries, }: ", @@ -16148,7 +16148,7 @@ "tags": [], "label": "SavedObjectsClientContract", "description": [ - "\nSaved Objects is Kibana's data persisentence mechanism allowing plugins to\nuse Elasticsearch for storing plugin state.\n\n## SavedObjectsClient errors\n\nSince the SavedObjectsClient has its hands in everything we\nare a little paranoid about the way we present errors back to\nto application code. Ideally, all errors will be either:\n\n 1. Caused by bad implementation (ie. undefined is not a function) and\n as such unpredictable\n 2. An error that has been classified and decorated appropriately\n by the decorators in {@link SavedObjectsErrorHelpers}\n\nType 1 errors are inevitable, but since all expected/handle-able errors\nshould be Type 2 the `isXYZError()` helpers exposed at\n`SavedObjectsErrorHelpers` should be used to understand and manage error\nresponses from the `SavedObjectsClient`.\n\nType 2 errors are decorated versions of the source error, so if\nthe elasticsearch client threw an error it will be decorated based\non its type. That means that rather than looking for `error.body.error.type` or\ndoing substring checks on `error.body.error.reason`, just use the helpers to\nunderstand the meaning of the error:\n\n ```js\n if (SavedObjectsErrorHelpers.isNotFoundError(error)) {\n // handle 404\n }\n\n if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) {\n // 401 handling should be automatic, but in case you wanted to know\n }\n\n // always rethrow the error unless you handle it\n throw error;\n ```\n\n### 404s from missing index\n\nFrom the perspective of application code and APIs the SavedObjectsClient is\na black box that persists objects. One of the internal details that users have\nno control over is that we use an elasticsearch index for persistance and that\nindex might be missing.\n\nAt the time of writing we are in the process of transitioning away from the\noperating assumption that the SavedObjects index is always available. Part of\nthis transition is handling errors resulting from an index missing. These used\nto trigger a 500 error in most cases, and in others cause 404s with different\nerror messages.\n\nFrom my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The\nobject the request/call was targeting could not be found. This is why #14141\ntakes special care to ensure that 404 errors are generic and don't distinguish\nbetween index missing or document missing.\n\nSee {@link SavedObjectsClient}\nSee {@link SavedObjectsErrorHelpers}\n" + "\nSaved Objects is Kibana's data persisentence mechanism allowing plugins to\nuse Elasticsearch for storing plugin state.\n\n## SavedObjectsClient errors\n\nSince the SavedObjectsClient has its hands in everything we\nare a little paranoid about the way we present errors back to\nto application code. Ideally, all errors will be either:\n\n 1. Caused by bad implementation (ie. undefined is not a function) and\n as such unpredictable\n 2. An error that has been classified and decorated appropriately\n by the decorators in {@link SavedObjectsErrorHelpers}\n\nType 1 errors are inevitable, but since all expected/handle-able errors\nshould be Type 2 the `isXYZError()` helpers exposed at\n`SavedObjectsErrorHelpers` should be used to understand and manage error\nresponses from the `SavedObjectsClient`.\n\nType 2 errors are decorated versions of the source error, so if\nthe elasticsearch client threw an error it will be decorated based\non its type. That means that rather than looking for `error.body.error.type` or\ndoing substring checks on `error.body.error.reason`, just use the helpers to\nunderstand the meaning of the error:\n\n ```js\n if (SavedObjectsErrorHelpers.isNotFoundError(error)) {\n // handle 404\n }\n\n if (SavedObjectsErrorHelpers.isNotAuthorizedError(error)) {\n // 401 handling should be automatic, but in case you wanted to know\n }\n\n // always rethrow the error unless you handle it\n throw error;\n ```\n\n### 404s from missing index\n\nFrom the perspective of application code and APIs the SavedObjectsClient is\na black box that persists objects. One of the internal details that users have\nno control over is that we use an elasticsearch index for persistence and that\nindex might be missing.\n\nAt the time of writing we are in the process of transitioning away from the\noperating assumption that the SavedObjects index is always available. Part of\nthis transition is handling errors resulting from an index missing. These used\nto trigger a 500 error in most cases, and in others cause 404s with different\nerror messages.\n\nFrom my (Spencer) perspective, a 404 from the SavedObjectsApi is a 404; The\nobject the request/call was targeting could not be found. This is why #14141\ntakes special care to ensure that 404 errors are generic and don't distinguish\nbetween index missing or document missing.\n\nSee {@link SavedObjectsClient}\nSee {@link SavedObjectsErrorHelpers}\n" ], "signature": [ "{ get: (type: string, id: string, options?: ", diff --git a/api_docs/core_saved_objects.mdx b/api_docs/core_saved_objects.mdx index 66d66b3d41c18..5fc7bc63466b1 100644 --- a/api_docs/core_saved_objects.mdx +++ b/api_docs/core_saved_objects.mdx @@ -18,7 +18,7 @@ Contact [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) for que | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 2293 | 27 | 1020 | 29 | +| 2300 | 27 | 1019 | 29 | ## Client diff --git a/api_docs/dashboard.json b/api_docs/dashboard.json index f43e6e3923ca9..b42846c747bfa 100644 --- a/api_docs/dashboard.json +++ b/api_docs/dashboard.json @@ -2426,15 +2426,15 @@ "references": [ { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx" + "path": "x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts" + "path": "x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx" }, { "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx" + "path": "x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx" } ] }, diff --git a/api_docs/dashboard_enhanced.json b/api_docs/dashboard_enhanced.json index 9712b25f7377a..92bdfbc11b3f6 100644 --- a/api_docs/dashboard_enhanced.json +++ b/api_docs/dashboard_enhanced.json @@ -617,7 +617,15 @@ "section": "def-server.SerializableRecord", "text": "SerializableRecord" }, - ">): void; }" + ">): void; setAnonymousAccessServiceProvider: (provider: () => ", + { + "pluginId": "share", + "scope": "common", + "docId": "kibSharePluginApi", + "section": "def-common.AnonymousAccessServiceContract", + "text": "AnonymousAccessServiceContract" + }, + ") => void; }" ], "path": "x-pack/plugins/dashboard_enhanced/public/plugin.ts", "deprecated": false diff --git a/api_docs/data.json b/api_docs/data.json index 31d0a919b2d44..ab02b81539cf2 100644 --- a/api_docs/data.json +++ b/api_docs/data.json @@ -4170,35 +4170,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", @@ -12311,10 +12311,6 @@ "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/utils.ts" }, - { - "plugin": "dataViews", - "path": "src/plugins/data_views/common/fields/utils.ts" - }, { "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/data_view_field.ts" @@ -13309,14 +13305,6 @@ "plugin": "monitoring", "path": "x-pack/plugins/monitoring/public/lib/kuery.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts" @@ -13529,14 +13517,6 @@ "plugin": "timelines", "path": "x-pack/plugins/timelines/public/mock/index_pattern.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, { "plugin": "infra", "path": "x-pack/plugins/infra/target/types/public/containers/with_kuery_autocompletion.d.ts" @@ -13801,6 +13781,14 @@ "plugin": "infra", "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts" @@ -15902,7 +15890,7 @@ "section": "def-common.FilterMeta", "text": "FilterMeta" }, - "; exists?: { field: string; } | undefined; }" + "; query: { exists?: { field: string; } | undefined; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, @@ -18097,6 +18085,11 @@ ], "label": "IFieldSubType", "description": [], + "signature": [ + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional" + ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, "removeBy": "8.1", @@ -19798,9 +19791,9 @@ }, " & { meta: ", "MatchAllFilterMeta", - "; match_all: ", + "; query: { match_all: ", "QueryDslMatchAllQuery", - "; }" + "; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, @@ -19929,7 +19922,7 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; range: { [key: string]: ", + "; query: { range: { [key: string]: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -19937,7 +19930,7 @@ "section": "def-common.RangeFilterParams", "text": "RangeFilterParams" }, - "; }; }" + "; }; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, @@ -20319,35 +20312,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts" }, { "plugin": "maps", @@ -20489,10 +20482,6 @@ "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/detections/components/rules/description_step/helpers.tsx" @@ -21515,66 +21504,6 @@ } ] }, - { - "parentPluginId": "data", - "id": "def-public.esFilters.isMissingFilter", - "type": "Function", - "tags": [], - "label": "isMissingFilter", - "description": [], - "signature": [ - "(filter: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - ") => filter is ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.MissingFilter", - "text": "MissingFilter" - } - ], - "path": "src/plugins/data/public/deprecated.ts", - "deprecated": false, - "returnComment": [], - "children": [ - { - "parentPluginId": "data", - "id": "def-public.esFilters.isMissingFilter.$1", - "type": "Object", - "tags": [], - "label": "filter", - "description": [], - "signature": [ - "{ $state?: { store: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterStateStore", - "text": "FilterStateStore" - }, - "; } | undefined; meta: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterMeta", - "text": "FilterMeta" - }, - "; query?: Record | undefined; }" - ], - "path": "node_modules/@kbn/es-query/target_types/filters/build_filters/missing_filter.d.ts", - "deprecated": false - } - ] - }, { "parentPluginId": "data", "id": "def-public.esFilters.isQueryStringFilter", @@ -22563,7 +22492,7 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; range: { [key: string]: ", + "; query: { range: { [key: string]: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -22571,7 +22500,7 @@ "section": "def-common.RangeFilterParams", "text": "RangeFilterParams" }, - "; }; }" + "; }; }; }" ], "path": "src/plugins/data/public/query/timefilter/lib/change_time_filter.ts", "deprecated": false @@ -22630,7 +22559,7 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; range: { [key: string]: ", + "; query: { range: { [key: string]: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -22638,7 +22567,7 @@ "section": "def-common.RangeFilterParams", "text": "RangeFilterParams" }, - "; }; }" + "; }; }; }" ], "path": "src/plugins/data/public/query/timefilter/lib/change_time_filter.ts", "deprecated": false @@ -24148,15 +24077,15 @@ "label": "isNestedField", "description": [], "signature": [ - "(field: ", + "(field: Pick<", { - "pluginId": "dataViews", + "pluginId": "@kbn/es-query", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" }, - ") => boolean" + ", \"subType\">) => boolean" ], "path": "src/plugins/data/public/index.ts", "deprecated": false, @@ -24170,13 +24099,150 @@ "label": "field", "description": [], "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" - } + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.isMultiField", + "type": "Function", + "tags": [], + "label": "isMultiField", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => boolean" + ], + "path": "src/plugins/data/public/index.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.isMultiField.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.getFieldSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeMulti", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "src/plugins/data/public/index.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.getFieldSubtypeMulti.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.getFieldSubtypeNested", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeNested", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "src/plugins/data/public/index.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-public.indexPatterns.getFieldSubtypeNested.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" ], "path": "src/plugins/data_views/common/fields/utils.ts", "deprecated": false @@ -26318,10 +26384,6 @@ "plugin": "indexPatternManagement", "path": "src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx" }, - { - "plugin": "visTypeMetric", - "path": "src/plugins/vis_types/metric/public/plugin.ts" - }, { "plugin": "visTypePie", "path": "src/plugins/vis_types/pie/public/pie_component.tsx" @@ -29774,35 +29836,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", @@ -33985,10 +34047,6 @@ "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/utils.ts" }, - { - "plugin": "dataViews", - "path": "src/plugins/data_views/common/fields/utils.ts" - }, { "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/data_view_field.ts" @@ -36961,6 +37019,11 @@ ], "label": "IFieldSubType", "description": [], + "signature": [ + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional" + ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, "removeBy": "8.1", @@ -41065,18 +41128,14 @@ { "parentPluginId": "data", "id": "def-common.DataViewField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", @@ -41127,6 +41186,82 @@ "path": "src/plugins/data_views/common/fields/data_view_field.ts", "deprecated": false }, + { + "parentPluginId": "data", + "id": "def-common.DataViewField.isSubtypeNested", + "type": "Function", + "tags": [], + "label": "isSubtypeNested", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "data", + "id": "def-common.DataViewField.isSubtypeMulti", + "type": "Function", + "tags": [], + "label": "isSubtypeMulti", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "data", + "id": "def-common.DataViewField.getSubtypeNested", + "type": "Function", + "tags": [], + "label": "getSubtypeNested", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "data", + "id": "def-common.DataViewField.getSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getSubtypeMulti", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "data", "id": "def-common.DataViewField.deleteCount", @@ -41151,13 +41286,9 @@ "description": [], "signature": [ "() => { count: number; script: string | undefined; lang: \"painless\" | \"expression\" | \"mustache\" | \"java\" | undefined; conflictDescriptions: Record | undefined; name: string; type: string; esTypes: string[] | undefined; scripted: boolean; searchable: boolean; aggregatable: boolean; readFromDocValues: boolean; subType: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; customLabel: string | undefined; }" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", @@ -43916,35 +44047,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", @@ -49135,6 +49266,106 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "data", + "id": "def-common.getFieldSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeMulti", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-common.getFieldSubtypeMulti.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "data", + "id": "def-common.getFieldSubtypeNested", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeNested", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "data", + "id": "def-common.getFieldSubtypeNested.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "data", "id": "def-common.getFilterableKbnTypeNames", @@ -49764,64 +49995,41 @@ }, { "parentPluginId": "data", - "id": "def-common.isMissingFilter", + "id": "def-common.isMultiField", "type": "Function", - "tags": [ - "deprecated" - ], - "label": "isMissingFilter", + "tags": [], + "label": "isMultiField", "description": [], "signature": [ - "(filter: ", + "(field: Pick<", { "pluginId": "@kbn/es-query", "scope": "common", "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" }, - ") => filter is ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.MissingFilter", - "text": "MissingFilter" - } + ", \"subType\">) => boolean" ], - "path": "src/plugins/data/common/es_query/index.ts", - "deprecated": true, - "removeBy": "8.1", - "references": [], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, "returnComment": [], "children": [ { "parentPluginId": "data", - "id": "def-common.isMissingFilter.$1", + "id": "def-common.isMultiField.$1", "type": "Object", "tags": [], - "label": "filter", + "label": "field", "description": [], "signature": [ - "{ $state?: { store: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterStateStore", - "text": "FilterStateStore" - }, - "; } | undefined; meta: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterMeta", - "text": "FilterMeta" - }, - "; query?: Record | undefined; }" + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" ], - "path": "node_modules/@kbn/es-query/target_types/filters/build_filters/missing_filter.d.ts", + "path": "src/plugins/data_views/common/fields/utils.ts", "deprecated": false } ], @@ -49835,18 +50043,19 @@ "label": "isNestedField", "description": [], "signature": [ - "(field: ", + "(field: Pick<", { - "pluginId": "dataViews", + "pluginId": "@kbn/es-query", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" }, - ") => boolean" + ", \"subType\">) => boolean" ], "path": "src/plugins/data_views/common/fields/utils.ts", "deprecated": false, + "returnComment": [], "children": [ { "parentPluginId": "data", @@ -49856,20 +50065,16 @@ "label": "field", "description": [], "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" - } + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" ], "path": "src/plugins/data_views/common/fields/utils.ts", - "deprecated": false, - "isRequired": true + "deprecated": false } ], - "returnComment": [], "initialIsOpen": false }, { @@ -51582,18 +51787,14 @@ { "parentPluginId": "data", "id": "def-common.FieldSpecExportFmt.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "src/plugins/data_views/common/types.ts", @@ -52005,10 +52206,6 @@ "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/utils.ts" }, - { - "plugin": "dataViews", - "path": "src/plugins/data_views/common/fields/utils.ts" - }, { "plugin": "dataViews", "path": "src/plugins/data_views/common/fields/data_view_field.ts" @@ -53003,14 +53200,6 @@ "plugin": "monitoring", "path": "x-pack/plugins/monitoring/public/lib/kuery.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts" @@ -53223,14 +53412,6 @@ "plugin": "timelines", "path": "x-pack/plugins/timelines/public/mock/index_pattern.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, { "plugin": "infra", "path": "x-pack/plugins/infra/target/types/public/containers/with_kuery_autocompletion.d.ts" @@ -53495,6 +53676,14 @@ "plugin": "infra", "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts" @@ -55604,7 +55793,7 @@ "section": "def-common.FilterMeta", "text": "FilterMeta" }, - "; exists?: { field: string; } | undefined; }" + "; query: { exists?: { field: string; } | undefined; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, @@ -57641,6 +57830,11 @@ ], "label": "IFieldSubType", "description": [], + "signature": [ + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional" + ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, "removeBy": "8.1", @@ -57713,19 +57907,19 @@ }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts" }, { "plugin": "dashboard", @@ -59166,9 +59360,9 @@ }, " & { meta: ", "MatchAllFilterMeta", - "; match_all: ", + "; query: { match_all: ", "QueryDslMatchAllQuery", - "; }" + "; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, @@ -59190,39 +59384,6 @@ "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "data", - "id": "def-common.MissingFilter", - "type": "Type", - "tags": [ - "deprecated" - ], - "label": "MissingFilter", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - " & { meta: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterMeta", - "text": "FilterMeta" - }, - "; missing: { field: string; }; }" - ], - "path": "src/plugins/data/common/es_query/index.ts", - "deprecated": true, - "removeBy": "8.1", - "references": [], - "initialIsOpen": false - }, { "parentPluginId": "data", "id": "def-common.OnError", @@ -59435,7 +59596,7 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; range: { [key: string]: ", + "; query: { range: { [key: string]: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -59443,7 +59604,7 @@ "section": "def-common.RangeFilterParams", "text": "RangeFilterParams" }, - "; }; }" + "; }; }; }" ], "path": "src/plugins/data/common/es_query/index.ts", "deprecated": true, diff --git a/api_docs/data.mdx b/api_docs/data.mdx index a8e983c6a6b24..0734f19d6c3eb 100644 --- a/api_docs/data.mdx +++ b/api_docs/data.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3181 | 43 | 2796 | 48 | +| 3192 | 43 | 2807 | 48 | ## Client diff --git a/api_docs/data_autocomplete.mdx b/api_docs/data_autocomplete.mdx index 61c786d94839d..1eef9ef1c6932 100644 --- a/api_docs/data_autocomplete.mdx +++ b/api_docs/data_autocomplete.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3181 | 43 | 2796 | 48 | +| 3192 | 43 | 2807 | 48 | ## Client diff --git a/api_docs/data_query.mdx b/api_docs/data_query.mdx index cc5a593ab216b..bb88e5868b605 100644 --- a/api_docs/data_query.mdx +++ b/api_docs/data_query.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3181 | 43 | 2796 | 48 | +| 3192 | 43 | 2807 | 48 | ## Client diff --git a/api_docs/data_search.json b/api_docs/data_search.json index 836984073c0e6..25ed62f473bd3 100644 --- a/api_docs/data_search.json +++ b/api_docs/data_search.json @@ -2277,13 +2277,7 @@ "label": "uiSettingsClient", "description": [], "signature": [ - { - "pluginId": "core", - "scope": "server", - "docId": "kibCorePluginApi", - "section": "def-server.IUiSettingsClient", - "text": "IUiSettingsClient" - } + "{ get: (key: string) => Promise; }" ], "path": "src/plugins/data/server/search/types.ts", "deprecated": false @@ -28577,7 +28571,7 @@ "label": "fn", "description": [], "signature": [ - "(input: null, args: Arguments) => any" + "(input: null, args: Arguments) => { type: \"kibana_filter\"; meta: { negate: boolean; alias: string; disabled: boolean; }; query: any; }" ], "path": "src/plugins/data/common/search/expressions/kibana_filter.ts", "deprecated": false, diff --git a/api_docs/data_search.mdx b/api_docs/data_search.mdx index 8b89cd491c166..c7256296f1634 100644 --- a/api_docs/data_search.mdx +++ b/api_docs/data_search.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3181 | 43 | 2796 | 48 | +| 3192 | 43 | 2807 | 48 | ## Client diff --git a/api_docs/data_ui.mdx b/api_docs/data_ui.mdx index 87c9d2ce08af0..39a1948d15c2c 100644 --- a/api_docs/data_ui.mdx +++ b/api_docs/data_ui.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 3181 | 43 | 2796 | 48 | +| 3192 | 43 | 2807 | 48 | ## Client diff --git a/api_docs/data_views.json b/api_docs/data_views.json index d3160ea108718..c77aa02425e22 100644 --- a/api_docs/data_views.json +++ b/api_docs/data_views.json @@ -4736,35 +4736,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", @@ -13245,18 +13245,14 @@ { "parentPluginId": "dataViews", "id": "def-common.DataViewField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", @@ -13307,6 +13303,82 @@ "path": "src/plugins/data_views/common/fields/data_view_field.ts", "deprecated": false }, + { + "parentPluginId": "dataViews", + "id": "def-common.DataViewField.isSubtypeNested", + "type": "Function", + "tags": [], + "label": "isSubtypeNested", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "dataViews", + "id": "def-common.DataViewField.isSubtypeMulti", + "type": "Function", + "tags": [], + "label": "isSubtypeMulti", + "description": [], + "signature": [ + "() => boolean" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "dataViews", + "id": "def-common.DataViewField.getSubtypeNested", + "type": "Function", + "tags": [], + "label": "getSubtypeNested", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "dataViews", + "id": "def-common.DataViewField.getSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getSubtypeMulti", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/data_view_field.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, { "parentPluginId": "dataViews", "id": "def-common.DataViewField.deleteCount", @@ -13331,13 +13403,9 @@ "description": [], "signature": [ "() => { count: number; script: string | undefined; lang: \"painless\" | \"expression\" | \"mustache\" | \"java\" | undefined; conflictDescriptions: Record | undefined; name: string; type: string; esTypes: string[] | undefined; scripted: boolean; searchable: boolean; aggregatable: boolean; readFromDocValues: boolean; subType: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; customLabel: string | undefined; }" ], "path": "src/plugins/data_views/common/fields/data_view_field.ts", @@ -16472,35 +16540,35 @@ }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/types.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx" + "path": "x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts" }, { "plugin": "observability", @@ -19769,6 +19837,106 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "dataViews", + "id": "def-common.getFieldSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeMulti", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "dataViews", + "id": "def-common.getFieldSubtypeMulti.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "dataViews", + "id": "def-common.getFieldSubtypeNested", + "type": "Function", + "tags": [], + "label": "getFieldSubtypeNested", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "dataViews", + "id": "def-common.getFieldSubtypeNested.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "dataViews", "id": "def-common.getIndexPatternLoadMeta", @@ -19838,6 +20006,48 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "dataViews", + "id": "def-common.isMultiField", + "type": "Function", + "tags": [], + "label": "isMultiField", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => boolean" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "dataViews", + "id": "def-common.isMultiField.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" + ], + "path": "src/plugins/data_views/common/fields/utils.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "dataViews", "id": "def-common.isNestedField", @@ -19846,18 +20056,19 @@ "label": "isNestedField", "description": [], "signature": [ - "(field: ", + "(field: Pick<", { - "pluginId": "dataViews", + "pluginId": "@kbn/es-query", "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" }, - ") => boolean" + ", \"subType\">) => boolean" ], "path": "src/plugins/data_views/common/fields/utils.ts", "deprecated": false, + "returnComment": [], "children": [ { "parentPluginId": "dataViews", @@ -19867,20 +20078,16 @@ "label": "field", "description": [], "signature": [ - { - "pluginId": "dataViews", - "scope": "common", - "docId": "kibDataViewsPluginApi", - "section": "def-common.IFieldType", - "text": "IFieldType" - } + "{ subType?: ", + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined; }" ], "path": "src/plugins/data_views/common/fields/utils.ts", - "deprecated": false, - "isRequired": true + "deprecated": false } ], - "returnComment": [], "initialIsOpen": false } ], @@ -20754,18 +20961,14 @@ { "parentPluginId": "dataViews", "id": "def-common.FieldSpecExportFmt.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "src/plugins/data_views/common/types.ts", @@ -22411,14 +22614,6 @@ "plugin": "monitoring", "path": "x-pack/plugins/monitoring/public/lib/kuery.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" - }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts" @@ -22631,14 +22826,6 @@ "plugin": "timelines", "path": "x-pack/plugins/timelines/public/mock/index_pattern.ts" }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, - { - "plugin": "securitySolution", - "path": "x-pack/plugins/security_solution/target/types/common/search_strategy/index_fields/index.d.ts" - }, { "plugin": "infra", "path": "x-pack/plugins/infra/target/types/public/containers/with_kuery_autocompletion.d.ts" @@ -22903,6 +23090,14 @@ "plugin": "infra", "path": "x-pack/plugins/infra/public/pages/metrics/index.tsx" }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, + { + "plugin": "securitySolution", + "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts" + }, { "plugin": "securitySolution", "path": "x-pack/plugins/security_solution/public/common/store/sourcerer/model.ts" @@ -24924,19 +25119,19 @@ }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts" }, { "plugin": "visualizations", - "path": "src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts" + "path": "src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts" }, { "plugin": "dashboard", diff --git a/api_docs/data_views.mdx b/api_docs/data_views.mdx index 373587de6f284..e907c075b9a8f 100644 --- a/api_docs/data_views.mdx +++ b/api_docs/data_views.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 671 | 6 | 531 | 5 | +| 681 | 6 | 541 | 5 | ## Client diff --git a/api_docs/deprecations_by_api.mdx b/api_docs/deprecations_by_api.mdx index 5e3c38eb354c1..a083c82e69d05 100644 --- a/api_docs/deprecations_by_api.mdx +++ b/api_docs/deprecations_by_api.mdx @@ -46,7 +46,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | dataViews, indexPatternManagement, data | - | | | dataViews, discover, ml, transform, canvas | - | | | dataViews, visTypeTimeseries, maps, lens, discover | - | -| | fleet, indexPatternFieldEditor, discover, dashboard, lens, ml, stackAlerts, indexPatternManagement, visTypeMetric, visTypePie, visTypeTable, visTypeTimeseries, visTypeXy, visTypeVislib | - | +| | fleet, indexPatternFieldEditor, discover, dashboard, lens, ml, stackAlerts, indexPatternManagement, visTypePie, visTypeTable, visTypeTimeseries, visTypeXy, visTypeVislib | - | | | reporting, visTypeTimeseries | - | | | data, lens, visTypeTimeseries, infra, maps, visTypeTimelion | - | | | dashboard, maps, graph, visualize | - | @@ -178,7 +178,6 @@ Safe to remove. | | | | | | -| | | | | | | | @@ -203,7 +202,6 @@ Safe to remove. | | | | | | -| | | | | | | | diff --git a/api_docs/deprecations_by_plugin.mdx b/api_docs/deprecations_by_plugin.mdx index 3714161b6c426..b33ab317ca885 100644 --- a/api_docs/deprecations_by_plugin.mdx +++ b/api_docs/deprecations_by_plugin.mdx @@ -205,12 +205,12 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField)+ 2 more | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern) | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 14 more | 8.1 | +| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 13 more | 8.1 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternSpec), [create_index_pattern.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/routes/create_index_pattern.ts#:~:text=IndexPatternSpec), [create_index_pattern.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/routes/create_index_pattern.ts#:~:text=IndexPatternSpec) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternType) | - | | | [data_views.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_views.ts#:~:text=IndexPatternListItem), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternListItem) | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 14 more | 8.1 | +| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 13 more | 8.1 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=IIndexPattern) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IIndexPatternsApiClient), [index_patterns_api_client.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/index_patterns_api_client.ts#:~:text=IIndexPatternsApiClient), [index_patterns_api_client.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/index_patterns_api_client.ts#:~:text=IIndexPatternsApiClient) | - | @@ -230,7 +230,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getNonScriptedFields) | 8.1 | | | [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=getScriptedFields), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=getScriptedFields), [data_view.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.ts#:~:text=getScriptedFields), [data_views.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_views.ts#:~:text=getScriptedFields), [register_index_pattern_usage_collection.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/register_index_pattern_usage_collection.ts#:~:text=getScriptedFields), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getScriptedFields), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getScriptedFields), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getScriptedFields), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getScriptedFields), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=getScriptedFields)+ 1 more | 8.1 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternField), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPatternField)+ 2 more | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 14 more | 8.1 | +| | [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/utils.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [data_view_field.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [field_list.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/field_list.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/types.ts#:~:text=IFieldType)+ 13 more | 8.1 | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [utils.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/utils.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes), [scripted_fields.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/server/deprecations/scripted_fields.ts#:~:text=IndexPatternAttributes) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [data_view_field.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/fields/data_view_field.test.ts#:~:text=IndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern), [data_view.test.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/data_views/data_view.test.ts#:~:text=IndexPattern) | - | | | [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/common/index.ts#:~:text=IndexPatternsService), [index.ts](https://github.com/elastic/kibana/tree/master/src/plugins/data_views/public/index.ts#:~:text=IndexPatternsService) | - | @@ -678,7 +678,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=IndexPattern), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=IndexPattern), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=IndexPattern), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=IndexPattern), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=IndexPattern), [lens_attributes.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts#:~:text=IndexPattern), [lens_attributes.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts#:~:text=IndexPattern), [lens_attributes.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts#:~:text=IndexPattern), [default_configs.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#:~:text=IndexPattern), [default_configs.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts#:~:text=IndexPattern)+ 38 more | - | | | [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec) | - | | | [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=indexPatterns), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=indexPatterns), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=indexPatterns), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/pages/alerts/index.tsx#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns), [observability_index_patterns.test.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.test.ts#:~:text=indexPatterns)+ 5 more | - | -| | [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters) | 8.1 | +| | [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=esFilters) | 8.1 | | | [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=ExistsFilter), [utils.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts#:~:text=ExistsFilter) | 8.1 | | | [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=Filter), [filter_value_label.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx#:~:text=Filter) | 8.1 | | | [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec), [observability_index_patterns.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/observability_index_patterns.ts#:~:text=IndexPatternSpec) | - | @@ -836,15 +836,15 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Deprecated API | Reference location(s) | Remove By | | ---------------|-----------|-----------| -| | [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx#:~:text=dashboardUrlGenerator), [use_risky_hosts_dashboard_button_href.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts#:~:text=dashboardUrlGenerator), [use_risky_hosts_dashboard_links.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx#:~:text=dashboardUrlGenerator) | - | +| | [use_risky_hosts_dashboard_button_href.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href.ts#:~:text=dashboardUrlGenerator), [use_risky_hosts_dashboard_links.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_risky_hosts_dashboard_links.tsx#:~:text=dashboardUrlGenerator), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx#:~:text=dashboardUrlGenerator) | - | | | [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [list_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx#:~:text=IndexPattern), [list_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx#:~:text=IndexPattern)+ 30 more | - | | | [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField) | - | -| | [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IIndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern)+ 76 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/url_state/types.ts#:~:text=IIndexPattern)+ 74 more | - | | | [middleware.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts#:~:text=indexPatterns), [plugin.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/plugin.tsx#:~:text=indexPatterns), [dependencies_start_mock.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/mock/endpoint/dependencies_start_mock.ts#:~:text=indexPatterns) | - | -| | [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx#:~:text=esFilters), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters)+ 15 more | 8.1 | +| | [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx#:~:text=esFilters), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/query_bar/index.tsx#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters), [epic.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/epic.ts#:~:text=esFilters)+ 14 more | 8.1 | | | [expandable_network.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx#:~:text=esQuery), [expandable_network.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/side_panel/network_details/expandable_network.tsx#:~:text=esQuery), [events_viewer.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx#:~:text=esQuery), [events_viewer.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/overview/components/events_by_dataset/index.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx#:~:text=esQuery), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/top_n/index.tsx#:~:text=esQuery)+ 30 more | 8.1 | | | [store.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/types/timeline/store.ts#:~:text=Filter), [store.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/types/timeline/store.ts#:~:text=Filter), [model.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/model.ts#:~:text=Filter), [model.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/model.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts#:~:text=Filter), [selectors.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts#:~:text=Filter), [selectors.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts#:~:text=Filter)+ 163 more | 8.1 | -| | [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IIndexPattern), [index.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern)+ 162 more | - | +| | [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [action.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [index.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/containers/source/index.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.tsx#:~:text=IIndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/url_state/types.ts#:~:text=IIndexPattern)+ 158 more | - | | | [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField), [field_name_cell.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/event_details/table/field_name_cell.tsx#:~:text=IndexPatternField) | - | | | [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [helpers.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/helpers.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [entry_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/entry_item.tsx#:~:text=IndexPattern), [list_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx#:~:text=IndexPattern), [list_item.tsx](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/components/threat_match/list_item.tsx#:~:text=IndexPattern)+ 30 more | - | | | [store.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/types/timeline/store.ts#:~:text=Filter), [store.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/common/types/timeline/store.ts#:~:text=Filter), [model.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/model.ts#:~:text=Filter), [model.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/model.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/actions.ts#:~:text=Filter), [selectors.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts#:~:text=Filter), [selectors.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/common/store/inputs/selectors.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts#:~:text=Filter), [actions.ts](https://github.com/elastic/kibana/tree/master/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts#:~:text=Filter)+ 163 more | 8.1 | @@ -983,14 +983,6 @@ warning: This document is auto-generated and is meant to be viewed inside our ex -## visTypeMetric - -| Deprecated API | Reference location(s) | Remove By | -| ---------------|-----------|-----------| -| | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/vis_types/metric/public/plugin.ts#:~:text=fieldFormats) | - | - - - ## visTypePie | Deprecated API | Reference location(s) | Remove By | @@ -1101,7 +1093,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [plugin.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/plugin.ts#:~:text=indexPatterns) | - | | | [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=esFilters), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=esFilters) | 8.1 | | | [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter) | 8.1 | -| | [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [timeseries_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [timeseries_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/timeseries_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE)+ 8 more | - | +| | [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [timeseries_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [timeseries_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_visualization_references/timeseries_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [visualization_saved_object_migrations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE), [controls_references.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/utils/saved_visualization_references/controls_references.ts#:~:text=INDEX_PATTERN_SAVED_OBJECT_TYPE)+ 8 more | - | | | [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=IndexPatternsContract), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=IndexPatternsContract), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=IndexPatternsContract), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=IndexPatternsContract) | - | | | [vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis.ts#:~:text=IndexPattern), [vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [create_vis_embeddable_from_object.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts#:~:text=IndexPattern), [create_vis_embeddable_from_object.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern)+ 10 more | - | | | [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter) | 8.1 | @@ -1109,7 +1101,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis.ts#:~:text=IndexPattern), [vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/vis_types/types.ts#:~:text=IndexPattern), [create_vis_embeddable_from_object.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts#:~:text=IndexPattern), [create_vis_embeddable_from_object.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/create_vis_embeddable_from_object.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=IndexPattern) | - | | | [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter), [visualize_embeddable.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/embeddable/visualize_embeddable.ts#:~:text=Filter) | 8.1 | | | [find_list_items.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts#:~:text=SavedObjectLoader), [find_list_items.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/find_list_items.ts#:~:text=SavedObjectLoader), [saved_visualizations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts#:~:text=SavedObjectLoader), [saved_visualizations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts#:~:text=SavedObjectLoader), [saved_visualizations.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/saved_visualizations.ts#:~:text=SavedObjectLoader), [services.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/services.ts#:~:text=SavedObjectLoader), [services.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/services.ts#:~:text=SavedObjectLoader) | - | -| | [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/types.ts#:~:text=SavedObject), [types.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/types.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject) | - | +| | [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject), [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObject) | - | | | [_saved_vis.ts](https://github.com/elastic/kibana/tree/master/src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts#:~:text=SavedObjectClass) | - | diff --git a/api_docs/elastic_apm_generator.json b/api_docs/elastic_apm_generator.json new file mode 100644 index 0000000000000..dc69c08bba5b2 --- /dev/null +++ b/api_docs/elastic_apm_generator.json @@ -0,0 +1,257 @@ +{ + "id": "@elastic/apm-generator", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.getObserverDefaults", + "type": "Function", + "tags": [], + "label": "getObserverDefaults", + "description": [], + "signature": [ + "() => Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>" + ], + "path": "packages/elastic-apm-generator/src/lib/defaults/get_observer_defaults.ts", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.getSpanDestinationMetrics", + "type": "Function", + "tags": [], + "label": "getSpanDestinationMetrics", + "description": [], + "signature": [ + "(events: Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]) => Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]" + ], + "path": "packages/elastic-apm-generator/src/lib/utils/get_span_destination_metrics.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.getSpanDestinationMetrics.$1", + "type": "Array", + "tags": [], + "label": "events", + "description": [], + "signature": [ + "Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]" + ], + "path": "packages/elastic-apm-generator/src/lib/utils/get_span_destination_metrics.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.getTransactionMetrics", + "type": "Function", + "tags": [], + "label": "getTransactionMetrics", + "description": [], + "signature": [ + "(events: Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]) => { \"transaction.duration.histogram\": { values: number[]; counts: number[]; }; _doc_count: number; '@timestamp'?: number | undefined; 'agent.name'?: string | undefined; 'agent.version'?: string | undefined; 'ecs.version'?: string | undefined; 'event.outcome'?: string | undefined; 'event.ingested'?: number | undefined; 'metricset.name'?: string | undefined; 'observer.version'?: string | undefined; 'observer.version_major'?: number | undefined; 'parent.id'?: string | undefined; 'processor.event'?: string | undefined; 'processor.name'?: string | undefined; 'trace.id'?: string | undefined; 'transaction.name'?: string | undefined; 'transaction.type'?: string | undefined; 'transaction.id'?: string | undefined; 'transaction.duration.us'?: number | undefined; 'transaction.sampled'?: true | undefined; 'service.name'?: string | undefined; 'service.environment'?: string | undefined; 'service.node.name'?: string | undefined; 'span.id'?: string | undefined; 'span.name'?: string | undefined; 'span.type'?: string | undefined; 'span.subtype'?: string | undefined; 'span.duration.us'?: number | undefined; 'span.destination.service.name'?: string | undefined; 'span.destination.service.resource'?: string | undefined; 'span.destination.service.type'?: string | undefined; 'span.destination.service.response_time.sum.us'?: number | undefined; 'span.destination.service.response_time.count'?: number | undefined; }[]" + ], + "path": "packages/elastic-apm-generator/src/lib/utils/get_transaction_metrics.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.getTransactionMetrics.$1", + "type": "Array", + "tags": [], + "label": "events", + "description": [], + "signature": [ + "Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]" + ], + "path": "packages/elastic-apm-generator/src/lib/utils/get_transaction_metrics.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.service", + "type": "Function", + "tags": [], + "label": "service", + "description": [], + "signature": [ + "(name: string, environment: string, agentName: string) => ", + "Service" + ], + "path": "packages/elastic-apm-generator/src/lib/service.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.service.$1", + "type": "string", + "tags": [], + "label": "name", + "description": [], + "signature": [ + "string" + ], + "path": "packages/elastic-apm-generator/src/lib/service.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.service.$2", + "type": "string", + "tags": [], + "label": "environment", + "description": [], + "signature": [ + "string" + ], + "path": "packages/elastic-apm-generator/src/lib/service.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.service.$3", + "type": "string", + "tags": [], + "label": "agentName", + "description": [], + "signature": [ + "string" + ], + "path": "packages/elastic-apm-generator/src/lib/service.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.timerange", + "type": "Function", + "tags": [], + "label": "timerange", + "description": [], + "signature": [ + "(from: number, to: number) => ", + "Timerange" + ], + "path": "packages/elastic-apm-generator/src/lib/timerange.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.timerange.$1", + "type": "number", + "tags": [], + "label": "from", + "description": [], + "signature": [ + "number" + ], + "path": "packages/elastic-apm-generator/src/lib/timerange.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.timerange.$2", + "type": "number", + "tags": [], + "label": "to", + "description": [], + "signature": [ + "number" + ], + "path": "packages/elastic-apm-generator/src/lib/timerange.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.toElasticsearchOutput", + "type": "Function", + "tags": [], + "label": "toElasticsearchOutput", + "description": [], + "signature": [ + "(events: Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[], versionOverride: string | undefined) => { _index: string; _source: {}; }[]" + ], + "path": "packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.toElasticsearchOutput.$1", + "type": "Array", + "tags": [], + "label": "events", + "description": [], + "signature": [ + "Partial<{ '@timestamp': number; 'agent.name': string; 'agent.version': string; 'ecs.version': string; 'event.outcome': string; 'event.ingested': number; 'metricset.name': string; 'observer.version': string; 'observer.version_major': number; 'parent.id': string; 'processor.event': string; 'processor.name': string; 'trace.id': string; 'transaction.name': string; 'transaction.type': string; 'transaction.id': string; 'transaction.duration.us': number; 'transaction.duration.histogram': { values: number[]; counts: number[]; }; 'transaction.sampled': true; 'service.name': string; 'service.environment': string; 'service.node.name': string; 'span.id': string; 'span.name': string; 'span.type': string; 'span.subtype': string; 'span.duration.us': number; 'span.destination.service.name': string; 'span.destination.service.resource': string; 'span.destination.service.type': string; 'span.destination.service.response_time.sum.us': number; 'span.destination.service.response_time.count': number; }>[]" + ], + "path": "packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "@elastic/apm-generator", + "id": "def-server.toElasticsearchOutput.$2", + "type": "string", + "tags": [], + "label": "versionOverride", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "packages/elastic-apm-generator/src/lib/output/to_elasticsearch_output.ts", + "deprecated": false, + "isRequired": false + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/elastic_apm_generator.mdx b/api_docs/elastic_apm_generator.mdx new file mode 100644 index 0000000000000..3b7667a6837b5 --- /dev/null +++ b/api_docs/elastic_apm_generator.mdx @@ -0,0 +1,27 @@ +--- +id: kibElasticApmGeneratorPluginApi +slug: /kibana-dev-docs/api/elastic-apm-generator +title: "@elastic/apm-generator" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the @elastic/apm-generator plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', '@elastic/apm-generator'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import elasticApmGeneratorObj from './elastic_apm_generator.json'; + +Elastic APM trace data generator + +Contact [Owner missing] for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 15 | 0 | 15 | 2 | + +## Server + +### Functions + + diff --git a/api_docs/event_log.json b/api_docs/event_log.json index 95ab6b473a0d3..1991fbff2589d 100644 --- a/api_docs/event_log.json +++ b/api_docs/event_log.json @@ -753,7 +753,7 @@ "label": "logEvent", "description": [], "signature": [ - "(properties: DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" + "(properties: DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -766,7 +766,7 @@ "label": "properties", "description": [], "signature": [ - "DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -783,7 +783,7 @@ "label": "startTiming", "description": [], "signature": [ - "(event: DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" + "(event: DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -796,7 +796,7 @@ "label": "event", "description": [], "signature": [ - "DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -813,7 +813,7 @@ "label": "stopTiming", "description": [], "signature": [ - "(event: DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" + "(event: DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => void" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -826,7 +826,7 @@ "label": "event", "description": [], "signature": [ - "DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, @@ -886,7 +886,7 @@ "label": "data", "description": [], "signature": [ - "(Readonly<{ tags?: string[] | undefined; kibana?: Readonly<{ version?: string | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" + "(Readonly<{ tags?: string[] | undefined; kibana?: Readonly<{ version?: string | undefined; alert?: Readonly<{ rule?: Readonly<{ execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ total_indexing_duration_ms?: number | undefined; total_search_duration_ms?: number | undefined; execution_gap_duration_s?: number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}> | undefined)[]" ], "path": "x-pack/plugins/event_log/server/es/cluster_client_adapter.ts", "deprecated": false @@ -905,7 +905,7 @@ "label": "IEvent", "description": [], "signature": [ - "DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -919,7 +919,7 @@ "label": "IValidatedEvent", "description": [], "signature": [ - "Readonly<{ tags?: string[] | undefined; kibana?: Readonly<{ version?: string | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}> | undefined" + "Readonly<{ tags?: string[] | undefined; kibana?: Readonly<{ version?: string | undefined; alert?: Readonly<{ rule?: Readonly<{ execution?: Readonly<{ status?: string | undefined; metrics?: Readonly<{ total_indexing_duration_ms?: number | undefined; total_search_duration_ms?: number | undefined; execution_gap_duration_s?: number | undefined; } & {}> | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}> | undefined" ], "path": "x-pack/plugins/event_log/generated/schemas.ts", "deprecated": false, @@ -1138,7 +1138,7 @@ "label": "getLogger", "description": [], "signature": [ - "(properties: DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => ", + "(properties: DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined) => ", { "pluginId": "eventLog", "scope": "server", @@ -1158,7 +1158,7 @@ "label": "properties", "description": [], "signature": [ - "DeepPartial[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" + "DeepPartial | undefined; uuid?: string | undefined; status_order?: number | undefined; } & {}> | undefined; } & {}> | undefined; } & {}> | undefined; saved_objects?: Readonly<{ type?: string | undefined; id?: string | undefined; rel?: string | undefined; namespace?: string | undefined; type_id?: string | undefined; } & {}>[] | undefined; alerting?: Readonly<{ status?: string | undefined; instance_id?: string | undefined; action_group_id?: string | undefined; action_subgroup?: string | undefined; } & {}> | undefined; server_uuid?: string | undefined; task?: Readonly<{ scheduled?: string | undefined; schedule_delay?: number | undefined; } & {}> | undefined; space_ids?: string[] | undefined; } & {}> | undefined; user?: Readonly<{ name?: string | undefined; } & {}> | undefined; error?: Readonly<{ type?: string | undefined; id?: string | undefined; message?: string | undefined; code?: string | undefined; stack_trace?: string | undefined; } & {}> | undefined; message?: string | undefined; event?: Readonly<{ start?: string | undefined; type?: string[] | undefined; id?: string | undefined; end?: string | undefined; category?: string[] | undefined; url?: string | undefined; code?: string | undefined; original?: string | undefined; action?: string | undefined; kind?: string | undefined; severity?: number | undefined; outcome?: string | undefined; created?: string | undefined; dataset?: string | undefined; duration?: number | undefined; hash?: string | undefined; ingested?: string | undefined; module?: string | undefined; provider?: string | undefined; reason?: string | undefined; reference?: string | undefined; risk_score?: number | undefined; risk_score_norm?: number | undefined; sequence?: number | undefined; timezone?: string | undefined; } & {}> | undefined; rule?: Readonly<{ id?: string | undefined; description?: string | undefined; name?: string | undefined; version?: string | undefined; license?: string | undefined; category?: string | undefined; reference?: string | undefined; author?: string[] | undefined; ruleset?: string | undefined; uuid?: string | undefined; } & {}> | undefined; '@timestamp'?: string | undefined; log?: Readonly<{ logger?: string | undefined; level?: string | undefined; } & {}> | undefined; ecs?: Readonly<{ version?: string | undefined; } & {}> | undefined; } & {}>>> | undefined" ], "path": "x-pack/plugins/event_log/server/types.ts", "deprecated": false, diff --git a/api_docs/expression_metric_vis.json b/api_docs/expression_metric_vis.json new file mode 100644 index 0000000000000..251e916797e7e --- /dev/null +++ b/api_docs/expression_metric_vis.json @@ -0,0 +1,837 @@ +{ + "id": "expressionMetricVis", + "client": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.metricVisFunction", + "type": "Function", + "tags": [], + "label": "metricVisFunction", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.MetricVisExpressionFunctionDefinition", + "text": "MetricVisExpressionFunctionDefinition" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/expression_functions/metric_vis_function.ts", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.DimensionsVisParam", + "type": "Interface", + "tags": [], + "label": "DimensionsVisParam", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.DimensionsVisParam.metrics", + "type": "Array", + "tags": [], + "label": "metrics", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"vis_dimension\", { accessor: number | ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.DatatableColumn", + "text": "DatatableColumn" + }, + "; format: { id?: string | undefined; params: Record; }; }>[]" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.DimensionsVisParam.bucket", + "type": "CompoundType", + "tags": [], + "label": "bucket", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"vis_dimension\", { accessor: number | ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.DatatableColumn", + "text": "DatatableColumn" + }, + "; format: { id?: string | undefined; params: Record; }; }> | undefined" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments", + "type": "Interface", + "tags": [], + "label": "MetricArguments", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.percentageMode", + "type": "boolean", + "tags": [], + "label": "percentageMode", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.colorSchema", + "type": "Enum", + "tags": [], + "label": "colorSchema", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.colorMode", + "type": "CompoundType", + "tags": [], + "label": "colorMode", + "description": [], + "signature": [ + "\"Background\" | \"Labels\" | \"None\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.useRanges", + "type": "boolean", + "tags": [], + "label": "useRanges", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.invertColors", + "type": "boolean", + "tags": [], + "label": "invertColors", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.showLabels", + "type": "boolean", + "tags": [], + "label": "showLabels", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.bgFill", + "type": "string", + "tags": [], + "label": "bgFill", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.subText", + "type": "string", + "tags": [], + "label": "subText", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.colorRange", + "type": "Array", + "tags": [], + "label": "colorRange", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Range", + "text": "Range" + }, + "[]" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.font", + "type": "Object", + "tags": [], + "label": "font", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionTypeStyle", + "text": "ExpressionTypeStyle" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.metric", + "type": "Array", + "tags": [], + "label": "metric", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"vis_dimension\", { accessor: number | ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.DatatableColumn", + "text": "DatatableColumn" + }, + "; format: { id?: string | undefined; params: Record; }; }>[]" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricArguments.bucket", + "type": "CompoundType", + "tags": [], + "label": "bucket", + "description": [], + "signature": [ + "{ type: \"vis_dimension\"; } & { accessor: number | ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.DatatableColumn", + "text": "DatatableColumn" + }, + "; format: { id?: string | undefined; params: Record; }; }" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions", + "type": "Interface", + "tags": [], + "label": "MetricOptions", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.value", + "type": "string", + "tags": [], + "label": "value", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.color", + "type": "string", + "tags": [], + "label": "color", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.bgColor", + "type": "string", + "tags": [], + "label": "bgColor", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.lightText", + "type": "boolean", + "tags": [], + "label": "lightText", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricOptions.rowIndex", + "type": "number", + "tags": [], + "label": "rowIndex", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam", + "type": "Interface", + "tags": [], + "label": "MetricVisParam", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.percentageMode", + "type": "boolean", + "tags": [], + "label": "percentageMode", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.percentageFormatPattern", + "type": "string", + "tags": [], + "label": "percentageFormatPattern", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.useRanges", + "type": "boolean", + "tags": [], + "label": "useRanges", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.colorSchema", + "type": "Enum", + "tags": [], + "label": "colorSchema", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.ColorSchemas", + "text": "ColorSchemas" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.metricColorMode", + "type": "CompoundType", + "tags": [], + "label": "metricColorMode", + "description": [], + "signature": [ + "\"Background\" | \"Labels\" | \"None\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.colorsRange", + "type": "Array", + "tags": [], + "label": "colorsRange", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Range", + "text": "Range" + }, + "[]" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.labels", + "type": "Object", + "tags": [], + "label": "labels", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.Labels", + "text": "Labels" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.invertColors", + "type": "boolean", + "tags": [], + "label": "invertColors", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisParam.style", + "type": "Object", + "tags": [], + "label": "style", + "description": [], + "signature": [ + { + "pluginId": "charts", + "scope": "common", + "docId": "kibChartsPluginApi", + "section": "def-common.Style", + "text": "Style" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisRenderConfig", + "type": "Interface", + "tags": [], + "label": "MetricVisRenderConfig", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisRenderConfig.visType", + "type": "string", + "tags": [], + "label": "visType", + "description": [], + "signature": [ + "\"metric\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisRenderConfig.visData", + "type": "Object", + "tags": [], + "label": "visData", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Datatable", + "text": "Datatable" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisRenderConfig.visConfig", + "type": "Object", + "tags": [], + "label": "visConfig", + "description": [], + "signature": [ + "{ metric: ", + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.MetricVisParam", + "text": "MetricVisParam" + }, + "; dimensions: ", + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.DimensionsVisParam", + "text": "DimensionsVisParam" + }, + "; }" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams", + "type": "Interface", + "tags": [], + "label": "VisParams", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams.addTooltip", + "type": "boolean", + "tags": [], + "label": "addTooltip", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams.addLegend", + "type": "boolean", + "tags": [], + "label": "addLegend", + "description": [], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams.dimensions", + "type": "Object", + "tags": [], + "label": "dimensions", + "description": [], + "signature": [ + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.DimensionsVisParam", + "text": "DimensionsVisParam" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams.metric", + "type": "Object", + "tags": [], + "label": "metric", + "description": [], + "signature": [ + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.MetricVisParam", + "text": "MetricVisParam" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.VisParams.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "\"metric\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_renderers.ts", + "deprecated": false + } + ], + "initialIsOpen": false + } + ], + "enums": [], + "misc": [ + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.EXPRESSION_METRIC_NAME", + "type": "string", + "tags": [], + "label": "EXPRESSION_METRIC_NAME", + "description": [], + "signature": [ + "\"metricVis\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricInput", + "type": "Type", + "tags": [], + "label": "MetricInput", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Datatable", + "text": "Datatable" + } + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.MetricVisExpressionFunctionDefinition", + "type": "Type", + "tags": [], + "label": "MetricVisExpressionFunctionDefinition", + "description": [], + "signature": [ + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionFunctionDefinition", + "text": "ExpressionFunctionDefinition" + }, + "<\"metricVis\", ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.Datatable", + "text": "Datatable" + }, + ", ", + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.MetricArguments", + "text": "MetricArguments" + }, + ", ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExpressionValueBoxed", + "text": "ExpressionValueBoxed" + }, + "<\"render\", { as: string; value: ", + { + "pluginId": "expressionMetricVis", + "scope": "common", + "docId": "kibExpressionMetricVisPluginApi", + "section": "def-common.MetricVisRenderConfig", + "text": "MetricVisRenderConfig" + }, + "; }>, ", + { + "pluginId": "expressions", + "scope": "common", + "docId": "kibExpressionsPluginApi", + "section": "def-common.ExecutionContext", + "text": "ExecutionContext" + }, + "<", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Adapters", + "text": "Adapters" + }, + ", ", + { + "pluginId": "@kbn/utility-types", + "scope": "server", + "docId": "kibKbnUtilityTypesPluginApi", + "section": "def-server.SerializableRecord", + "text": "SerializableRecord" + }, + ">>" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/types/expression_functions.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.PLUGIN_ID", + "type": "string", + "tags": [], + "label": "PLUGIN_ID", + "description": [], + "signature": [ + "\"expressionMetricVis\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/index.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "expressionMetricVis", + "id": "def-common.PLUGIN_NAME", + "type": "string", + "tags": [], + "label": "PLUGIN_NAME", + "description": [], + "signature": [ + "\"expressionMetricVis\"" + ], + "path": "src/plugins/chart_expressions/expression_metric/common/index.ts", + "deprecated": false, + "initialIsOpen": false + } + ], + "objects": [] + } +} \ No newline at end of file diff --git a/api_docs/expression_metric_vis.mdx b/api_docs/expression_metric_vis.mdx new file mode 100644 index 0000000000000..891d4d3f022d5 --- /dev/null +++ b/api_docs/expression_metric_vis.mdx @@ -0,0 +1,33 @@ +--- +id: kibExpressionMetricVisPluginApi +slug: /kibana-dev-docs/api/expressionMetricVis +title: "expressionMetricVis" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the expressionMetricVis plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'expressionMetricVis'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import expressionMetricVisObj from './expression_metric_vis.json'; + +Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. + +Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 49 | 0 | 49 | 0 | + +## Common + +### Functions + + +### Interfaces + + +### Consts, variables and types + + diff --git a/api_docs/fleet.json b/api_docs/fleet.json index b1a9d1bf89488..0bdb078d6f70d 100644 --- a/api_docs/fleet.json +++ b/api_docs/fleet.json @@ -731,6 +731,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -755,6 +779,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -779,6 +827,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -802,6 +874,30 @@ "section": "def-common.EpmPackageAdditions", "text": "EpmPackageAdditions" }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + ">" ], "path": "x-pack/plugins/fleet/public/types/ui_extensions.ts", @@ -2064,40 +2160,6 @@ ], "returnComment": [] }, - { - "parentPluginId": "fleet", - "id": "def-public.pagePathGetters.add_integration_from_policy", - "type": "Function", - "tags": [], - "label": "add_integration_from_policy", - "description": [ - "// TODO: This might need to be removed because we do not have a way to pick an integration in line anymore" - ], - "signature": [ - "({ policyId }: ", - "DynamicPagePathValues", - ") => [string, string]" - ], - "path": "x-pack/plugins/fleet/public/constants/page_paths.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "fleet", - "id": "def-public.pagePathGetters.add_integration_from_policy.$1", - "type": "Object", - "tags": [], - "label": "{ policyId }", - "description": [], - "signature": [ - "DynamicPagePathValues" - ], - "path": "x-pack/plugins/fleet/public/constants/page_paths.ts", - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [] - }, { "parentPluginId": "fleet", "id": "def-public.pagePathGetters.add_integration_to_policy", @@ -6342,6 +6404,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -6366,6 +6452,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -6390,6 +6500,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -6413,6 +6547,30 @@ "section": "def-common.EpmPackageAdditions", "text": "EpmPackageAdditions" }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | (Pick<", { "pluginId": "fleet", @@ -6439,7 +6597,33 @@ "section": "def-common.RegistryPackage", "text": "RegistryPackage" }, - ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })) => boolean" + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"installing\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"install_failed\"; } & { integration?: string | undefined; id: string; })) => boolean" ], "path": "x-pack/plugins/fleet/common/services/packages_with_integrations.ts", "deprecated": false, @@ -6480,8 +6664,8 @@ "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.NotInstalled", - "text": "NotInstalled" + "section": "def-common.Installing", + "text": "Installing" }, "> | (Pick<", + ">> | ", { "pluginId": "fleet", "scope": "common", "docId": "kibFleetPluginApi", - "section": "def-common.RegistryPackage", - "text": "RegistryPackage" + "section": "def-common.Installed", + "text": "Installed" }, - ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"installed\"; savedObject: ", - "SavedObject", - "<", + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.NotInstalled", + "text": "NotInstalled" + }, + "> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"installed\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" }, ">; } & { integration?: string | undefined; id: string; }) | (Pick<", { @@ -6573,7 +6853,33 @@ "section": "def-common.RegistryPackage", "text": "RegistryPackage" }, - ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })" + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"installing\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"install_failed\"; } & { integration?: string | undefined; id: string; })" ], "path": "x-pack/plugins/fleet/common/services/packages_with_integrations.ts", "deprecated": false, @@ -11069,6 +11375,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -11093,6 +11423,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -11117,6 +11471,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -11140,6 +11518,30 @@ "section": "def-common.EpmPackageAdditions", "text": "EpmPackageAdditions" }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + ">" ], "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", @@ -12090,7 +12492,7 @@ "label": "install_status", "description": [], "signature": [ - "\"installed\" | \"installing\"" + "\"installed\" | \"installing\" | \"install_failed\"" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false @@ -15771,6 +16173,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -15795,6 +16221,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -15819,6 +16269,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -15842,6 +16316,30 @@ "section": "def-common.EpmPackageAdditions", "text": "EpmPackageAdditions" }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + ">" ], "path": "x-pack/plugins/fleet/common/types/rest_spec/epm.ts", @@ -17120,7 +17618,7 @@ "label": "EpmPackageInstallStatus", "description": [], "signature": [ - "\"installed\" | \"installing\"" + "\"installed\" | \"installing\" | \"install_failed\"" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false, @@ -17390,6 +17888,14 @@ "text": "Installed" }, " | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + " | ", { "pluginId": "fleet", "scope": "common", @@ -17397,6 +17903,14 @@ "section": "def-common.NotInstalled", "text": "NotInstalled" }, + " | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, "" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", @@ -17439,7 +17953,7 @@ "label": "InstallationStatus", "description": [], "signature": [ - "{ readonly Installed: \"installed\"; readonly NotInstalled: \"not_installed\"; }" + "{ readonly Installed: \"installed\"; readonly Installing: \"installing\"; readonly InstallFailed: \"install_failed\"; readonly NotInstalled: \"not_installed\"; }" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false, @@ -17469,6 +17983,44 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "fleet", + "id": "def-common.InstallFailed", + "type": "Type", + "tags": [], + "label": "InstallFailed", + "description": [], + "signature": [ + "T & { status: \"install_failed\"; }" + ], + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "fleet", + "id": "def-common.Installing", + "type": "Type", + "tags": [], + "label": "Installing", + "description": [], + "signature": [ + "T & { status: \"installing\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; }" + ], + "path": "x-pack/plugins/fleet/common/types/models/epm.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "fleet", "id": "def-common.InstallSource", @@ -17636,6 +18188,14 @@ "section": "def-common.KibanaAssetParts", "text": "KibanaAssetParts" }, + "[]; tag: ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.KibanaAssetParts", + "text": "KibanaAssetParts" + }, "[]; }" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", @@ -17869,6 +18429,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -17893,6 +18477,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -17917,6 +18525,30 @@ "text": "EpmPackageAdditions" }, ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installing", + "text": "Installing" + }, + "> | ", { "pluginId": "fleet", "scope": "common", @@ -17940,6 +18572,30 @@ "section": "def-common.EpmPackageAdditions", "text": "EpmPackageAdditions" }, + ">> | ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.InstallFailed", + "text": "InstallFailed" + }, + ">" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", @@ -18001,7 +18657,33 @@ "section": "def-common.RegistryPackage", "text": "RegistryPackage" }, - ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; })" + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"installing\"; savedObject: ", + "SavedObject", + "<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.Installation", + "text": "Installation" + }, + ">; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"not_installed\"; } & { integration?: string | undefined; id: string; }) | (Pick<", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.RegistryPackage", + "text": "RegistryPackage" + }, + ", \"type\" | \"title\" | \"description\" | \"icons\" | \"name\" | \"version\" | \"path\" | \"download\" | \"internal\" | \"data_streams\" | \"release\" | \"policy_templates\"> & { status: \"install_failed\"; } & { integration?: string | undefined; id: string; })" ], "path": "x-pack/plugins/fleet/common/types/models/epm.ts", "deprecated": false, @@ -18155,6 +18837,22 @@ "section": "def-common.PackageList", "text": "PackageList" }, + "; installing: ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.PackageList", + "text": "PackageList" + }, + "; install_failed: ", + { + "pluginId": "fleet", + "scope": "common", + "docId": "kibFleetPluginApi", + "section": "def-common.PackageList", + "text": "PackageList" + }, "; not_installed: ", { "pluginId": "fleet", @@ -20496,7 +21194,7 @@ "label": "installationStatuses", "description": [], "signature": [ - "{ readonly Installed: \"installed\"; readonly NotInstalled: \"not_installed\"; }" + "{ readonly Installed: \"installed\"; readonly Installing: \"installing\"; readonly InstallFailed: \"install_failed\"; readonly NotInstalled: \"not_installed\"; }" ], "path": "x-pack/plugins/fleet/common/constants/epm.ts", "deprecated": false, diff --git a/api_docs/fleet.mdx b/api_docs/fleet.mdx index bd467d94391f6..88d006982d0f0 100644 --- a/api_docs/fleet.mdx +++ b/api_docs/fleet.mdx @@ -18,7 +18,7 @@ Contact [Fleet](https://github.com/orgs/elastic/teams/fleet) for questions regar | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1207 | 15 | 1106 | 10 | +| 1207 | 15 | 1107 | 10 | ## Client diff --git a/api_docs/interactive_setup.json b/api_docs/interactive_setup.json index b10a3bd9f149a..bfccfd28388c4 100644 --- a/api_docs/interactive_setup.json +++ b/api_docs/interactive_setup.json @@ -265,6 +265,104 @@ } ], "misc": [ + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_CONFIGURE_FAILURE", + "type": "string", + "tags": [], + "label": "ERROR_CONFIGURE_FAILURE", + "description": [], + "signature": [ + "\"configure_failure\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED", + "type": "string", + "tags": [], + "label": "ERROR_ELASTICSEARCH_CONNECTION_CONFIGURED", + "description": [], + "signature": [ + "\"elasticsearch_connection_configured\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_ENROLL_FAILURE", + "type": "string", + "tags": [], + "label": "ERROR_ENROLL_FAILURE", + "description": [], + "signature": [ + "\"enroll_failure\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_KIBANA_CONFIG_FAILURE", + "type": "string", + "tags": [], + "label": "ERROR_KIBANA_CONFIG_FAILURE", + "description": [], + "signature": [ + "\"kibana_config_failure\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_KIBANA_CONFIG_NOT_WRITABLE", + "type": "string", + "tags": [], + "label": "ERROR_KIBANA_CONFIG_NOT_WRITABLE", + "description": [], + "signature": [ + "\"kibana_config_not_writable\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_OUTSIDE_PREBOOT_STAGE", + "type": "string", + "tags": [], + "label": "ERROR_OUTSIDE_PREBOOT_STAGE", + "description": [], + "signature": [ + "\"outside_preboot_stage\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, + { + "parentPluginId": "interactiveSetup", + "id": "def-common.ERROR_PING_FAILURE", + "type": "string", + "tags": [], + "label": "ERROR_PING_FAILURE", + "description": [], + "signature": [ + "\"ping_failure\"" + ], + "path": "src/plugins/interactive_setup/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "interactiveSetup", "id": "def-common.VERIFICATION_CODE_LENGTH", diff --git a/api_docs/interactive_setup.mdx b/api_docs/interactive_setup.mdx index afc6ca7bb7e5a..b954d2c423543 100644 --- a/api_docs/interactive_setup.mdx +++ b/api_docs/interactive_setup.mdx @@ -18,7 +18,7 @@ Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-securit | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 19 | 0 | 9 | 0 | +| 26 | 0 | 16 | 0 | ## Common diff --git a/api_docs/kbn_config.json b/api_docs/kbn_config.json index 71827efb5cea0..f8a49fd358863 100644 --- a/api_docs/kbn_config.json +++ b/api_docs/kbn_config.json @@ -250,6 +250,45 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecationContext", + "type": "Interface", + "tags": [], + "label": "ConfigDeprecationContext", + "description": [ + "\nDeprecation context provided to {@link ConfigDeprecation | config deprecations}\n" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecationContext.version", + "type": "string", + "tags": [], + "label": "version", + "description": [ + "The current Kibana version, e.g `7.16.1`, `8.0.0`" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecationContext.branch", + "type": "string", + "tags": [], + "label": "branch", + "description": [ + "The current Kibana branch, e.g `7.x`, `7.16`, `master`" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/config", "id": "def-server.ConfigDeprecationFactory", @@ -275,7 +314,13 @@ "(deprecatedKey: string, removeBy: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -340,7 +385,13 @@ "(deprecatedKey: string, removeBy: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -405,7 +456,13 @@ "(oldKey: string, newKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -470,7 +527,13 @@ "(oldKey: string, newKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -535,7 +598,13 @@ "(unusedKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -586,7 +655,13 @@ "(unusedKey: string, details?: Partial<", "DeprecatedConfigDetails", "> | undefined) => ", - "ConfigDeprecation" + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + } ], "path": "packages/kbn-config/src/deprecation/types.ts", "deprecated": false, @@ -773,6 +848,123 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation", + "type": "Type", + "tags": [], + "label": "ConfigDeprecation", + "description": [ + "\nConfiguration deprecation returned from {@link ConfigDeprecationProvider} that handles a single deprecation from the configuration.\n" + ], + "signature": [ + "(config: Readonly<{ [x: string]: any; }>, fromPath: string, addDeprecation: ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.AddConfigDeprecation", + "text": "AddConfigDeprecation" + }, + ", context: ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationContext", + "text": "ConfigDeprecationContext" + }, + ") => void | ", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationCommand", + "text": "ConfigDeprecationCommand" + } + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation.$1", + "type": "Object", + "tags": [], + "label": "config", + "description": [ + "must not be mutated, return {@link ConfigDeprecationCommand} to change config shape." + ], + "signature": [ + "{ readonly [x: string]: any; }" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation.$2", + "type": "string", + "tags": [], + "label": "fromPath", + "description": [], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation.$3", + "type": "Function", + "tags": [], + "label": "addDeprecation", + "description": [], + "signature": [ + "(details: ", + "DeprecatedConfigDetails", + ") => void" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false, + "returnComment": [], + "children": [ + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation.$3.$1", + "type": "Object", + "tags": [], + "label": "details", + "description": [], + "signature": [ + "DeprecatedConfigDetails" + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + } + ] + }, + { + "parentPluginId": "@kbn/config", + "id": "def-server.ConfigDeprecation.$4", + "type": "Object", + "tags": [], + "label": "context", + "description": [], + "signature": [ + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecationContext", + "text": "ConfigDeprecationContext" + } + ], + "path": "packages/kbn-config/src/deprecation/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/config", "id": "def-server.ConfigDeprecationProvider", @@ -792,7 +984,13 @@ "text": "ConfigDeprecationFactory" }, ") => ", - "ConfigDeprecation", + { + "pluginId": "@kbn/config", + "scope": "server", + "docId": "kibKbnConfigPluginApi", + "section": "def-server.ConfigDeprecation", + "text": "ConfigDeprecation" + }, "[]" ], "path": "packages/kbn-config/src/deprecation/types.ts", diff --git a/api_docs/kbn_config.mdx b/api_docs/kbn_config.mdx index 94939da9e8d78..4f4058ccd4509 100644 --- a/api_docs/kbn_config.mdx +++ b/api_docs/kbn_config.mdx @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 57 | 0 | 42 | 2 | +| 66 | 0 | 46 | 1 | ## Server diff --git a/api_docs/kbn_es_query.json b/api_docs/kbn_es_query.json index f86c6a78eeb2d..0533bd7756849 100644 --- a/api_docs/kbn_es_query.json +++ b/api_docs/kbn_es_query.json @@ -1725,6 +1725,210 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getDataViewFieldSubtypeMulti", + "type": "Function", + "tags": [], + "label": "getDataViewFieldSubtypeMulti", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeMulti", + "text": "IFieldSubTypeMulti" + }, + " | undefined" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getDataViewFieldSubtypeMulti.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getDataViewFieldSubtypeNested", + "type": "Function", + "tags": [], + "label": "getDataViewFieldSubtypeNested", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => ", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.IFieldSubTypeNested", + "text": "IFieldSubTypeNested" + }, + " | undefined" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.getDataViewFieldSubtypeNested.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.isDataViewFieldSubtypeMulti", + "type": "Function", + "tags": [], + "label": "isDataViewFieldSubtypeMulti", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => boolean" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.isDataViewFieldSubtypeMulti.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.isDataViewFieldSubtypeNested", + "type": "Function", + "tags": [], + "label": "isDataViewFieldSubtypeNested", + "description": [], + "signature": [ + "(field: Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">) => boolean" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.isDataViewFieldSubtypeNested.$1", + "type": "Object", + "tags": [], + "label": "field", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "@kbn/es-query", + "scope": "common", + "docId": "kibKbnEsQueryPluginApi", + "section": "def-common.DataViewFieldBase", + "text": "DataViewFieldBase" + }, + ", \"subType\">" + ], + "path": "packages/kbn-es-query/src/utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.isExistsFilter", @@ -2010,60 +2214,6 @@ ], "initialIsOpen": false }, - { - "parentPluginId": "@kbn/es-query", - "id": "def-common.isMissingFilter", - "type": "Function", - "tags": [], - "label": "isMissingFilter", - "description": [], - "signature": [ - "(filter: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - ") => filter is ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.MissingFilter", - "text": "MissingFilter" - } - ], - "path": "packages/kbn-es-query/src/filters/build_filters/missing_filter.ts", - "deprecated": false, - "children": [ - { - "parentPluginId": "@kbn/es-query", - "id": "def-common.isMissingFilter.$1", - "type": "Object", - "tags": [], - "label": "filter", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - } - ], - "path": "packages/kbn-es-query/src/filters/build_filters/missing_filter.ts", - "deprecated": false, - "isRequired": true - } - ], - "returnComment": [ - "`true` if a filter is an `MissingFilter`" - ], - "initialIsOpen": false - }, { "parentPluginId": "@kbn/es-query", "id": "def-common.isPhraseFilter", @@ -3136,18 +3286,14 @@ { "parentPluginId": "@kbn/es-query", "id": "def-common.DataViewFieldBase.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "packages/kbn-es-query/src/es_query/types.ts", @@ -3279,38 +3425,49 @@ }, { "parentPluginId": "@kbn/es-query", - "id": "def-common.IFieldSubType", + "id": "def-common.IFieldSubTypeMulti", "type": "Interface", "tags": [], - "label": "IFieldSubType", - "description": [ - "\nA field's sub type" - ], + "label": "IFieldSubTypeMulti", + "description": [], "path": "packages/kbn-es-query/src/es_query/types.ts", "deprecated": false, "children": [ { "parentPluginId": "@kbn/es-query", - "id": "def-common.IFieldSubType.multi", + "id": "def-common.IFieldSubTypeMulti.multi", "type": "Object", "tags": [], "label": "multi", "description": [], "signature": [ - "{ parent: string; } | undefined" + "{ parent: string; }" ], "path": "packages/kbn-es-query/src/es_query/types.ts", "deprecated": false - }, + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.IFieldSubTypeNested", + "type": "Interface", + "tags": [], + "label": "IFieldSubTypeNested", + "description": [], + "path": "packages/kbn-es-query/src/es_query/types.ts", + "deprecated": false, + "children": [ { "parentPluginId": "@kbn/es-query", - "id": "def-common.IFieldSubType.nested", + "id": "def-common.IFieldSubTypeNested.nested", "type": "Object", "tags": [], "label": "nested", "description": [], "signature": [ - "{ path: string; } | undefined" + "{ path: string; }" ], "path": "packages/kbn-es-query/src/es_query/types.ts", "deprecated": false @@ -3665,7 +3822,7 @@ "section": "def-common.FilterMeta", "text": "FilterMeta" }, - "; exists?: { field: string; } | undefined; }" + "; query: { exists?: { field: string; } | undefined; }; }" ], "path": "packages/kbn-es-query/src/filters/build_filters/exists_filter.ts", "deprecated": false, @@ -3697,14 +3854,6 @@ "text": "MatchAllFilter" }, " | ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.MissingFilter", - "text": "MissingFilter" - }, - " | ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -3777,6 +3926,24 @@ "deprecated": false, "initialIsOpen": false }, + { + "parentPluginId": "@kbn/es-query", + "id": "def-common.IFieldSubType", + "type": "Type", + "tags": [], + "label": "IFieldSubType", + "description": [ + "\nA field's sub type" + ], + "signature": [ + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional" + ], + "path": "packages/kbn-es-query/src/es_query/types.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/es-query", "id": "def-common.IndexPatternBase", @@ -3840,43 +4007,14 @@ }, " & { meta: ", "MatchAllFilterMeta", - "; match_all: ", + "; query: { match_all: ", "QueryDslMatchAllQuery", - "; }" + "; }; }" ], "path": "packages/kbn-es-query/src/filters/build_filters/match_all_filter.ts", "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "@kbn/es-query", - "id": "def-common.MissingFilter", - "type": "Type", - "tags": [], - "label": "MissingFilter", - "description": [], - "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.Filter", - "text": "Filter" - }, - " & { meta: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.FilterMeta", - "text": "FilterMeta" - }, - "; missing: { field: string; }; }" - ], - "path": "packages/kbn-es-query/src/filters/build_filters/missing_filter.ts", - "deprecated": false, - "initialIsOpen": false - }, { "parentPluginId": "@kbn/es-query", "id": "def-common.PhraseFilter", @@ -3995,7 +4133,7 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; range: { [key: string]: ", + "; query: { range: { [key: string]: ", { "pluginId": "@kbn/es-query", "scope": "common", @@ -4003,7 +4141,7 @@ "section": "def-common.RangeFilterParams", "text": "RangeFilterParams" }, - "; }; }" + "; }; }; }" ], "path": "packages/kbn-es-query/src/filters/build_filters/range_filter.ts", "deprecated": false, @@ -4086,9 +4224,9 @@ "section": "def-common.RangeFilterMeta", "text": "RangeFilterMeta" }, - "; script: { script: ", + "; query: { script: { script: ", "InlineScript", - "; }; }" + "; }; }; }" ], "path": "packages/kbn-es-query/src/filters/build_filters/range_filter.ts", "deprecated": false, diff --git a/api_docs/kbn_es_query.mdx b/api_docs/kbn_es_query.mdx index 0ba50032b3634..be44bd1e5a9a4 100644 --- a/api_docs/kbn_es_query.mdx +++ b/api_docs/kbn_es_query.mdx @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 198 | 2 | 146 | 12 | +| 205 | 2 | 153 | 14 | ## Common diff --git a/api_docs/kbn_logging.json b/api_docs/kbn_logging.json index 32ab4bac1ffac..efb4d27831199 100644 --- a/api_docs/kbn_logging.json +++ b/api_docs/kbn_logging.json @@ -706,7 +706,7 @@ "EcsHttp", " | undefined; log?: Pick<", "EcsLog", - ", \"origin\" | \"original\" | \"file\" | \"syslog\"> | undefined; network?: ", + ", \"origin\" | \"file\" | \"syslog\"> | undefined; network?: ", "EcsNetwork", " | undefined; observer?: ", "EcsObserver", diff --git a/api_docs/kbn_monaco.json b/api_docs/kbn_monaco.json index fd755fda853ec..b87619f900457 100644 --- a/api_docs/kbn_monaco.json +++ b/api_docs/kbn_monaco.json @@ -32,8 +32,8 @@ "pluginId": "@kbn/monaco", "scope": "common", "docId": "kibKbnMonacoPluginApi", - "section": "def-common.LangModule", - "text": "LangModule" + "section": "def-common.LangModuleType", + "text": "LangModuleType" }, ") => void" ], @@ -52,8 +52,8 @@ "pluginId": "@kbn/monaco", "scope": "common", "docId": "kibKbnMonacoPluginApi", - "section": "def-common.LangModule", - "text": "LangModule" + "section": "def-common.LangModuleType", + "text": "LangModuleType" } ], "path": "packages/kbn-monaco/src/helpers.ts", @@ -68,26 +68,26 @@ "interfaces": [ { "parentPluginId": "@kbn/monaco", - "id": "def-common.CompleteLangModule", + "id": "def-common.CompleteLangModuleType", "type": "Interface", "tags": [], - "label": "CompleteLangModule", + "label": "CompleteLangModuleType", "description": [], "signature": [ { "pluginId": "@kbn/monaco", "scope": "common", "docId": "kibKbnMonacoPluginApi", - "section": "def-common.CompleteLangModule", - "text": "CompleteLangModule" + "section": "def-common.CompleteLangModuleType", + "text": "CompleteLangModuleType" }, " extends ", { "pluginId": "@kbn/monaco", "scope": "common", "docId": "kibKbnMonacoPluginApi", - "section": "def-common.LangModule", - "text": "LangModule" + "section": "def-common.LangModuleType", + "text": "LangModuleType" } ], "path": "packages/kbn-monaco/src/types.ts", @@ -95,7 +95,7 @@ "children": [ { "parentPluginId": "@kbn/monaco", - "id": "def-common.CompleteLangModule.languageConfiguration", + "id": "def-common.CompleteLangModuleType.languageConfiguration", "type": "Object", "tags": [], "label": "languageConfiguration", @@ -109,7 +109,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.CompleteLangModule.getSuggestionProvider", + "id": "def-common.CompleteLangModuleType.getSuggestionProvider", "type": "Object", "tags": [], "label": "getSuggestionProvider", @@ -122,7 +122,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.CompleteLangModule.getSyntaxErrors", + "id": "def-common.CompleteLangModuleType.getSyntaxErrors", "type": "Object", "tags": [], "label": "getSyntaxErrors", @@ -138,17 +138,17 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule", + "id": "def-common.LangModuleType", "type": "Interface", "tags": [], - "label": "LangModule", + "label": "LangModuleType", "description": [], "path": "packages/kbn-monaco/src/types.ts", "deprecated": false, "children": [ { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule.ID", + "id": "def-common.LangModuleType.ID", "type": "string", "tags": [], "label": "ID", @@ -158,7 +158,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule.lexerRules", + "id": "def-common.LangModuleType.lexerRules", "type": "Object", "tags": [], "label": "lexerRules", @@ -172,7 +172,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule.languageConfiguration", + "id": "def-common.LangModuleType.languageConfiguration", "type": "Object", "tags": [], "label": "languageConfiguration", @@ -186,7 +186,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule.getSuggestionProvider", + "id": "def-common.LangModuleType.getSuggestionProvider", "type": "Object", "tags": [], "label": "getSuggestionProvider", @@ -199,7 +199,7 @@ }, { "parentPluginId": "@kbn/monaco", - "id": "def-common.LangModule.getSyntaxErrors", + "id": "def-common.LangModuleType.getSyntaxErrors", "type": "Object", "tags": [], "label": "getSyntaxErrors", @@ -245,10 +245,136 @@ } ], "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem", + "type": "Interface", + "tags": [], + "label": "PainlessCompletionItem", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem.label", + "type": "string", + "tags": [], + "label": "label", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem.kind", + "type": "CompoundType", + "tags": [], + "label": "kind", + "description": [], + "signature": [ + "\"type\" | \"keyword\" | \"field\" | \"property\" | \"method\" | \"class\" | \"constructor\"" + ], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem.documentation", + "type": "string", + "tags": [], + "label": "documentation", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem.insertText", + "type": "string", + "tags": [], + "label": "insertText", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionItem.insertTextAsSnippet", + "type": "CompoundType", + "tags": [], + "label": "insertTextAsSnippet", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionResult", + "type": "Interface", + "tags": [], + "label": "PainlessCompletionResult", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionResult.isIncomplete", + "type": "boolean", + "tags": [], + "label": "isIncomplete", + "description": [], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + }, + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionResult.suggestions", + "type": "Array", + "tags": [], + "label": "suggestions", + "description": [], + "signature": [ + { + "pluginId": "@kbn/monaco", + "scope": "common", + "docId": "kibKbnMonacoPluginApi", + "section": "def-common.PainlessCompletionItem", + "text": "PainlessCompletionItem" + }, + "[]" + ], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false } ], "enums": [], "misc": [ + { + "parentPluginId": "@kbn/monaco", + "id": "def-common.PainlessCompletionKind", + "type": "Type", + "tags": [], + "label": "PainlessCompletionKind", + "description": [], + "signature": [ + "\"type\" | \"keyword\" | \"field\" | \"property\" | \"method\" | \"class\" | \"constructor\"" + ], + "path": "packages/kbn-monaco/src/painless/types.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/monaco", "id": "def-common.PainlessContext", diff --git a/api_docs/kbn_monaco.mdx b/api_docs/kbn_monaco.mdx index 4d205f38a7609..08c7f95ee4f43 100644 --- a/api_docs/kbn_monaco.mdx +++ b/api_docs/kbn_monaco.mdx @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 28 | 0 | 28 | 3 | +| 38 | 0 | 38 | 3 | ## Common diff --git a/api_docs/kbn_optimizer.json b/api_docs/kbn_optimizer.json index 2e0fe2fb179ba..e03d89f200ba4 100644 --- a/api_docs/kbn_optimizer.json +++ b/api_docs/kbn_optimizer.json @@ -314,6 +314,61 @@ } ], "functions": [ + { + "parentPluginId": "@kbn/optimizer", + "id": "def-server.logOptimizerProgress", + "type": "Function", + "tags": [], + "label": "logOptimizerProgress", + "description": [], + "signature": [ + "(log: ", + { + "pluginId": "@kbn/dev-utils", + "scope": "server", + "docId": "kibKbnDevUtilsPluginApi", + "section": "def-server.ToolingLog", + "text": "ToolingLog" + }, + ") => ", + "MonoTypeOperatorFunction", + "<", + { + "pluginId": "@kbn/optimizer", + "scope": "server", + "docId": "kibKbnOptimizerPluginApi", + "section": "def-server.OptimizerUpdate", + "text": "OptimizerUpdate" + }, + ">" + ], + "path": "packages/kbn-optimizer/src/log_optimizer_progress.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/optimizer", + "id": "def-server.logOptimizerProgress.$1", + "type": "Object", + "tags": [], + "label": "log", + "description": [], + "signature": [ + { + "pluginId": "@kbn/dev-utils", + "scope": "server", + "docId": "kibKbnDevUtilsPluginApi", + "section": "def-server.ToolingLog", + "text": "ToolingLog" + } + ], + "path": "packages/kbn-optimizer/src/log_optimizer_progress.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/optimizer", "id": "def-server.logOptimizerState", diff --git a/api_docs/kbn_optimizer.mdx b/api_docs/kbn_optimizer.mdx index e59e362121899..e4af57faa7907 100644 --- a/api_docs/kbn_optimizer.mdx +++ b/api_docs/kbn_optimizer.mdx @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 42 | 0 | 42 | 9 | +| 44 | 0 | 44 | 9 | ## Server diff --git a/api_docs/kbn_securitysolution_list_utils.json b/api_docs/kbn_securitysolution_list_utils.json index bfc009f17df0a..5b956218f5ac8 100644 --- a/api_docs/kbn_securitysolution_list_utils.json +++ b/api_docs/kbn_securitysolution_list_utils.json @@ -2774,7 +2774,7 @@ "label": "bool", "description": [], "signature": [ - "{ must?: unknown; must_not?: unknown; should?: unknown[] | undefined; filter?: unknown; minimum_should_match?: number | undefined; }" + "QueryDslBoolQuery" ], "path": "packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts", "deprecated": false @@ -3265,7 +3265,7 @@ "label": "nested", "description": [], "signature": [ - "{ path: string; query: unknown; score_mode: string; }" + "QueryDslNestedQuery" ], "path": "packages/kbn-securitysolution-list-utils/src/build_exception_filter/index.ts", "deprecated": false diff --git a/api_docs/kbn_typed_react_router_config.json b/api_docs/kbn_typed_react_router_config.json index aaa1c025910c6..3f9fa9c74d723 100644 --- a/api_docs/kbn_typed_react_router_config.json +++ b/api_docs/kbn_typed_react_router_config.json @@ -165,7 +165,7 @@ "label": "Outlet", "description": [], "signature": [ - "() => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" + "() => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | null" ], "path": "packages/kbn-typed-react-router-config/src/outlet.tsx", "deprecated": false, @@ -173,6 +173,61 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.OutletContextProvider", + "type": "Function", + "tags": [], + "label": "OutletContextProvider", + "description": [], + "signature": [ + "({\n element,\n children,\n}: { element: React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>; children: React.ReactNode; }) => JSX.Element" + ], + "path": "packages/kbn-typed-react-router-config/src/outlet.tsx", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.OutletContextProvider.$1", + "type": "Object", + "tags": [], + "label": "{\n element,\n children,\n}", + "description": [], + "path": "packages/kbn-typed-react-router-config/src/outlet.tsx", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.OutletContextProvider.$1.element", + "type": "Object", + "tags": [], + "label": "element", + "description": [], + "signature": [ + "React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" + ], + "path": "packages/kbn-typed-react-router-config/src/outlet.tsx", + "deprecated": false + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.OutletContextProvider.$1.children", + "type": "CompoundType", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "string | number | boolean | {} | React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | React.ReactNodeArray | React.ReactPortal | null | undefined" + ], + "path": "packages/kbn-typed-react-router-config/src/outlet.tsx", + "deprecated": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "@kbn/typed-react-router-config", "id": "def-common.route", @@ -236,7 +291,7 @@ "section": "def-common.Route", "text": "Route" }, - "[]>; children: React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>; }) => JSX.Element" + "[]>; children: React.ReactNode; }) => JSX.Element" ], "path": "packages/kbn-typed-react-router-config/src/use_router.tsx", "deprecated": false, @@ -282,12 +337,12 @@ { "parentPluginId": "@kbn/typed-react-router-config", "id": "def-common.RouterContextProvider.$1.children", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "children", "description": [], "signature": [ - "React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" + "string | number | boolean | {} | React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | React.ReactNodeArray | React.ReactPortal | null | undefined" ], "path": "packages/kbn-typed-react-router-config/src/use_router.tsx", "deprecated": false @@ -340,7 +395,7 @@ }, "[]>; history: ", "History", - "; children: React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>; }) => JSX.Element" + "; children: React.ReactNode; }) => JSX.Element" ], "path": "packages/kbn-typed-react-router-config/src/router_provider.tsx", "deprecated": false, @@ -400,12 +455,12 @@ { "parentPluginId": "@kbn/typed-react-router-config", "id": "def-common.RouterProvider.$1.children", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "children", "description": [], "signature": [ - "React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" + "string | number | boolean | {} | React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | React.ReactNodeArray | React.ReactPortal | null | undefined" ], "path": "packages/kbn-typed-react-router-config/src/router_provider.tsx", "deprecated": false @@ -2027,6 +2082,44 @@ } ], "returnComment": [] + }, + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.Router.getRoutesToMatch", + "type": "Function", + "tags": [], + "label": "getRoutesToMatch", + "description": [], + "signature": [ + "(path: string) => ", + { + "pluginId": "@kbn/typed-react-router-config", + "scope": "common", + "docId": "kibKbnTypedReactRouterConfigPluginApi", + "section": "def-common.FlattenRoutesOf", + "text": "FlattenRoutesOf" + }, + "" + ], + "path": "packages/kbn-typed-react-router-config/src/types/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.Router.getRoutesToMatch.$1", + "type": "string", + "tags": [], + "label": "path", + "description": [], + "signature": [ + "string" + ], + "path": "packages/kbn-typed-react-router-config/src/types/index.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] } ], "initialIsOpen": false @@ -2034,6 +2127,24 @@ ], "enums": [], "misc": [ + { + "parentPluginId": "@kbn/typed-react-router-config", + "id": "def-common.FlattenRoutesOf", + "type": "Type", + "tags": [], + "label": "FlattenRoutesOf", + "description": [], + "signature": [ + "Pick<", + "ValuesType", + ">, Exclude>, \"parents\">>[]" + ], + "path": "packages/kbn-typed-react-router-config/src/types/index.ts", + "deprecated": false, + "initialIsOpen": false + }, { "parentPluginId": "@kbn/typed-react-router-config", "id": "def-common.Match", diff --git a/api_docs/kbn_typed_react_router_config.mdx b/api_docs/kbn_typed_react_router_config.mdx index 9a665ee8e676f..a25fa737996c5 100644 --- a/api_docs/kbn_typed_react_router_config.mdx +++ b/api_docs/kbn_typed_react_router_config.mdx @@ -18,7 +18,7 @@ Contact [Owner missing] for questions regarding this plugin. | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 71 | 0 | 71 | 1 | +| 78 | 0 | 78 | 1 | ## Common diff --git a/api_docs/kibana_utils.json b/api_docs/kibana_utils.json index 6411b8b7e1cee..744c735e94d68 100644 --- a/api_docs/kibana_utils.json +++ b/api_docs/kibana_utils.json @@ -1486,7 +1486,7 @@ "label": "createGetterSetter", "description": [], "signature": [ - "(name: string) => [", + "(name: string, isValueRequired?: boolean) => [", { "pluginId": "kibanaUtils", "scope": "common", @@ -1520,6 +1520,20 @@ "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", "deprecated": false, "isRequired": true + }, + { + "parentPluginId": "kibanaUtils", + "id": "def-public.createGetterSetter.$2", + "type": "boolean", + "tags": [], + "label": "isValueRequired", + "description": [], + "signature": [ + "boolean" + ], + "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", + "deprecated": false, + "isRequired": true } ], "returnComment": [], @@ -7113,7 +7127,7 @@ "label": "createGetterSetter", "description": [], "signature": [ - "(name: string) => [", + "(name: string, isValueRequired?: boolean) => [", { "pluginId": "kibanaUtils", "scope": "common", @@ -7147,6 +7161,20 @@ "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", "deprecated": false, "isRequired": true + }, + { + "parentPluginId": "kibanaUtils", + "id": "def-server.createGetterSetter.$2", + "type": "boolean", + "tags": [], + "label": "isValueRequired", + "description": [], + "signature": [ + "boolean" + ], + "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", + "deprecated": false, + "isRequired": true } ], "returnComment": [], @@ -8686,7 +8714,7 @@ "label": "createGetterSetter", "description": [], "signature": [ - "(name: string) => [", + "(name: string, isValueRequired?: boolean) => [", { "pluginId": "kibanaUtils", "scope": "common", @@ -8720,6 +8748,20 @@ "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", "deprecated": false, "isRequired": true + }, + { + "parentPluginId": "kibanaUtils", + "id": "def-common.createGetterSetter.$2", + "type": "boolean", + "tags": [], + "label": "isValueRequired", + "description": [], + "signature": [ + "boolean" + ], + "path": "src/plugins/kibana_utils/common/create_getter_setter.ts", + "deprecated": false, + "isRequired": true } ], "returnComment": [], diff --git a/api_docs/kibana_utils.mdx b/api_docs/kibana_utils.mdx index b428f08aca7e1..807999db2f4e9 100644 --- a/api_docs/kibana_utils.mdx +++ b/api_docs/kibana_utils.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 603 | 3 | 410 | 8 | +| 606 | 3 | 413 | 8 | ## Client diff --git a/api_docs/lens.json b/api_docs/lens.json index 9d9591af006ac..18f6f3320f624 100644 --- a/api_docs/lens.json +++ b/api_docs/lens.json @@ -1807,6 +1807,19 @@ ], "path": "x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts", "deprecated": false + }, + { + "parentPluginId": "lens", + "id": "def-public.YConfig.textVisibility", + "type": "CompoundType", + "tags": [], + "label": "textVisibility", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts", + "deprecated": false } ], "initialIsOpen": false diff --git a/api_docs/lens.mdx b/api_docs/lens.mdx index 6165c3ec65d5d..9232e5da09428 100644 --- a/api_docs/lens.mdx +++ b/api_docs/lens.mdx @@ -18,7 +18,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 252 | 0 | 234 | 24 | +| 253 | 0 | 235 | 24 | ## Client diff --git a/api_docs/maps.json b/api_docs/maps.json index b5ff309ccbbbf..3b4551b099afb 100644 --- a/api_docs/maps.json +++ b/api_docs/maps.json @@ -2962,7 +2962,7 @@ "section": "def-public.LayerWizard", "text": "LayerWizard" }, - ") => Promise" + ") => void" ], "path": "x-pack/plugins/maps/public/api/setup_api.ts", "deprecated": false, @@ -3000,7 +3000,7 @@ "signature": [ "(entry: ", "SourceRegistryEntry", - ") => Promise" + ") => void" ], "path": "x-pack/plugins/maps/public/api/setup_api.ts", "deprecated": false, diff --git a/api_docs/observability.json b/api_docs/observability.json index 5887895476e3e..aa66c500c05e5 100644 --- a/api_docs/observability.json +++ b/api_docs/observability.json @@ -238,9 +238,11 @@ "label": "FilterValueLabel", "description": [], "signature": [ - "({\n label,\n field,\n value,\n negate,\n indexPattern,\n invertFilter,\n removeFilter,\n allowExclusion = true,\n}: Props) => JSX.Element | null" + "(props: ", + "FilterValueLabelProps", + ") => JSX.Element" ], - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx", + "path": "x-pack/plugins/observability/public/components/shared/index.tsx", "deprecated": false, "children": [ { @@ -248,12 +250,12 @@ "id": "def-public.FilterValueLabel.$1", "type": "Object", "tags": [], - "label": "{\n label,\n field,\n value,\n negate,\n indexPattern,\n invertFilter,\n removeFilter,\n allowExclusion = true,\n}", + "label": "props", "description": [], "signature": [ - "Props" + "FilterValueLabelProps" ], - "path": "x-pack/plugins/observability/public/components/shared/filter_value_label/filter_value_label.tsx", + "path": "x-pack/plugins/observability/public/components/shared/index.tsx", "deprecated": false, "isRequired": true } @@ -386,6 +388,48 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "observability", + "id": "def-public.InspectorContextProvider", + "type": "Function", + "tags": [], + "label": "InspectorContextProvider", + "description": [], + "signature": [ + "({ children }: { children: React.ReactNode; }) => JSX.Element" + ], + "path": "x-pack/plugins/observability/public/context/inspector/inspector_context.tsx", + "deprecated": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-public.InspectorContextProvider.$1", + "type": "Object", + "tags": [], + "label": "{ children }", + "description": [], + "path": "x-pack/plugins/observability/public/context/inspector/inspector_context.tsx", + "deprecated": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-public.InspectorContextProvider.$1.children", + "type": "CompoundType", + "tags": [], + "label": "children", + "description": [], + "signature": [ + "string | number | boolean | {} | React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)> | React.ReactNodeArray | React.ReactPortal | null | undefined" + ], + "path": "x-pack/plugins/observability/public/context/inspector/inspector_context.tsx", + "deprecated": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "observability", "id": "def-public.LazyAlertsFlyout", @@ -833,6 +877,23 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "observability", + "id": "def-public.useInspectorContext", + "type": "Function", + "tags": [], + "label": "useInspectorContext", + "description": [], + "signature": [ + "() => ", + "InspectorContextValue" + ], + "path": "x-pack/plugins/observability/public/context/inspector/use_inspector_context.tsx", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "observability", "id": "def-public.useTheme", @@ -3660,6 +3721,173 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse", + "type": "Function", + "tags": [], + "label": "getInspectResponse", + "description": [ + "\nCreate a formatted response to be sent in the _inspect key for use in the\ninspector." + ], + "signature": [ + "({\n esError,\n esRequestParams,\n esRequestStatus,\n esResponse,\n kibanaRequest,\n operationName,\n startTime,\n}: { esError: ", + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.WrappedElasticsearchClientError", + "text": "WrappedElasticsearchClientError" + }, + " | null; esRequestParams: Record; esRequestStatus: ", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.RequestStatus", + "text": "RequestStatus" + }, + "; esResponse: any; kibanaRequest: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "; operationName: string; startTime: number; }) => ", + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.Request", + "text": "Request" + } + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1", + "type": "Object", + "tags": [], + "label": "{\n esError,\n esRequestParams,\n esRequestStatus,\n esResponse,\n kibanaRequest,\n operationName,\n startTime,\n}", + "description": [], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.esError", + "type": "CompoundType", + "tags": [], + "label": "esError", + "description": [], + "signature": [ + { + "pluginId": "observability", + "scope": "server", + "docId": "kibObservabilityPluginApi", + "section": "def-server.WrappedElasticsearchClientError", + "text": "WrappedElasticsearchClientError" + }, + " | null" + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.esRequestParams", + "type": "Object", + "tags": [], + "label": "esRequestParams", + "description": [], + "signature": [ + "{ [x: string]: any; }" + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.esRequestStatus", + "type": "Enum", + "tags": [], + "label": "esRequestStatus", + "description": [], + "signature": [ + { + "pluginId": "inspector", + "scope": "common", + "docId": "kibInspectorPluginApi", + "section": "def-common.RequestStatus", + "text": "RequestStatus" + } + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.esResponse", + "type": "Any", + "tags": [], + "label": "esResponse", + "description": [], + "signature": [ + "any" + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.kibanaRequest", + "type": "Object", + "tags": [], + "label": "kibanaRequest", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreHttpPluginApi", + "section": "def-server.KibanaRequest", + "text": "KibanaRequest" + }, + "" + ], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.operationName", + "type": "string", + "tags": [], + "label": "operationName", + "description": [], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + }, + { + "parentPluginId": "observability", + "id": "def-server.getInspectResponse.$1.startTime", + "type": "number", + "tags": [], + "label": "startTime", + "description": [], + "path": "x-pack/plugins/observability/server/utils/get_inspect_response.ts", + "deprecated": false + } + ] + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "observability", "id": "def-server.kqlQuery", diff --git a/api_docs/observability.mdx b/api_docs/observability.mdx index 4b7ac03a70e14..57228c938c386 100644 --- a/api_docs/observability.mdx +++ b/api_docs/observability.mdx @@ -18,7 +18,7 @@ Contact [Observability UI](https://github.com/orgs/elastic/teams/observability-u | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 245 | 0 | 245 | 10 | +| 258 | 1 | 257 | 12 | ## Client diff --git a/api_docs/plugin_directory.mdx b/api_docs/plugin_directory.mdx index ab85e02919102..533a86b9e806f 100644 --- a/api_docs/plugin_directory.mdx +++ b/api_docs/plugin_directory.mdx @@ -12,13 +12,13 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Count | Plugins or Packages with a
public API | Number of teams | |--------------|----------|------------------------| -| 201 | 156 | 32 | +| 202 | 158 | 32 | ### Public API health stats | API Count | Any Count | Missing comments | Missing exports | |--------------|----------|-----------------|--------| -| 24158 | 274 | 19590 | 1579 | +| 24409 | 277 | 19821 | 1585 | ## Plugin Directory @@ -27,23 +27,23 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Alerting](https://github.com/orgs/elastic/teams/kibana-alerting-services) | - | 125 | 0 | 125 | 8 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 23 | 0 | 22 | 1 | | | [Kibana Alerting](https://github.com/orgs/elastic/teams/kibana-alerting-services) | - | 249 | 0 | 241 | 17 | -| | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | - | 42 | 0 | 42 | 37 | +| | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | The user interface for Elastic APM | 42 | 0 | 42 | 37 | | | [APM UI](https://github.com/orgs/elastic/teams/apm-ui) | - | 6 | 0 | 6 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 9 | 0 | 9 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Considering using bfetch capabilities when fetching large amounts of data. This services supports batching HTTP requests and streaming responses back. | 77 | 1 | 66 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Canvas application to Kibana | 9 | 0 | 8 | 3 | | | [Security Solution Threat Hunting](https://github.com/orgs/elastic/teams/security-threat-hunting) | The Case management system in Kibana | 475 | 0 | 431 | 14 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 223 | 2 | 192 | 3 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 285 | 4 | 253 | 3 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 22 | 0 | 22 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 9 | 0 | 9 | 1 | -| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2293 | 27 | 1020 | 29 | +| | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 2300 | 27 | 1019 | 29 | | crossClusterReplication | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Fleet](https://github.com/orgs/elastic/teams/fleet) | Add custom data integrations so they can be displayed in the Fleet integrations app | 85 | 1 | 78 | 1 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds the Dashboard app to Kibana | 145 | 1 | 132 | 10 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 51 | 0 | 50 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3181 | 43 | 2796 | 48 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 3192 | 43 | 2807 | 48 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Enhanced data plugin. (See src/plugins/data.) Enhances the main data plugin with a search session management UI. Includes a reusable search session indicator component to use in other applications. Exposes routes for managing search sessions. Includes a service that monitors, updates, and cleans up search session saved objects. | 16 | 0 | 16 | 2 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 671 | 6 | 531 | 5 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Data services are useful for searching and querying data from Elasticsearch. Helpful utilities include: a re-usable react query bar, KQL autocomplete, async search, Data Views (Index Patterns) and field formatters. | 681 | 6 | 541 | 5 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The Data Visualizer tools help you understand your data, by analyzing the metrics and fields in a log file or an existing Elasticsearch index. | 80 | 5 | 80 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 10 | 0 | 8 | 2 | | | [Data Discovery](https://github.com/orgs/elastic/teams/kibana-data-discovery) | This plugin contains the Discover application and the saved search embeddable. | 82 | 0 | 56 | 6 | @@ -57,6 +57,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'error' renderer to expressions | 12 | 0 | 12 | 2 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'image' function and renderer to expressions | 24 | 0 | 24 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'metric' function and renderer to expressions | 30 | 0 | 25 | 0 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Expression MetricVis plugin adds a `metric` renderer and function to the expression plugin. The renderer will display the `metric` chart. | 49 | 0 | 49 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'repeatImage' function and renderer to expressions | 30 | 0 | 30 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'revealImage' function and renderer to expressions | 12 | 0 | 12 | 3 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds 'shape' function and renderer to expressions | 143 | 0 | 143 | 0 | @@ -65,7 +66,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 216 | 0 | 98 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Index pattern fields and ambiguous values formatters | 288 | 7 | 250 | 3 | | | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | The file upload plugin contains components and services for uploading a file, analyzing its data, and then importing the data into an Elasticsearch index. Supported file types include CSV, TSV, newline-delimited JSON and GeoJSON. | 129 | 4 | 129 | 1 | -| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1207 | 15 | 1106 | 10 | +| | [Fleet](https://github.com/orgs/elastic/teams/fleet) | - | 1207 | 15 | 1107 | 10 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 68 | 0 | 14 | 5 | | globalSearchBar | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | | globalSearchProviders | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -81,13 +82,13 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | ingestPipelines | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | inputControlVis | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | Adds Input Control visualization to Kibana | 0 | 0 | 0 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 123 | 6 | 96 | 4 | -| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides UI and APIs for the interactive setup mode. | 19 | 0 | 9 | 0 | +| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides UI and APIs for the interactive setup mode. | 26 | 0 | 16 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | - | 48 | 1 | 45 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 6 | 0 | 6 | 0 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 297 | 8 | 260 | 5 | | kibanaUsageCollection | [Kibana Telemtry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 0 | 0 | 0 | 0 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 603 | 3 | 410 | 8 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 252 | 0 | 234 | 24 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 606 | 3 | 413 | 8 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana. | 253 | 0 | 235 | 24 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 8 | 0 | 8 | 0 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 3 | 0 | 3 | 0 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 117 | 0 | 42 | 8 | @@ -101,7 +102,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Stack Monitoring](https://github.com/orgs/elastic/teams/stack-monitoring-ui) | - | 10 | 0 | 10 | 2 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 31 | 0 | 31 | 2 | | | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 17 | 0 | 17 | 0 | -| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 245 | 0 | 245 | 10 | +| | [Observability UI](https://github.com/orgs/elastic/teams/observability-ui) | - | 258 | 1 | 257 | 12 | | | [Security asset management](https://github.com/orgs/elastic/teams/security-asset-management) | - | 11 | 0 | 11 | 0 | | painlessLab | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Kibana Presentation](https://github.com/orgs/elastic/teams/kibana-presentation) | The Presentation Utility Plugin is a set of common, shared components and toolkits for solutions within the Presentation space, (e.g. Dashboards, Canvas). | 178 | 3 | 151 | 5 | @@ -117,11 +118,10 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | - | 22 | 0 | 17 | 1 | | searchprofiler | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides authentication and authorization features, and exposes functionality to understand the capabilities of the currently authenticated user. | 113 | 0 | 51 | 7 | -| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin exposes a limited set of security functionality to OSS plugins. | 12 | 0 | 9 | 3 | -| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 1353 | 8 | 1299 | 29 | -| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 137 | 1 | 90 | 10 | +| | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 1361 | 8 | 1307 | 30 | +| | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds URL Service and sharing capabilities to Kibana | 143 | 1 | 90 | 10 | | | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 23 | 1 | 22 | 1 | -| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 205 | 0 | 21 | 2 | +| | [Platform Security](https://github.com/orgs/elastic/teams/kibana-security) | This plugin provides the Spaces feature, which allows saved objects to be organized into meaningful categories. | 208 | 0 | 21 | 1 | | | [Kibana Alerting](https://github.com/orgs/elastic/teams/kibana-alerting-services) | - | 4 | 0 | 4 | 0 | | | [Kibana Alerting](https://github.com/orgs/elastic/teams/kibana-alerting-services) | - | 70 | 0 | 32 | 7 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 41 | 0 | 0 | 0 | @@ -129,7 +129,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 1 | 0 | 1 | 0 | | | [Kibana Telemetry](https://github.com/orgs/elastic/teams/kibana-telemetry) | - | 14 | 0 | 13 | 0 | | | [Security solution](https://github.com/orgs/elastic/teams/security-solution) | - | 968 | 6 | 847 | 25 | -| transform | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 0 | 0 | 0 | 0 | +| | [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) | This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. | 4 | 0 | 4 | 1 | | translations | [Kibana Localization](https://github.com/orgs/elastic/teams/kibana-localization) | - | 0 | 0 | 0 | 0 | | | [Kibana Alerting](https://github.com/orgs/elastic/teams/kibana-alerting-services) | - | 239 | 1 | 230 | 18 | | | [App Services](https://github.com/orgs/elastic/teams/kibana-app-services) | Adds UI Actions service to Kibana | 127 | 0 | 88 | 11 | @@ -150,7 +150,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Registers the vega visualization. Is the elastic version of vega and vega-lite libraries. | 2 | 0 | 2 | 0 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and heatmap charts. We want to replace them with elastic-charts. | 26 | 0 | 25 | 1 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the new xy-axis chart using the elastic-charts library, which will eventually replace the vislib xy-axis charts including bar, area, and line. | 57 | 0 | 51 | 5 | -| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 275 | 13 | 257 | 15 | +| | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable. | 304 | 13 | 286 | 16 | | | [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Contains the visualize application which includes the listing page and the app frame, which will load the visualization's editor. | 24 | 0 | 23 | 1 | | watcher | [Stack Management](https://github.com/orgs/elastic/teams/kibana-stack-management) | - | 0 | 0 | 0 | 0 | | xpackLegacy | [Kibana Core](https://github.com/orgs/elastic/teams/kibana-core) | - | 0 | 0 | 0 | 0 | @@ -159,6 +159,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | Package name           | Maintaining team | Description | API Cnt | Any Cnt | Missing
comments | Missing
exports | |--------------|----------------|-----------|--------------|----------|---------------|--------| +| | [Owner missing] | Elastic APM trace data generator | 15 | 0 | 15 | 2 | | | [Owner missing] | elasticsearch datemath parser, used in kibana | 44 | 0 | 43 | 0 | | | [Owner missing] | - | 11 | 5 | 11 | 0 | | | [Owner missing] | Alerts components and hooks | 9 | 1 | 9 | 0 | @@ -166,20 +167,20 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 14 | 0 | 14 | 0 | | | [Owner missing] | - | 11 | 0 | 11 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | -| | [Owner missing] | - | 57 | 0 | 42 | 2 | +| | [Owner missing] | - | 66 | 0 | 46 | 1 | | | [Owner missing] | - | 109 | 3 | 107 | 18 | | | [Owner missing] | - | 13 | 0 | 7 | 0 | | | [Owner missing] | - | 258 | 7 | 231 | 4 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 25 | 0 | 12 | 1 | -| | [Owner missing] | - | 198 | 2 | 146 | 12 | +| | [Owner missing] | - | 205 | 2 | 153 | 14 | | | [Owner missing] | - | 20 | 0 | 16 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 2 | | | [Owner missing] | - | 18 | 0 | 18 | 2 | | | [Owner missing] | - | 30 | 0 | 5 | 37 | | | [Owner missing] | - | 467 | 9 | 378 | 0 | -| | [Owner missing] | - | 28 | 0 | 28 | 3 | -| | [Owner missing] | - | 42 | 0 | 42 | 9 | +| | [Owner missing] | - | 38 | 0 | 38 | 3 | +| | [Owner missing] | - | 44 | 0 | 44 | 9 | | | [Owner missing] | - | 1 | 0 | 1 | 0 | | | [Owner missing] | Just some helpers for kibana plugin devs. | 1 | 0 | 1 | 0 | | | [Owner missing] | - | 63 | 0 | 49 | 5 | @@ -203,7 +204,7 @@ warning: This document is auto-generated and is meant to be viewed inside our ex | | [Owner missing] | - | 18 | 1 | 18 | 0 | | | [Owner missing] | - | 2 | 0 | 2 | 0 | | | [Owner missing] | - | 200 | 5 | 177 | 9 | -| | [Owner missing] | - | 71 | 0 | 71 | 1 | +| | [Owner missing] | - | 78 | 0 | 78 | 1 | | | [Owner missing] | - | 29 | 1 | 10 | 1 | | | [Owner missing] | - | 31 | 1 | 21 | 0 | diff --git a/api_docs/saved_objects.json b/api_docs/saved_objects.json index b4e8c07d33ca2..af63ad120a8c0 100644 --- a/api_docs/saved_objects.json +++ b/api_docs/saved_objects.json @@ -2118,14 +2118,6 @@ "plugin": "discover", "path": "src/plugins/discover/public/application/apps/main/discover_main_route.tsx" }, - { - "plugin": "visualizations", - "path": "src/plugins/visualizations/public/types.ts" - }, - { - "plugin": "visualizations", - "path": "src/plugins/visualizations/public/types.ts" - }, { "plugin": "visualizations", "path": "src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts" diff --git a/api_docs/security_solution.json b/api_docs/security_solution.json index fe6a5cbb9858d..ebff5a94fbe2c 100644 --- a/api_docs/security_solution.json +++ b/api_docs/security_solution.json @@ -412,7 +412,9 @@ "label": "config", "description": [], "signature": [ - "Readonly<{} & { enabled: boolean; signalsIndex: string; maxRuleImportExportSize: number; maxRuleImportPayloadBytes: number; maxTimelineImportExportSize: number; maxTimelineImportPayloadBytes: number; alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; alertIgnoreFields: string[]; enableExperimental: string[]; endpointResultListDefaultFirstPageIndex: number; endpointResultListDefaultPageSize: number; packagerTaskInterval: string; prebuiltRulesFromFileSystem: boolean; prebuiltRulesFromSavedObjects: boolean; }>" + "Readonly<{} & { enabled: boolean; signalsIndex: string; maxRuleImportExportSize: number; maxRuleImportPayloadBytes: number; maxTimelineImportExportSize: number; maxTimelineImportPayloadBytes: number; alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; alertIgnoreFields: string[]; enableExperimental: string[]; ruleExecutionLog: Readonly<{} & { underlyingClient: ", + "UnderlyingLogClient", + "; }>; endpointResultListDefaultFirstPageIndex: number; endpointResultListDefaultPageSize: number; packagerTaskInterval: string; prebuiltRulesFromFileSystem: boolean; prebuiltRulesFromSavedObjects: boolean; }>" ], "path": "x-pack/plugins/security_solution/server/client/client.ts", "deprecated": false, @@ -845,7 +847,9 @@ "label": "ConfigType", "description": [], "signature": [ - "{ readonly enabled: boolean; readonly signalsIndex: string; readonly maxRuleImportExportSize: number; readonly maxRuleImportPayloadBytes: number; readonly maxTimelineImportExportSize: number; readonly maxTimelineImportPayloadBytes: number; readonly alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; readonly alertIgnoreFields: string[]; readonly enableExperimental: string[]; readonly endpointResultListDefaultFirstPageIndex: number; readonly endpointResultListDefaultPageSize: number; readonly packagerTaskInterval: string; readonly prebuiltRulesFromFileSystem: boolean; readonly prebuiltRulesFromSavedObjects: boolean; }" + "{ readonly enabled: boolean; readonly signalsIndex: string; readonly maxRuleImportExportSize: number; readonly maxRuleImportPayloadBytes: number; readonly maxTimelineImportExportSize: number; readonly maxTimelineImportPayloadBytes: number; readonly alertMergeStrategy: \"allFields\" | \"missingFields\" | \"noFields\"; readonly alertIgnoreFields: string[]; readonly enableExperimental: string[]; readonly ruleExecutionLog: Readonly<{} & { underlyingClient: ", + "UnderlyingLogClient", + "; }>; readonly endpointResultListDefaultFirstPageIndex: number; readonly endpointResultListDefaultPageSize: number; readonly packagerTaskInterval: string; readonly prebuiltRulesFromFileSystem: boolean; readonly prebuiltRulesFromSavedObjects: boolean; }" ], "path": "x-pack/plugins/security_solution/server/config.ts", "deprecated": false, @@ -2437,12 +2441,15 @@ { "parentPluginId": "securitySolution", "id": "def-common.BrowserField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - "{ [key: string]: unknown; nested?: { path: string; } | undefined; } | undefined" + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined" ], "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", "deprecated": false @@ -2942,13 +2949,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", @@ -7725,18 +7728,194 @@ }, { "parentPluginId": "securitySolution", - "id": "def-common.HostsRiskyHostsStrategyResponse", + "id": "def-common.HostsRiskScore", + "type": "Interface", + "tags": [], + "label": "HostsRiskScore", + "description": [], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScore.host", + "type": "Object", + "tags": [], + "label": "host", + "description": [], + "signature": [ + "{ name: string; }" + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScore.risk_score", + "type": "number", + "tags": [], + "label": "risk_score", + "description": [], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScore.risk", + "type": "string", + "tags": [], + "label": "risk", + "description": [], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreRequestOptions", + "type": "Interface", + "tags": [], + "label": "HostsRiskScoreRequestOptions", + "description": [], + "signature": [ + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.HostsRiskScoreRequestOptions", + "text": "HostsRiskScoreRequestOptions" + }, + " extends ", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.IEsSearchRequest", + "text": "IEsSearchRequest" + } + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreRequestOptions.defaultIndex", + "type": "Array", + "tags": [], + "label": "defaultIndex", + "description": [], + "signature": [ + "string[]" + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreRequestOptions.factoryQueryType", + "type": "CompoundType", + "tags": [], + "label": "factoryQueryType", + "description": [], + "signature": [ + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.HostsQueries", + "text": "HostsQueries" + }, + " | ", + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.HostsKpiQueries", + "text": "HostsKpiQueries" + }, + " | ", + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.UebaQueries", + "text": "UebaQueries" + }, + " | ", + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.NetworkQueries", + "text": "NetworkQueries" + }, + " | ", + { + "pluginId": "securitySolution", + "scope": "common", + "docId": "kibSecuritySolutionPluginApi", + "section": "def-common.NetworkKpiQueries", + "text": "NetworkKpiQueries" + }, + " | ", + "CtiQueries", + " | \"matrixHistogram\" | \"matrixHistogramEntities\" | undefined" + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreRequestOptions.hostName", + "type": "string", + "tags": [], + "label": "hostName", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreRequestOptions.timerange", + "type": "Object", + "tags": [], + "label": "timerange", + "description": [], + "signature": [ + { + "pluginId": "timelines", + "scope": "common", + "docId": "kibTimelinesPluginApi", + "section": "def-common.TimerangeInput", + "text": "TimerangeInput" + }, + " | undefined" + ], + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "securitySolution", + "id": "def-common.HostsRiskScoreStrategyResponse", "type": "Interface", "tags": [], - "label": "HostsRiskyHostsStrategyResponse", + "label": "HostsRiskScoreStrategyResponse", "description": [], "signature": [ { "pluginId": "securitySolution", "scope": "common", "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.HostsRiskyHostsStrategyResponse", - "text": "HostsRiskyHostsStrategyResponse" + "section": "def-common.HostsRiskScoreStrategyResponse", + "text": "HostsRiskScoreStrategyResponse" }, " extends ", { @@ -7748,12 +7927,12 @@ }, "" ], - "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts", + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", "deprecated": false, "children": [ { "parentPluginId": "securitySolution", - "id": "def-common.HostsRiskyHostsStrategyResponse.inspect", + "id": "def-common.HostsRiskScoreStrategyResponse.inspect", "type": "CompoundType", "tags": [], "label": "inspect", @@ -7768,7 +7947,7 @@ }, " | null | undefined" ], - "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts", + "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts", "deprecated": false } ], @@ -8937,18 +9116,14 @@ { "parentPluginId": "securitySolution", "id": "def-common.IndexField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts", @@ -19263,13 +19438,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", @@ -19832,26 +20003,6 @@ "deprecated": false, "initialIsOpen": false }, - { - "parentPluginId": "securitySolution", - "id": "def-common.HostsRiskyHostsRequestOptions", - "type": "Type", - "tags": [], - "label": "HostsRiskyHostsRequestOptions", - "description": [], - "signature": [ - { - "pluginId": "securitySolution", - "scope": "common", - "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.RequestBasicOptions", - "text": "RequestBasicOptions" - } - ], - "path": "x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risky_hosts/index.ts", - "deprecated": false, - "initialIsOpen": false - }, { "parentPluginId": "securitySolution", "id": "def-common.HostTacticsSortField", @@ -21393,13 +21544,13 @@ "section": "def-common.HostsQueries", "text": "HostsQueries" }, - ".riskyHosts ? ", + ".hostsRiskScore ? ", { "pluginId": "securitySolution", "scope": "common", "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.RequestBasicOptions", - "text": "RequestBasicOptions" + "section": "def-common.HostsRiskScoreRequestOptions", + "text": "HostsRiskScoreRequestOptions" }, " : T extends ", { @@ -21883,13 +22034,13 @@ "section": "def-common.HostsQueries", "text": "HostsQueries" }, - ".riskyHosts ? ", + ".hostsRiskScore ? ", { "pluginId": "securitySolution", "scope": "common", "docId": "kibSecuritySolutionPluginApi", - "section": "def-common.HostsRiskyHostsStrategyResponse", - "text": "HostsRiskyHostsStrategyResponse" + "section": "def-common.HostsRiskScoreStrategyResponse", + "text": "HostsRiskScoreStrategyResponse" }, " : T extends ", { diff --git a/api_docs/security_solution.mdx b/api_docs/security_solution.mdx index e3920a4a3685f..fec34843bbcdb 100644 --- a/api_docs/security_solution.mdx +++ b/api_docs/security_solution.mdx @@ -18,7 +18,7 @@ Contact [Security solution](https://github.com/orgs/elastic/teams/security-solut | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 1353 | 8 | 1299 | 29 | +| 1361 | 8 | 1307 | 30 | ## Client diff --git a/api_docs/share.json b/api_docs/share.json index df192ba86bec3..992fcb9a0d43f 100644 --- a/api_docs/share.json +++ b/api_docs/share.json @@ -1912,7 +1912,15 @@ "section": "def-server.SerializableRecord", "text": "SerializableRecord" }, - ">): void; }" + ">): void; setAnonymousAccessServiceProvider: (provider: () => ", + { + "pluginId": "share", + "scope": "common", + "docId": "kibSharePluginApi", + "section": "def-common.AnonymousAccessServiceContract", + "text": "AnonymousAccessServiceContract" + }, + ") => void; }" ], "path": "src/plugins/share/public/plugin.ts", "deprecated": false, @@ -2224,6 +2232,107 @@ } ], "interfaces": [ + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessServiceContract", + "type": "Interface", + "tags": [], + "label": "AnonymousAccessServiceContract", + "description": [ + "\nThe contract that is used to check anonymous access for the purposes of sharing public links. The implementation is intended to be\nprovided by the security plugin." + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessServiceContract.getState", + "type": "Function", + "tags": [], + "label": "getState", + "description": [ + "\nThis function returns the current state of anonymous access." + ], + "signature": [ + "() => Promise<", + { + "pluginId": "share", + "scope": "common", + "docId": "kibSharePluginApi", + "section": "def-common.AnonymousAccessState", + "text": "AnonymousAccessState" + }, + ">" + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessServiceContract.getCapabilities", + "type": "Function", + "tags": [], + "label": "getCapabilities", + "description": [ + "\nThis function returns the capabilities of the anonymous access user." + ], + "signature": [ + "() => Promise<", + "Capabilities", + ">" + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false, + "children": [], + "returnComment": [] + } + ], + "initialIsOpen": false + }, + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessState", + "type": "Interface", + "tags": [], + "label": "AnonymousAccessState", + "description": [ + "\nThe state of anonymous access." + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessState.isEnabled", + "type": "boolean", + "tags": [], + "label": "isEnabled", + "description": [ + "\nWhether anonymous access is enabled or not." + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false + }, + { + "parentPluginId": "share", + "id": "def-common.AnonymousAccessState.accessURLParameters", + "type": "CompoundType", + "tags": [], + "label": "accessURLParameters", + "description": [ + "\nIf anonymous access is enabled, this reflects what URL parameters need to be added to a Kibana link to make it publicly accessible.\nNote that if anonymous access is the only authentication method, this will be null." + ], + "signature": [ + "Record | null" + ], + "path": "src/plugins/share/common/anonymous_access/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "share", "id": "def-common.LocatorDefinition", diff --git a/api_docs/share.mdx b/api_docs/share.mdx index 38fb5dc1a8485..61c0e26f24470 100644 --- a/api_docs/share.mdx +++ b/api_docs/share.mdx @@ -18,7 +18,7 @@ Contact [App Services](https://github.com/orgs/elastic/teams/kibana-app-services | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 137 | 1 | 90 | 10 | +| 143 | 1 | 90 | 10 | ## Client diff --git a/api_docs/spaces.json b/api_docs/spaces.json index e8195a0a47fc4..b391ca6ea0dbb 100644 --- a/api_docs/spaces.json +++ b/api_docs/spaces.json @@ -295,6 +295,45 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "spaces", + "id": "def-public.EmbeddableLegacyUrlConflictProps", + "type": "Interface", + "tags": [], + "label": "EmbeddableLegacyUrlConflictProps", + "description": [ + "\nProperties for the EmbeddableLegacyUrlConflict component." + ], + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "spaces", + "id": "def-public.EmbeddableLegacyUrlConflictProps.targetType", + "type": "string", + "tags": [], + "label": "targetType", + "description": [ + "\nThe target type of the legacy URL alias." + ], + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", + "deprecated": false + }, + { + "parentPluginId": "spaces", + "id": "def-public.EmbeddableLegacyUrlConflictProps.sourceId", + "type": "string", + "tags": [], + "label": "sourceId", + "description": [ + "\nThe source ID of the legacy URL alias." + ], + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "spaces", "id": "def-public.GetSpaceResult", @@ -359,7 +398,7 @@ "description": [ "\nProperties for the LegacyUrlConflict component." ], - "path": "x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts", + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", "deprecated": false, "children": [ { @@ -374,7 +413,7 @@ "signature": [ "string | undefined" ], - "path": "x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts", + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", "deprecated": false }, { @@ -386,7 +425,7 @@ "description": [ "\nThe ID of the object that is currently shown on the page." ], - "path": "x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts", + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", "deprecated": false }, { @@ -398,7 +437,7 @@ "description": [ "\nThe ID of the other object that the legacy URL alias points to." ], - "path": "x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts", + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", "deprecated": false }, { @@ -408,9 +447,9 @@ "tags": [], "label": "otherObjectPath", "description": [ - "\nThe path to use for the new URL, optionally including `search` and/or `hash` URL components." + "\n The path within your application to use for the new URL, optionally including `search` and/or `hash` URL components. Do not include\n `/app/my-app` or the current base path." ], - "path": "x-pack/plugins/spaces/public/share_saved_objects_to_space/types.ts", + "path": "x-pack/plugins/spaces/public/legacy_urls/types.ts", "deprecated": false } ], @@ -1305,12 +1344,12 @@ }, { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getLegacyUrlConflict", + "id": "def-public.SpacesApiUiComponent.getEmbeddableLegacyUrlConflict", "type": "Function", "tags": [], - "label": "getLegacyUrlConflict", + "label": "getEmbeddableLegacyUrlConflict", "description": [ - "\nDisplays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `\"conflict\"` outcome, which\nindicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a\ndifferent object (B).\n\nIn this case, `SavedObjectsClient.resolve()` has returned object A. This component displays a warning callout to the user explaining\nthat there is a conflict, and it includes a button that will redirect the user to object B when clicked.\n\nConsumers need to determine the local path for the new URL on their own, based on the object ID that was used to call\n`SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example...\n\nA is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`.\n\nFull legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1`\n\nNew URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`" + "\nDisplays a callout that needs to be used if an embeddable component call to `SavedObjectsClient.resolve()` results in an `\"conflict\"`\noutcome, which indicates that the user has loaded an embeddable which is associated directly with one object (A), *and* with a legacy\nURL that points to a different object (B)." ], "signature": [ "(props: ", @@ -1318,8 +1357,8 @@ "pluginId": "spaces", "scope": "public", "docId": "kibSpacesPluginApi", - "section": "def-public.LegacyUrlConflictProps", - "text": "LegacyUrlConflictProps" + "section": "def-public.EmbeddableLegacyUrlConflictProps", + "text": "EmbeddableLegacyUrlConflictProps" }, ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" ], @@ -1329,7 +1368,7 @@ "children": [ { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getLegacyUrlConflict.$1", + "id": "def-public.SpacesApiUiComponent.getEmbeddableLegacyUrlConflict.$1", "type": "Uncategorized", "tags": [], "label": "props", @@ -1344,12 +1383,12 @@ }, { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getSpaceAvatar", + "id": "def-public.SpacesApiUiComponent.getLegacyUrlConflict", "type": "Function", "tags": [], - "label": "getSpaceAvatar", + "label": "getLegacyUrlConflict", "description": [ - "\nDisplays an avatar for the given space." + "\nDisplays a callout that needs to be used if a call to `SavedObjectsClient.resolve()` results in an `\"conflict\"` outcome, which\nindicates that the user has loaded the page which is associated directly with one object (A), *and* with a legacy URL that points to a\ndifferent object (B).\n\nIn this case, `SavedObjectsClient.resolve()` has returned object A. This component displays a warning callout to the user explaining\nthat there is a conflict, and it includes a button that will redirect the user to object B when clicked.\n\nConsumers need to determine the local path for the new URL on their own, based on the object ID that was used to call\n`SavedObjectsClient.resolve()` (A) and the `alias_target_id` value in the response (B). For example...\n\nA is `workpad-123` and B is `workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e`.\n\nFull legacy URL: `https://localhost:5601/app/canvas#/workpad/workpad-123/page/1`\n\nNew URL path: `#/workpad/workpad-e08b9bdb-ec14-4339-94c4-063bddfd610e/page/1`" ], "signature": [ "(props: ", @@ -1357,8 +1396,8 @@ "pluginId": "spaces", "scope": "public", "docId": "kibSpacesPluginApi", - "section": "def-public.SpaceAvatarProps", - "text": "SpaceAvatarProps" + "section": "def-public.LegacyUrlConflictProps", + "text": "LegacyUrlConflictProps" }, ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" ], @@ -1368,7 +1407,7 @@ "children": [ { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getSpaceAvatar.$1", + "id": "def-public.SpacesApiUiComponent.getLegacyUrlConflict.$1", "type": "Uncategorized", "tags": [], "label": "props", @@ -1383,16 +1422,22 @@ }, { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getSavedObjectConflictMessage", + "id": "def-public.SpacesApiUiComponent.getSpaceAvatar", "type": "Function", "tags": [], - "label": "getSavedObjectConflictMessage", + "label": "getSpaceAvatar", "description": [ - "\nDisplays a saved object conflict message that directs user to disable legacy URL alias" + "\nDisplays an avatar for the given space." ], "signature": [ "(props: ", - "SavedObjectConflictMessageProps", + { + "pluginId": "spaces", + "scope": "public", + "docId": "kibSpacesPluginApi", + "section": "def-public.SpaceAvatarProps", + "text": "SpaceAvatarProps" + }, ") => React.ReactElement React.ReactElement React.Component)> | null) | (new (props: any) => React.Component)>" ], "path": "x-pack/plugins/spaces/public/ui_api/types.ts", @@ -1401,7 +1446,7 @@ "children": [ { "parentPluginId": "spaces", - "id": "def-public.SpacesApiUiComponent.getSavedObjectConflictMessage.$1", + "id": "def-public.SpacesApiUiComponent.getSpaceAvatar.$1", "type": "Uncategorized", "tags": [], "label": "props", diff --git a/api_docs/spaces.mdx b/api_docs/spaces.mdx index 12e803604cede..9f4bf1d79346a 100644 --- a/api_docs/spaces.mdx +++ b/api_docs/spaces.mdx @@ -18,7 +18,7 @@ Contact [Platform Security](https://github.com/orgs/elastic/teams/kibana-securit | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 205 | 0 | 21 | 2 | +| 208 | 0 | 21 | 1 | ## Client diff --git a/api_docs/timelines.json b/api_docs/timelines.json index eb31eb6849a86..416f623520394 100644 --- a/api_docs/timelines.json +++ b/api_docs/timelines.json @@ -1657,13 +1657,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; })[]; id: string; title: string; filters?: ", { "pluginId": "@kbn/es-query", @@ -1709,13 +1705,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; })[]; savedObjectId: string | null; unit?: ((n: number) => React.ReactNode) | undefined; dataProviders: ", { "pluginId": "timelines", @@ -1732,15 +1724,15 @@ "section": "def-common.RowRendererId", "text": "RowRendererId" }, - "[]; expandedDetail: ", + "[]; expandedDetail: Partial>; footerText?: React.ReactNode; graphEventId?: string | undefined; kqlQuery: { filterQuery: ", { "pluginId": "timelines", "scope": "common", @@ -5582,12 +5574,15 @@ { "parentPluginId": "timelines", "id": "def-common.BrowserField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - "{ [key: string]: unknown; nested?: { path: string; } | undefined; } | undefined" + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", + " | undefined" ], "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", "deprecated": false @@ -6153,13 +6148,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", @@ -8418,18 +8409,14 @@ { "parentPluginId": "timelines", "id": "def-common.IndexField.subType", - "type": "Object", + "type": "CompoundType", "tags": [], "label": "subType", "description": [], "signature": [ - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined" ], "path": "x-pack/plugins/timelines/common/search_strategy/index_fields/index.ts", @@ -11273,14 +11260,15 @@ "label": "expandedDetail", "description": [], "signature": [ + "Partial> | undefined" ], "path": "x-pack/plugins/timelines/common/types/timeline/store.ts", "deprecated": false @@ -12716,13 +12704,9 @@ "text": "ColumnHeaderType" }, "; description?: string | undefined; example?: string | undefined; format?: string | undefined; linkField?: string | undefined; placeholder?: string | undefined; subType?: ", - { - "pluginId": "@kbn/es-query", - "scope": "common", - "docId": "kibKbnEsQueryPluginApi", - "section": "def-common.IFieldSubType", - "text": "IFieldSubType" - }, + "IFieldSubTypeMultiOptional", + " | ", + "IFieldSubTypeNestedOptional", " | undefined; type?: string | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/columns/index.tsx", @@ -14383,17 +14367,9 @@ "label": "TimelineExpandedDetail", "description": [], "signature": [ - "{ query?: Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", - "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; graph?: Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", - "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; notes?: Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", - "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; pinned?: Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", - "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; eql?: Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", + "{ [x: string]: { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; }" + " | undefined; } | undefined; } | Partial> | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; } | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/index.ts", "deprecated": false, @@ -14407,9 +14383,9 @@ "label": "TimelineExpandedDetailType", "description": [], "signature": [ - "Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", + "{ panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", "Ecs", - " | undefined; } | undefined; } | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; }" + " | undefined; } | undefined; } | Partial> | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; } | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/index.ts", "deprecated": false, @@ -14423,9 +14399,9 @@ "label": "TimelineExpandedEventType", "description": [], "signature": [ - "Record | { panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", + "{ panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", "Ecs", - " | undefined; } | undefined; }" + " | undefined; } | undefined; } | Partial>" ], "path": "x-pack/plugins/timelines/common/types/timeline/index.ts", "deprecated": false, @@ -14439,7 +14415,7 @@ "label": "TimelineExpandedHostType", "description": [], "signature": [ - "Record | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; }" + "Partial> | { panelView?: \"hostDetail\" | undefined; params?: { hostName: string; } | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/index.ts", "deprecated": false, @@ -14453,7 +14429,7 @@ "label": "TimelineExpandedNetworkType", "description": [], "signature": [ - "Record | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; }" + "Partial> | { panelView?: \"networkDetail\" | undefined; params?: { ip: string; flowTarget: FlowTarget; } | undefined; }" ], "path": "x-pack/plugins/timelines/common/types/timeline/index.ts", "deprecated": false, @@ -14894,7 +14870,9 @@ "label": "ToggleDetailPanel", "description": [], "signature": [ - "(Record & { tabType?: ", + "({ panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", + "Ecs", + " | undefined; } | undefined; } & { tabType?: ", { "pluginId": "timelines", "scope": "common", @@ -14902,9 +14880,7 @@ "section": "def-common.TimelineTabs", "text": "TimelineTabs" }, - " | undefined; timelineId: string; }) | ({ panelView?: \"eventDetail\" | undefined; params?: { eventId: string; indexName: string; ecsData?: ", - "Ecs", - " | undefined; } | undefined; } & { tabType?: ", + " | undefined; timelineId: string; }) | (Partial> & { tabType?: ", { "pluginId": "timelines", "scope": "common", diff --git a/api_docs/transform.json b/api_docs/transform.json new file mode 100644 index 0000000000000..cbf3b9dbac343 --- /dev/null +++ b/api_docs/transform.json @@ -0,0 +1,101 @@ +{ + "id": "transform", + "client": { + "classes": [], + "functions": [ + { + "parentPluginId": "transform", + "id": "def-public.getTransformHealthRuleType", + "type": "Function", + "tags": [], + "label": "getTransformHealthRuleType", + "description": [], + "signature": [ + "() => ", + { + "pluginId": "triggersActionsUi", + "scope": "public", + "docId": "kibTriggersActionsUiPluginApi", + "section": "def-public.AlertTypeModel", + "text": "AlertTypeModel" + }, + "<", + "TransformHealthRuleParams", + ">" + ], + "path": "x-pack/plugins/transform/public/alerting/transform_health_rule_type/register_transform_health_rule.ts", + "deprecated": false, + "children": [], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "server": { + "classes": [], + "functions": [ + { + "parentPluginId": "transform", + "id": "def-server.registerTransformHealthRuleType", + "type": "Function", + "tags": [], + "label": "registerTransformHealthRuleType", + "description": [], + "signature": [ + "(params: RegisterParams) => void" + ], + "path": "x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "transform", + "id": "def-server.registerTransformHealthRuleType.$1", + "type": "Object", + "tags": [], + "label": "params", + "description": [], + "signature": [ + "RegisterParams" + ], + "path": "x-pack/plugins/transform/server/lib/alerting/transform_health_rule_type/register_transform_health_rule_type.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + } + ], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [] + }, + "common": { + "classes": [], + "functions": [], + "interfaces": [], + "enums": [], + "misc": [], + "objects": [ + { + "parentPluginId": "transform", + "id": "def-common.TRANSFORM_RULE_TYPE", + "type": "Object", + "tags": [], + "label": "TRANSFORM_RULE_TYPE", + "description": [], + "signature": [ + "{ readonly TRANSFORM_HEALTH: \"transform_health\"; }" + ], + "path": "x-pack/plugins/transform/common/constants.ts", + "deprecated": false, + "initialIsOpen": false + } + ] + } +} \ No newline at end of file diff --git a/api_docs/transform.mdx b/api_docs/transform.mdx new file mode 100644 index 0000000000000..21541177b3cec --- /dev/null +++ b/api_docs/transform.mdx @@ -0,0 +1,37 @@ +--- +id: kibTransformPluginApi +slug: /kibana-dev-docs/api/transform +title: "transform" +image: https://source.unsplash.com/400x175/?github +summary: API docs for the transform plugin +date: 2020-11-16 +tags: ['contributor', 'dev', 'apidocs', 'kibana', 'transform'] +warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. +--- +import transformObj from './transform.json'; + +This plugin provides access to the transforms features provided by Elastic. Transforms enable you to convert existing Elasticsearch indices into summarized indices, which provide opportunities for new insights and analytics. + +Contact [Machine Learning UI](https://github.com/orgs/elastic/teams/ml-ui) for questions regarding this plugin. + +**Code health stats** + +| Public API count | Any count | Items lacking comments | Missing exports | +|-------------------|-----------|------------------------|-----------------| +| 4 | 0 | 4 | 1 | + +## Client + +### Functions + + +## Server + +### Functions + + +## Common + +### Objects + + diff --git a/api_docs/vis_default_editor.json b/api_docs/vis_default_editor.json index 883d846e67bb8..619c3734e9517 100644 --- a/api_docs/vis_default_editor.json +++ b/api_docs/vis_default_editor.json @@ -943,9 +943,9 @@ "(paramName: T, value: ", { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemaParams", + "section": "def-common.ColorSchemaParams", "text": "ColorSchemaParams" }, "[T]) => void" @@ -977,9 +977,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.ColorSchemaParams", + "section": "def-common.ColorSchemaParams", "text": "ColorSchemaParams" }, "[T]" diff --git a/api_docs/vis_type_vislib.json b/api_docs/vis_type_vislib.json index f7bac3f7e3e9e..3b5d895ac4c83 100644 --- a/api_docs/vis_type_vislib.json +++ b/api_docs/vis_type_vislib.json @@ -107,9 +107,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.Labels", + "section": "def-common.Labels", "text": "Labels" } ], diff --git a/api_docs/vis_type_xy.json b/api_docs/vis_type_xy.json index 92df25763766e..0cb9fd390d188 100644 --- a/api_docs/vis_type_xy.json +++ b/api_docs/vis_type_xy.json @@ -141,9 +141,9 @@ "signature": [ { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.Labels", + "section": "def-common.Labels", "text": "Labels" } ], @@ -231,9 +231,9 @@ "Partial<", { "pluginId": "charts", - "scope": "public", + "scope": "common", "docId": "kibChartsPluginApi", - "section": "def-public.Style", + "section": "def-common.Style", "text": "Style" }, "> | undefined" diff --git a/api_docs/visualizations.json b/api_docs/visualizations.json index e93eec8a07ee8..d131d522b3515 100644 --- a/api_docs/visualizations.json +++ b/api_docs/visualizations.json @@ -1140,6 +1140,37 @@ } ], "functions": [ + { + "parentPluginId": "visualizations", + "id": "def-public.getFullPath", + "type": "Function", + "tags": [], + "label": "getFullPath", + "description": [], + "signature": [ + "(id: string) => string" + ], + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.getFullPath.$1", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "visualizations", "id": "def-public.getVisSchemas", @@ -1360,6 +1391,37 @@ "returnComment": [], "initialIsOpen": false }, + { + "parentPluginId": "visualizations", + "id": "def-public.urlFor", + "type": "Function", + "tags": [], + "label": "urlFor", + "description": [], + "signature": [ + "(id: string) => string" + ], + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.urlFor.$1", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [], + "initialIsOpen": false + }, { "parentPluginId": "visualizations", "id": "def-public.VisualizationContainer", @@ -1494,6 +1556,98 @@ ], "initialIsOpen": false }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions", + "type": "Interface", + "tags": [], + "label": "GetVisOptions", + "description": [], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.id", + "type": "string", + "tags": [], + "label": "id", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.searchSource", + "type": "CompoundType", + "tags": [], + "label": "searchSource", + "description": [], + "signature": [ + "boolean | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.migrationVersion", + "type": "Object", + "tags": [], + "label": "migrationVersion", + "description": [], + "signature": [ + "SavedObjectsMigrationVersion", + " | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.savedSearchId", + "type": "string", + "tags": [], + "label": "savedSearchId", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.type", + "type": "string", + "tags": [], + "label": "type", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.GetVisOptions.indexPattern", + "type": "string", + "tags": [], + "label": "indexPattern", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + } + ], + "initialIsOpen": false + }, { "parentPluginId": "visualizations", "id": "def-public.HistogramParams", @@ -1640,6 +1794,19 @@ ], "path": "src/plugins/visualizations/public/types.ts", "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.ISavedVis.sharingSavedObjectProps", + "type": "Object", + "tags": [], + "label": "sharingSavedObjectProps", + "description": [], + "signature": [ + "{ outcome?: \"conflict\" | \"exactMatch\" | \"aliasMatch\" | undefined; aliasTargetId?: string | undefined; errorJSON?: string | undefined; } | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false } ], "initialIsOpen": false @@ -2531,14 +2698,6 @@ "text": "VisSavedObject" }, " extends ", - { - "pluginId": "savedObjects", - "scope": "public", - "docId": "kibSavedObjectsPluginApi", - "section": "def-public.SavedObject", - "text": "SavedObject" - }, - ",", { "pluginId": "visualizations", "scope": "public", @@ -2549,7 +2708,119 @@ ], "path": "src/plugins/visualizations/public/types.ts", "deprecated": false, - "children": [], + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.lastSavedTitle", + "type": "string", + "tags": [], + "label": "lastSavedTitle", + "description": [], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.getEsType", + "type": "Function", + "tags": [], + "label": "getEsType", + "description": [], + "signature": [ + "() => string" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.getDisplayName", + "type": "Function", + "tags": [], + "label": "getDisplayName", + "description": [], + "signature": [ + "(() => string) | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false, + "children": [], + "returnComment": [] + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.displayName", + "type": "string", + "tags": [], + "label": "displayName", + "description": [], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.migrationVersion", + "type": "Object", + "tags": [], + "label": "migrationVersion", + "description": [], + "signature": [ + "SavedObjectsMigrationVersion", + " | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.searchSource", + "type": "Object", + "tags": [], + "label": "searchSource", + "description": [], + "signature": [ + "Pick<", + { + "pluginId": "data", + "scope": "common", + "docId": "kibDataSearchPluginApi", + "section": "def-common.SearchSource", + "text": "SearchSource" + }, + ", \"create\" | \"history\" | \"setPreferredSearchStrategyId\" | \"setField\" | \"removeField\" | \"setFields\" | \"getId\" | \"getFields\" | \"getField\" | \"getOwnField\" | \"createCopy\" | \"createChild\" | \"setParent\" | \"getParent\" | \"fetch$\" | \"fetch\" | \"onRequestStart\" | \"getSearchRequestBody\" | \"destroy\" | \"getSerializedFields\" | \"serialize\"> | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.version", + "type": "string", + "tags": [], + "label": "version", + "description": [], + "signature": [ + "string | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisSavedObject.tags", + "type": "Array", + "tags": [], + "label": "tags", + "description": [], + "signature": [ + "string[] | undefined" + ], + "path": "src/plugins/visualizations/public/types.ts", + "deprecated": false + } + ], "initialIsOpen": false }, { @@ -4788,8 +5059,8 @@ "pluginId": "visualizations", "scope": "public", "docId": "kibVisualizationsPluginApi", - "section": "def-public.ISavedVis", - "text": "ISavedVis" + "section": "def-public.VisSavedObject", + "text": "VisSavedObject" }, ") => ", { @@ -4825,11 +5096,11 @@ "pluginId": "visualizations", "scope": "public", "docId": "kibVisualizationsPluginApi", - "section": "def-public.ISavedVis", - "text": "ISavedVis" + "section": "def-public.VisSavedObject", + "text": "VisSavedObject" } ], - "path": "src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", "deprecated": false } ] @@ -4896,7 +5167,7 @@ }, ">" ], - "path": "src/plugins/visualizations/public/saved_visualizations/_saved_vis.ts", + "path": "src/plugins/visualizations/public/utils/saved_visualize_utils.ts", "deprecated": false } ] @@ -4932,6 +5203,193 @@ } ] }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.getSavedVisualization", + "type": "Function", + "tags": [], + "label": "getSavedVisualization", + "description": [], + "signature": [ + "(opts?: string | ", + { + "pluginId": "visualizations", + "scope": "public", + "docId": "kibVisualizationsPluginApi", + "section": "def-public.GetVisOptions", + "text": "GetVisOptions" + }, + " | undefined) => Promise<", + { + "pluginId": "visualizations", + "scope": "public", + "docId": "kibVisualizationsPluginApi", + "section": "def-public.VisSavedObject", + "text": "VisSavedObject" + }, + ">" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.getSavedVisualization.$1", + "type": "CompoundType", + "tags": [], + "label": "opts", + "description": [], + "signature": [ + "string | ", + { + "pluginId": "visualizations", + "scope": "public", + "docId": "kibVisualizationsPluginApi", + "section": "def-public.GetVisOptions", + "text": "GetVisOptions" + }, + " | undefined" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": false + } + ], + "returnComment": [] + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.saveVisualization", + "type": "Function", + "tags": [], + "label": "saveVisualization", + "description": [], + "signature": [ + "(savedVis: ", + { + "pluginId": "visualizations", + "scope": "public", + "docId": "kibVisualizationsPluginApi", + "section": "def-public.VisSavedObject", + "text": "VisSavedObject" + }, + ", saveOptions: ", + "SaveVisOptions", + ") => Promise" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.saveVisualization.$1", + "type": "Object", + "tags": [], + "label": "savedVis", + "description": [], + "signature": [ + { + "pluginId": "visualizations", + "scope": "public", + "docId": "kibVisualizationsPluginApi", + "section": "def-public.VisSavedObject", + "text": "VisSavedObject" + } + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.saveVisualization.$2", + "type": "Object", + "tags": [], + "label": "saveOptions", + "description": [], + "signature": [ + "SaveVisOptions" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": true + } + ], + "returnComment": [] + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.findListItems", + "type": "Function", + "tags": [], + "label": "findListItems", + "description": [], + "signature": [ + "(searchTerm: string, listingLimit: number, references?: ", + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + "[] | undefined) => Promise<{ hits: Record[]; total: number; }>" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "children": [ + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.findListItems.$1", + "type": "string", + "tags": [], + "label": "searchTerm", + "description": [], + "signature": [ + "string" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.findListItems.$2", + "type": "number", + "tags": [], + "label": "listingLimit", + "description": [], + "signature": [ + "number" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": true + }, + { + "parentPluginId": "visualizations", + "id": "def-public.VisualizationsStart.findListItems.$3", + "type": "Array", + "tags": [], + "label": "references", + "description": [], + "signature": [ + { + "pluginId": "core", + "scope": "server", + "docId": "kibCoreSavedObjectsPluginApi", + "section": "def-server.SavedObjectsFindOptionsReference", + "text": "SavedObjectsFindOptionsReference" + }, + "[] | undefined" + ], + "path": "src/plugins/visualizations/public/plugin.ts", + "deprecated": false, + "isRequired": false + } + ], + "returnComment": [] + }, { "parentPluginId": "visualizations", "id": "def-public.VisualizationsStart.__LEGACY", @@ -4964,17 +5422,7 @@ "section": "def-public.VisualizeInput", "text": "VisualizeInput" }, - "> & { id: string; }, savedVisualizationsLoader?: (", - { - "pluginId": "savedObjects", - "scope": "public", - "docId": "kibSavedObjectsPluginApi", - "section": "def-public.SavedObjectLoader", - "text": "SavedObjectLoader" - }, - " & { findListItems: (search: string, sizeOrOptions?: number | ", - "FindListItemsOptions", - " | undefined) => any; }) | undefined, attributeService?: ", + "> & { id: string; }, attributeService?: ", { "pluginId": "embeddable", "scope": "public", diff --git a/api_docs/visualizations.mdx b/api_docs/visualizations.mdx index a9501b59a1a55..5253bcdcd57e0 100644 --- a/api_docs/visualizations.mdx +++ b/api_docs/visualizations.mdx @@ -18,7 +18,7 @@ Contact [Vis Editors](https://github.com/orgs/elastic/teams/kibana-vis-editors) | Public API count | Any count | Items lacking comments | Missing exports | |-------------------|-----------|------------------------|-----------------| -| 275 | 13 | 257 | 15 | +| 304 | 13 | 286 | 16 | ## Client diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.json b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.json deleted file mode 100644 index c7b43d9436cb9..0000000000000 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.json +++ /dev/null @@ -1,76 +0,0 @@ -{ - "id": "pluginA.foo", - "client": { - "classes": [], - "functions": [ - { - "parentPluginId": "pluginA", - "id": "def-public.doTheFooFnThing", - "type": "Function", - "tags": [], - "label": "doTheFooFnThing", - "description": [], - "signature": [ - "() => void" - ], - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", - "deprecated": false, - "children": [], - "returnComment": [], - "initialIsOpen": false - } - ], - "interfaces": [], - "enums": [], - "misc": [ - { - "parentPluginId": "pluginA", - "id": "def-public.FooType", - "type": "Type", - "tags": [], - "label": "FooType", - "description": [], - "signature": [ - "() => \"foo\"" - ], - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/public/foo/index.ts", - "deprecated": false, - "returnComment": [], - "children": [], - "initialIsOpen": false - } - ], - "objects": [] - }, - "server": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [], - "objects": [] - }, - "common": { - "classes": [], - "functions": [], - "interfaces": [], - "enums": [], - "misc": [ - { - "parentPluginId": "pluginA", - "id": "def-common.commonFoo", - "type": "string", - "tags": [], - "label": "commonFoo", - "description": [], - "signature": [ - "\"COMMON VAR!\"" - ], - "path": "packages/kbn-docs-utils/src/api_docs/tests/__fixtures__/src/plugin_a/common/foo/index.ts", - "deprecated": false, - "initialIsOpen": false - } - ], - "objects": [] - } -} \ No newline at end of file diff --git a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.mdx b/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.mdx deleted file mode 100644 index 1fd371b585ce7..0000000000000 --- a/packages/kbn-docs-utils/src/api_docs/tests/snapshots/plugin_a.foo.mdx +++ /dev/null @@ -1,35 +0,0 @@ ---- -id: kibPluginAFooPluginApi -slug: /kibana-dev-docs/pluginA.fooPluginApi -title: pluginA.foo -image: https://source.unsplash.com/400x175/?github -summary: API docs for the pluginA.foo plugin -date: 2020-11-16 -tags: ['contributor', 'dev', 'apidocs', 'kibana', 'pluginA.foo'] -warning: This document is auto-generated and is meant to be viewed inside our experimental, new docs system. Reach out in #docs-engineering for more info. ---- -import pluginA.fooObj from './plugin_a.foo.json'; - - - -Contact Kibana Core for questions regarding this plugin. - -**Code health stats** - -| Public API count | Any count | Items lacking comments | Missing exports | -|-------------------|-----------|------------------------|-----------------| -| 3 | 0 | 0 | 0 | - -## Client - -### Functions - - -### Consts, variables and types - - -## Common - -### Consts, variables and types - - From 6cb91c472dfbc9ef6c38258407bc8a4b2dca6540 Mon Sep 17 00:00:00 2001 From: James Rucker Date: Mon, 11 Oct 2021 17:08:49 -0700 Subject: [PATCH 039/287] [Enterprise Search] Warn about Kibana access when adding single user role mappings (#114567) * Warn about Kibana access when adding single user role mappings to Enterprise Search * i18n and sentence case for the Kibana access warning title --- .../components/role_mappings/user.tsx | 1 + .../shared/role_mapping/constants.ts | 22 ++++++ .../role_mapping/user_added_info.test.tsx | 67 +++++++++++++++++++ .../shared/role_mapping/user_added_info.tsx | 29 +++++++- .../public/applications/shared/types.ts | 1 + .../views/role_mappings/user.tsx | 1 + 6 files changed, 118 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx index 57f08c22220f7..9e041dd83a293 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/role_mappings/user.tsx @@ -69,6 +69,7 @@ export const User: React.FC = () => { username={singleUserRoleMapping.elasticsearchUser.username} email={singleUserRoleMapping.elasticsearchUser.email as string} roleType={singleUserRoleMapping.roleMapping.roleType} + showKibanaAccessWarning={!singleUserRoleMapping.hasEnterpriseSearchRole} /> ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts index 25a1e084a3a60..d2229b428932f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/constants.ts @@ -448,3 +448,25 @@ export const SMTP_CALLOUT_LABEL = i18n.translate( export const SMTP_LINK_LABEL = i18n.translate('xpack.enterpriseSearch.roleMapping.smtpLinkLabel', { defaultMessage: 'SMTP configuration is provided', }); + +export const KIBANA_ACCESS_WARNING_TITLE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.kibanaAccessWarningTitle', + { + defaultMessage: 'Kibana access warning', + } +); + +export const KIBANA_ACCESS_WARNING_ERROR_MESSAGE = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.kibanaAccessWarningErrorMessage', + { + defaultMessage: + 'This Elasticsearch user does not have an Enterprise Search role in Elasticsearch. They may not have access to Kibana.', + } +); + +export const KIBANA_ACCESS_WARNING_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.roleMapping.kibanaAccessWarningDescription', + { + defaultMessage: 'Consider giving them the "enterprise-search-user" role.', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx index 57200b389591d..05ccafbe3e38f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.test.tsx @@ -16,6 +16,7 @@ describe('UserAddedInfo', () => { username: 'user1', email: 'test@test.com', roleType: 'user', + showKibanaAccessWarning: false, }; it('renders with email', () => { @@ -117,4 +118,70 @@ describe('UserAddedInfo', () => { `); }); + + it('renders with the Kibana access warning', () => { + const wrapper = shallow(); + + expect(wrapper).toMatchInlineSnapshot(` + + + + This Elasticsearch user does not have an Enterprise Search role in Elasticsearch. They may not have access to Kibana. + + + + Consider giving them the "enterprise-search-user" role. + + + + + + Username + + + + user1 + + + + + Email + + + + test@test.com + + + + + Role + + + + user + + + + `); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx index 37804414a94a9..71fb3d76295f0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/role_mapping/user_added_info.tsx @@ -7,22 +7,45 @@ import React from 'react'; -import { EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; +import { EuiCallOut, EuiSpacer, EuiText, EuiTextColor } from '@elastic/eui'; import { USERNAME_LABEL, EMAIL_LABEL } from '../constants'; -import { ROLE_LABEL } from './constants'; +import { + KIBANA_ACCESS_WARNING_TITLE, + KIBANA_ACCESS_WARNING_DESCRIPTION, + KIBANA_ACCESS_WARNING_ERROR_MESSAGE, + ROLE_LABEL, +} from './constants'; interface Props { username: string; email: string; roleType: string; + showKibanaAccessWarning: boolean; } +const kibanaAccessWarning = ( + <> + + {KIBANA_ACCESS_WARNING_ERROR_MESSAGE} + + {KIBANA_ACCESS_WARNING_DESCRIPTION} + + + +); + const noItemsPlaceholder = ; -export const UserAddedInfo: React.FC = ({ username, email, roleType }) => ( +export const UserAddedInfo: React.FC = ({ + username, + email, + roleType, + showKibanaAccessWarning, +}) => ( <> + {showKibanaAccessWarning && kibanaAccessWarning} {USERNAME_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts index 4743e808cc6ea..51a83cb15cca5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/types.ts @@ -55,4 +55,5 @@ export interface SingleUserRoleMapping { invitation: Invitation | null; elasticsearchUser: ElasticsearchUser; roleMapping: T; + hasEnterpriseSearchRole?: boolean; } diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx index 33785b4a4ce0e..e6244a0c7b2e0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/role_mappings/user.tsx @@ -66,6 +66,7 @@ export const User: React.FC = () => { username={singleUserRoleMapping.elasticsearchUser.username} email={singleUserRoleMapping.elasticsearchUser.email as string} roleType={singleUserRoleMapping.roleMapping.roleType} + showKibanaAccessWarning={!singleUserRoleMapping.hasEnterpriseSearchRole} /> ); From 44c9611bd9256f069b7dddac4d95c90adcace628 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 11 Oct 2021 17:49:21 -0700 Subject: [PATCH 040/287] [8.0] Remove support for configuring csp.rules (#114379) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kibana-plugin-core-server.cspconfig.md | 1 - ...bana-plugin-core-server.cspconfig.rules.md | 11 -- ...core-server.icspconfig.disableembedding.md | 2 +- .../kibana-plugin-core-server.icspconfig.md | 3 +- ...ana-plugin-core-server.icspconfig.rules.md | 13 -- docs/migration/migrate_8_0.asciidoc | 6 + docs/setup/settings.asciidoc | 10 +- .../deprecation/core_deprecations.test.ts | 96 ------------ .../config/deprecation/core_deprecations.ts | 59 +------ src/core/server/csp/config.test.ts | 144 ++--------------- src/core/server/csp/config.ts | 41 ----- src/core/server/csp/csp_config.test.ts | 62 ++------ src/core/server/csp/csp_config.ts | 14 +- src/core/server/csp/csp_directives.test.ts | 147 +----------------- src/core/server/csp/csp_directives.ts | 28 +--- .../http_resources_service.test.ts | 8 +- src/core/server/server.api.md | 3 - .../resources/base/bin/kibana-docker | 1 - .../collectors/csp/csp_collector.test.ts | 8 +- 19 files changed, 54 insertions(+), 603 deletions(-) delete mode 100644 docs/development/core/server/kibana-plugin-core-server.cspconfig.rules.md delete mode 100644 docs/development/core/server/kibana-plugin-core-server.icspconfig.rules.md diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md index 0337a1f4d3301..46f24dfda6739 100644 --- a/docs/development/core/server/kibana-plugin-core-server.cspconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.cspconfig.md @@ -24,7 +24,6 @@ The constructor for this class is marked as internal. Third-party code should no | [DEFAULT](./kibana-plugin-core-server.cspconfig.default.md) | static | CspConfig | | | [disableEmbedding](./kibana-plugin-core-server.cspconfig.disableembedding.md) | | boolean | | | [header](./kibana-plugin-core-server.cspconfig.header.md) | | string | | -| [rules](./kibana-plugin-core-server.cspconfig.rules.md) | | string[] | | | [strict](./kibana-plugin-core-server.cspconfig.strict.md) | | boolean | | | [warnLegacyBrowsers](./kibana-plugin-core-server.cspconfig.warnlegacybrowsers.md) | | boolean | | diff --git a/docs/development/core/server/kibana-plugin-core-server.cspconfig.rules.md b/docs/development/core/server/kibana-plugin-core-server.cspconfig.rules.md deleted file mode 100644 index 2bc73345fe0d0..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.cspconfig.rules.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CspConfig](./kibana-plugin-core-server.cspconfig.md) > [rules](./kibana-plugin-core-server.cspconfig.rules.md) - -## CspConfig.rules property - -Signature: - -```typescript -readonly rules: string[]; -``` diff --git a/docs/development/core/server/kibana-plugin-core-server.icspconfig.disableembedding.md b/docs/development/core/server/kibana-plugin-core-server.icspconfig.disableembedding.md index 2cfd680459fbc..42b177c348afe 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icspconfig.disableembedding.md +++ b/docs/development/core/server/kibana-plugin-core-server.icspconfig.disableembedding.md @@ -4,7 +4,7 @@ ## ICspConfig.disableEmbedding property -Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled \*and\* no custom rules have been defined, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. +Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.icspconfig.md b/docs/development/core/server/kibana-plugin-core-server.icspconfig.md index ee49950df076c..9da31cdc11e36 100644 --- a/docs/development/core/server/kibana-plugin-core-server.icspconfig.md +++ b/docs/development/core/server/kibana-plugin-core-server.icspconfig.md @@ -16,9 +16,8 @@ export interface ICspConfig | Property | Type | Description | | --- | --- | --- | -| [disableEmbedding](./kibana-plugin-core-server.icspconfig.disableembedding.md) | boolean | Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled \*and\* no custom rules have been defined, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. | +| [disableEmbedding](./kibana-plugin-core-server.icspconfig.disableembedding.md) | boolean | Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. | | [header](./kibana-plugin-core-server.icspconfig.header.md) | string | The CSP rules in a formatted directives string for use in a Content-Security-Policy header. | -| [rules](./kibana-plugin-core-server.icspconfig.rules.md) | string[] | The CSP rules used for Kibana. | | [strict](./kibana-plugin-core-server.icspconfig.strict.md) | boolean | Specify whether browsers that do not support CSP should be able to use Kibana. Use true to block and false to allow. | | [warnLegacyBrowsers](./kibana-plugin-core-server.icspconfig.warnlegacybrowsers.md) | boolean | Specify whether users with legacy browsers should be warned about their lack of Kibana security compliance. | diff --git a/docs/development/core/server/kibana-plugin-core-server.icspconfig.rules.md b/docs/development/core/server/kibana-plugin-core-server.icspconfig.rules.md deleted file mode 100644 index 808eefc30b64c..0000000000000 --- a/docs/development/core/server/kibana-plugin-core-server.icspconfig.rules.md +++ /dev/null @@ -1,13 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ICspConfig](./kibana-plugin-core-server.icspconfig.md) > [rules](./kibana-plugin-core-server.icspconfig.rules.md) - -## ICspConfig.rules property - -The CSP rules used for Kibana. - -Signature: - -```typescript -readonly rules: string[]; -``` diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index 60a65580501a6..dc6754fba1ffc 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -48,6 +48,12 @@ for example, `logstash-*`. *Impact:* To allow Kibana to function for these legacy browsers, set `csp.strict: false`. Since this is about enforcing a security protocol, we *strongly discourage* disabling `csp.strict` unless it is critical that you support Internet Explorer 11. +[float] +==== Configuring content security policy rules is no longer supported +*Details:* Configuring `csp.rules` is removed in favor of per-directive specific configuration. Configuring the default `csp.script_src`, `csp.workers_src` and `csp.style_src` values is not required. + +*Impact:* Configure per-directive sources instead. See https://github.com/elastic/kibana/pull/102059 for more details. + [float] ==== Default logging timezone is now the system's timezone *Details:* In prior releases the timezone used in logs defaulted to UTC. We now use the host machine's timezone by default. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 9c3d4fc29f137..48bf5fe2cd7b3 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -26,13 +26,6 @@ Toggling this causes the server to regenerate assets on the next startup, which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* -| `csp.rules:` - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] -A https://w3c.github.io/webappsec-csp/[Content Security Policy] template -that disables certain unnecessary and potentially insecure capabilities in -the browser. It is strongly recommended that you keep the default CSP rules -that ship with {kib}. - | `csp.script_src:` | Add sources for the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/script-src[Content Security Policy `script-src` directive]. @@ -502,8 +495,7 @@ To disable, set to `null`. *Default:* `null` | Controls whether the https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy[`Content-Security-Policy`] and https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options[`X-Frame-Options`] headers are configured to disable embedding {kib} in other webpages using iframes. When set to `true`, secure headers are used to disable embedding, which adds the `frame-ancestors: -'self'` directive to the `Content-Security-Policy` response header (if you are using the default CSP rules), and adds the `X-Frame-Options: -SAMEORIGIN` response header. *Default:* `false` +'self'` directive to the `Content-Security-Policy` response header and adds the `X-Frame-Options: SAMEORIGIN` response header. *Default:* `false` | `server.customResponseHeaders:` {ess-icon} | Header names and values to diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 95e23561a9378..e08f2216f5cbe 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -83,100 +83,4 @@ describe('core deprecations', () => { expect(messages).toHaveLength(0); }); }); - - describe('cspRulesDeprecation', () => { - describe('with nonce source', () => { - it('logs a warning', () => { - const settings = { - csp: { - rules: [`script-src 'self' 'nonce-{nonce}'`], - }, - }; - const { messages } = applyCoreDeprecations(settings); - expect(messages).toMatchInlineSnapshot(` - Array [ - "csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in script-src", - ] - `); - }); - - it('replaces a nonce', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}'`] } }).migrated.csp - .rules - ).toEqual([`script-src 'self'`]); - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval' 'nonce-{nonce}'`] } }) - .migrated.csp.rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - - it('removes a quoted nonce', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'self' 'nonce-{nonce}'`] } }).migrated - .csp.rules - ).toEqual([`script-src 'self'`]); - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'nonce-{nonce}' 'self'`] } }).migrated - .csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a non-quoted nonce', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'self' nonce-{nonce}`] } }).migrated - .csp.rules - ).toEqual([`script-src 'self'`]); - expect( - applyCoreDeprecations({ csp: { rules: [`script-src nonce-{nonce} 'self'`] } }).migrated - .csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes a strange nonce', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'self' blah-{nonce}-wow`] } }).migrated - .csp.rules - ).toEqual([`script-src 'self'`]); - }); - - it('removes multiple nonces', () => { - expect( - applyCoreDeprecations({ - csp: { - rules: [ - `script-src 'nonce-{nonce}' 'self' blah-{nonce}-wow`, - `style-src 'nonce-{nonce}' 'self'`, - ], - }, - }).migrated.csp.rules - ).toEqual([`script-src 'self'`, `style-src 'self'`]); - }); - }); - - describe('without self source', () => { - it('logs a warning', () => { - const { messages } = applyCoreDeprecations({ - csp: { rules: [`script-src 'unsafe-eval'`] }, - }); - expect(messages).toMatchInlineSnapshot(` - Array [ - "csp.rules must contain the 'self' source. Automatically adding to script-src.", - ] - `); - }); - - it('adds self', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`script-src 'unsafe-eval'`] } }).migrated.csp.rules - ).toEqual([`script-src 'unsafe-eval' 'self'`]); - }); - }); - - it('does not add self to other policies', () => { - expect( - applyCoreDeprecations({ csp: { rules: [`worker-src blob:`] } }).migrated.csp.rules - ).toEqual([`worker-src blob:`]); - }); - }); }); diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 4e5f711fe9f3a..5adbb338b42e4 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -45,64 +45,7 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecati } }; -const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { - const NONCE_STRING = `{nonce}`; - // Policies that should include the 'self' source - const SELF_POLICIES = Object.freeze(['script-src', 'style-src']); - const SELF_STRING = `'self'`; - - const rules: string[] = settings.csp?.rules; - if (rules) { - const parsed = new Map( - rules.map((ruleStr) => { - const parts = ruleStr.split(/\s+/); - return [parts[0], parts.slice(1)]; - }) - ); - - return { - set: [ - { - path: 'csp.rules', - value: [...parsed].map(([policy, sourceList]) => { - if (sourceList.find((source) => source.includes(NONCE_STRING))) { - addDeprecation({ - message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, - correctiveActions: { - manualSteps: [`Replace {nonce} syntax with 'self' in ${policy}`], - }, - }); - sourceList = sourceList.filter((source) => !source.includes(NONCE_STRING)); - - // Add 'self' if not present - if (!sourceList.find((source) => source.includes(SELF_STRING))) { - sourceList.push(SELF_STRING); - } - } - - if ( - SELF_POLICIES.includes(policy) && - !sourceList.find((source) => source.includes(SELF_STRING)) - ) { - addDeprecation({ - message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, - correctiveActions: { - manualSteps: [`Add 'self' source to ${policy}.`], - }, - }); - sourceList.push(SELF_STRING); - } - - return `${policy} ${sourceList.join(' ')}`.trim(); - }), - }, - ], - }; - } -}; - -export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ +export const coreDeprecationProvider: ConfigDeprecationProvider = () => [ rewriteCorsSettings, rewriteBasePathDeprecation, - cspRulesDeprecation, ]; diff --git a/src/core/server/csp/config.test.ts b/src/core/server/csp/config.test.ts index 6db93addb7da8..346caf488431c 100644 --- a/src/core/server/csp/config.test.ts +++ b/src/core/server/csp/config.test.ts @@ -80,21 +80,6 @@ describe('config.validate()', () => { ).not.toThrow(); }); - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - script_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -104,6 +89,7 @@ describe('config.validate()', () => { `"[script_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -124,21 +110,6 @@ describe('config.validate()', () => { }); describe(`"worker_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - worker_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -148,6 +119,7 @@ describe('config.validate()', () => { `"[worker_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -168,21 +140,6 @@ describe('config.validate()', () => { }); describe(`"style_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - style_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -192,6 +149,7 @@ describe('config.validate()', () => { `"[style_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -212,21 +170,6 @@ describe('config.validate()', () => { }); describe(`"connect_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - connect_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -236,6 +179,7 @@ describe('config.validate()', () => { `"[connect_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -256,21 +200,6 @@ describe('config.validate()', () => { }); describe(`"default_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - default_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -280,6 +209,7 @@ describe('config.validate()', () => { `"[default_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -300,21 +230,6 @@ describe('config.validate()', () => { }); describe(`"font_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - font_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -324,6 +239,7 @@ describe('config.validate()', () => { `"[font_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -344,21 +260,6 @@ describe('config.validate()', () => { }); describe(`"frame_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - frame_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -368,6 +269,7 @@ describe('config.validate()', () => { `"[frame_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -388,21 +290,6 @@ describe('config.validate()', () => { }); describe(`"img_src"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - img_src: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -412,6 +299,7 @@ describe('config.validate()', () => { `"[img_src]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ @@ -432,21 +320,6 @@ describe('config.validate()', () => { }); describe(`"frame_ancestors"`, () => { - it(`throws if 'rules' is also specified`, () => { - expect(() => - config.schema.validate({ - rules: [ - `script-src 'unsafe-eval' 'self'`, - `worker-src 'unsafe-eval' 'self'`, - `style-src 'unsafe-eval' 'self'`, - ], - frame_ancestors: [`'self'`], - }) - ).toThrowErrorMatchingInlineSnapshot( - `"\\"csp.rules\\" cannot be used when specifying per-directive additions such as \\"script_src\\", \\"worker_src\\" or \\"style_src\\""` - ); - }); - it('throws if using an `nonce-*` value', () => { expect(() => config.schema.validate({ @@ -456,6 +329,7 @@ describe('config.validate()', () => { `"[frame_ancestors]: using \\"nonce-*\\" is considered insecure and is not allowed"` ); }); + it("throws if using `none` or `'none'`", () => { expect(() => config.schema.validate({ diff --git a/src/core/server/csp/config.ts b/src/core/server/csp/config.ts index 3a7cb20985cea..22aa11e4e0c3a 100644 --- a/src/core/server/csp/config.ts +++ b/src/core/server/csp/config.ts @@ -39,7 +39,6 @@ const getDirectiveValueValidator = ({ allowNone, allowNonce }: DirectiveValidati const configSchema = schema.object( { - rules: schema.maybe(schema.arrayOf(schema.string())), script_src: schema.arrayOf(schema.string(), { defaultValue: [], validate: getDirectiveValidator({ allowNone: false, allowNonce: false }), @@ -89,9 +88,6 @@ const configSchema = schema.object( }, { validate: (cspConfig) => { - if (cspConfig.rules && hasDirectiveSpecified(cspConfig)) { - return `"csp.rules" cannot be used when specifying per-directive additions such as "script_src", "worker_src" or "style_src"`; - } const hasUnsafeInlineScriptSrc = cspConfig.script_src.includes(`unsafe-inline`) || cspConfig.script_src.includes(`'unsafe-inline'`); @@ -106,22 +102,6 @@ const configSchema = schema.object( } ); -const hasDirectiveSpecified = (rawConfig: CspConfigType): boolean => { - return Boolean( - rawConfig.script_src.length || - rawConfig.worker_src.length || - rawConfig.style_src.length || - rawConfig.connect_src.length || - rawConfig.default_src.length || - rawConfig.font_src.length || - rawConfig.frame_src.length || - rawConfig.img_src.length || - rawConfig.frame_ancestors.length || - rawConfig.report_uri.length || - rawConfig.report_to.length - ); -}; - /** * @internal */ @@ -132,25 +112,4 @@ export const config: ServiceConfigDescriptor = { // ? https://github.com/elastic/kibana/pull/52251 path: 'csp', schema: configSchema, - deprecations: () => [ - (rawConfig, fromPath, addDeprecation) => { - const cspConfig = rawConfig[fromPath]; - if (cspConfig?.rules) { - addDeprecation({ - message: - '`csp.rules` is deprecated in favor of directive specific configuration. Please use `csp.connect_src`, ' + - '`csp.default_src`, `csp.font_src`, `csp.frame_ancestors`, `csp.frame_src`, `csp.img_src`, ' + - '`csp.report_uri`, `csp.report_to`, `csp.script_src`, `csp.style_src`, and `csp.worker_src` instead.', - correctiveActions: { - manualSteps: [ - `Remove "csp.rules" from the Kibana config file."`, - `Add directive specific configurations to the config file using "csp.connect_src", "csp.default_src", "csp.font_src", ` + - `"csp.frame_ancestors", "csp.frame_src", "csp.img_src", "csp.report_uri", "csp.report_to", "csp.script_src", ` + - `"csp.style_src", and "csp.worker_src".`, - ], - }, - }); - } - }, - ], }; diff --git a/src/core/server/csp/csp_config.test.ts b/src/core/server/csp/csp_config.test.ts index a1bac7d4ae73e..1ec78fae7532d 100644 --- a/src/core/server/csp/csp_config.test.ts +++ b/src/core/server/csp/csp_config.test.ts @@ -34,11 +34,6 @@ describe('CspConfig', () => { CspConfig { "disableEmbedding": false, "header": "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'", - "rules": Array [ - "script-src 'unsafe-eval' 'self'", - "worker-src blob: 'self'", - "style-src 'unsafe-inline' 'self'", - ], "strict": true, "warnLegacyBrowsers": true, } @@ -50,13 +45,6 @@ describe('CspConfig', () => { }); describe('partial config', () => { - test('allows "rules" to be set and changes header', () => { - const rules = [`foo 'self'`, `bar 'self'`]; - const config = new CspConfig({ ...defaultConfig, rules }); - expect(config.rules).toEqual(rules); - expect(config.header).toMatchInlineSnapshot(`"foo 'self'; bar 'self'"`); - }); - test('allows "strict" to be set', () => { const config = new CspConfig({ ...defaultConfig, strict: false }); expect(config.strict).toEqual(false); @@ -70,67 +58,57 @@ describe('CspConfig', () => { expect(config.warnLegacyBrowsers).not.toEqual(CspConfig.DEFAULT.warnLegacyBrowsers); }); - test('allows "worker_src" to be set and changes header', () => { + test('allows "worker_src" to be set and changes header from defaults', () => { const config = new CspConfig({ ...defaultConfig, - rules: [], worker_src: ['foo', 'bar'], }); - expect(config.rules).toEqual([`worker-src foo bar`]); - expect(config.header).toEqual(`worker-src foo bar`); + expect(config.header).toEqual( + `script-src 'unsafe-eval' 'self'; worker-src blob: 'self' foo bar; style-src 'unsafe-inline' 'self'` + ); }); test('allows "style_src" to be set and changes header', () => { const config = new CspConfig({ ...defaultConfig, - rules: [], style_src: ['foo', 'bar'], }); - expect(config.rules).toEqual([`style-src foo bar`]); - expect(config.header).toEqual(`style-src foo bar`); + + expect(config.header).toEqual( + `script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self' foo bar` + ); }); test('allows "script_src" to be set and changes header', () => { const config = new CspConfig({ ...defaultConfig, - rules: [], script_src: ['foo', 'bar'], }); - expect(config.rules).toEqual([`script-src foo bar`]); - expect(config.header).toEqual(`script-src foo bar`); + + expect(config.header).toEqual( + `script-src 'unsafe-eval' 'self' foo bar; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'` + ); }); test('allows all directives to be set and changes header', () => { const config = new CspConfig({ ...defaultConfig, - rules: [], script_src: ['script', 'foo'], worker_src: ['worker', 'bar'], style_src: ['style', 'dolly'], }); - expect(config.rules).toEqual([ - `script-src script foo`, - `worker-src worker bar`, - `style-src style dolly`, - ]); expect(config.header).toEqual( - `script-src script foo; worker-src worker bar; style-src style dolly` + `script-src 'unsafe-eval' 'self' script foo; worker-src blob: 'self' worker bar; style-src 'unsafe-inline' 'self' style dolly` ); }); - test('applies defaults when `rules` is undefined', () => { + test('appends config directives to defaults', () => { const config = new CspConfig({ ...defaultConfig, - rules: undefined, script_src: ['script'], worker_src: ['worker'], style_src: ['style'], }); - expect(config.rules).toEqual([ - `script-src 'unsafe-eval' 'self' script`, - `worker-src blob: 'self' worker`, - `style-src 'unsafe-inline' 'self' style`, - ]); expect(config.header).toEqual( `script-src 'unsafe-eval' 'self' script; worker-src blob: 'self' worker; style-src 'unsafe-inline' 'self' style` ); @@ -139,25 +117,15 @@ describe('CspConfig', () => { describe('allows "disableEmbedding" to be set', () => { const disableEmbedding = true; - test('and changes rules/header if custom rules are not defined', () => { + test('and changes rules and header', () => { const config = new CspConfig({ ...defaultConfig, disableEmbedding }); expect(config.disableEmbedding).toEqual(disableEmbedding); expect(config.disableEmbedding).not.toEqual(CspConfig.DEFAULT.disableEmbedding); - expect(config.rules).toEqual(expect.arrayContaining([`frame-ancestors 'self'`])); expect(config.header).toMatchInlineSnapshot( `"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'; frame-ancestors 'self'"` ); }); - test('and does not change rules/header if custom rules are defined', () => { - const rules = [`foo 'self'`, `bar 'self'`]; - const config = new CspConfig({ ...defaultConfig, disableEmbedding, rules }); - expect(config.disableEmbedding).toEqual(disableEmbedding); - expect(config.disableEmbedding).not.toEqual(CspConfig.DEFAULT.disableEmbedding); - expect(config.rules).toEqual(rules); - expect(config.header).toMatchInlineSnapshot(`"foo 'self'; bar 'self'"`); - }); - test('and overrides `frame-ancestors` if set', () => { const config = new CspConfig({ ...defaultConfig, diff --git a/src/core/server/csp/csp_config.ts b/src/core/server/csp/csp_config.ts index 13778088d9df2..f0f968ecd0ceb 100644 --- a/src/core/server/csp/csp_config.ts +++ b/src/core/server/csp/csp_config.ts @@ -16,11 +16,6 @@ const DEFAULT_CONFIG = Object.freeze(config.schema.validate({})); * @public */ export interface ICspConfig { - /** - * The CSP rules used for Kibana. - */ - readonly rules: string[]; - /** * Specify whether browsers that do not support CSP should be * able to use Kibana. Use `true` to block and `false` to allow. @@ -34,8 +29,7 @@ export interface ICspConfig { readonly warnLegacyBrowsers: boolean; /** - * Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled *and* no custom rules have been - * defined, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. + * Whether or not embedding (using iframes) should be allowed by the CSP. If embedding is disabled, a restrictive 'frame-ancestors' rule will be added to the default CSP rules. */ readonly disableEmbedding: boolean; @@ -54,7 +48,6 @@ export class CspConfig implements ICspConfig { static readonly DEFAULT = new CspConfig(DEFAULT_CONFIG); readonly #directives: CspDirectives; - public readonly rules: string[]; public readonly strict: boolean; public readonly warnLegacyBrowsers: boolean; public readonly disableEmbedding: boolean; @@ -66,14 +59,11 @@ export class CspConfig implements ICspConfig { */ constructor(rawCspConfig: CspConfigType) { this.#directives = CspDirectives.fromConfig(rawCspConfig); - if (!rawCspConfig.rules?.length && rawCspConfig.disableEmbedding) { + if (rawCspConfig.disableEmbedding) { this.#directives.clearDirectiveValues('frame-ancestors'); this.#directives.addDirectiveValue('frame-ancestors', `'self'`); } - - this.rules = this.#directives.getRules(); this.header = this.#directives.getCspHeader(); - this.strict = rawCspConfig.strict; this.warnLegacyBrowsers = rawCspConfig.warnLegacyBrowsers; this.disableEmbedding = rawCspConfig.disableEmbedding; diff --git a/src/core/server/csp/csp_directives.test.ts b/src/core/server/csp/csp_directives.test.ts index 1077b6ea9f3cd..f4a9e256e2f98 100644 --- a/src/core/server/csp/csp_directives.test.ts +++ b/src/core/server/csp/csp_directives.test.ts @@ -11,33 +11,12 @@ import { config as cspConfig } from './config'; describe('CspDirectives', () => { describe('#addDirectiveValue', () => { - it('properly updates the rules', () => { - const directives = new CspDirectives(); - directives.addDirectiveValue('style-src', 'foo'); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "style-src foo", - ] - `); - - directives.addDirectiveValue('style-src', 'bar'); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "style-src foo bar", - ] - `); - }); - it('properly updates the header', () => { const directives = new CspDirectives(); directives.addDirectiveValue('style-src', 'foo'); - expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo"`); directives.addDirectiveValue('style-src', 'bar'); - expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo bar"`); }); @@ -50,12 +29,6 @@ describe('CspDirectives', () => { expect(directives.getCspHeader()).toMatchInlineSnapshot( `"style-src foo bar; worker-src dolly"` ); - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "style-src foo bar", - "worker-src dolly", - ] - `); }); it('removes duplicates', () => { @@ -65,11 +38,6 @@ describe('CspDirectives', () => { directives.addDirectiveValue('style-src', 'bar'); expect(directives.getCspHeader()).toMatchInlineSnapshot(`"style-src foo bar"`); - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "style-src foo bar", - ] - `); }); it('automatically adds single quotes for keywords', () => { @@ -106,18 +74,6 @@ describe('CspDirectives', () => { }); describe('#fromConfig', () => { - it('returns the correct rules for the default config', () => { - const config = cspConfig.schema.validate({}); - const directives = CspDirectives.fromConfig(config); - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'unsafe-eval' 'self'", - "worker-src blob: 'self'", - "style-src 'unsafe-inline' 'self'", - ] - `); - }); - it('returns the correct header for the default config', () => { const config = cspConfig.schema.validate({}); const directives = CspDirectives.fromConfig(config); @@ -126,75 +82,6 @@ describe('CspDirectives', () => { ); }); - it('handles config with rules', () => { - const config = cspConfig.schema.validate({ - rules: [`script-src 'self' http://foo.com`, `worker-src 'self'`], - }); - const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'self' http://foo.com", - "worker-src 'self'", - ] - `); - expect(directives.getCspHeader()).toMatchInlineSnapshot( - `"script-src 'self' http://foo.com; worker-src 'self'"` - ); - }); - - it('adds single quotes for keyword for rules', () => { - const config = cspConfig.schema.validate({ - rules: [`script-src self http://foo.com`, `worker-src self`], - }); - const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'self' http://foo.com", - "worker-src 'self'", - ] - `); - expect(directives.getCspHeader()).toMatchInlineSnapshot( - `"script-src 'self' http://foo.com; worker-src 'self'"` - ); - }); - - it('handles multiple whitespaces when parsing rules', () => { - const config = cspConfig.schema.validate({ - rules: [` script-src 'self' http://foo.com `, ` worker-src 'self' `], - }); - const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'self' http://foo.com", - "worker-src 'self'", - ] - `); - expect(directives.getCspHeader()).toMatchInlineSnapshot( - `"script-src 'self' http://foo.com; worker-src 'self'"` - ); - }); - - it('supports unregistered directives', () => { - const config = cspConfig.schema.validate({ - rules: [`script-src 'self' http://foo.com`, `img-src 'self'`, 'foo bar'], - }); - const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'self' http://foo.com", - "img-src 'self'", - "foo bar", - ] - `); - expect(directives.getCspHeader()).toMatchInlineSnapshot( - `"script-src 'self' http://foo.com; img-src 'self'; foo bar"` - ); - }); - it('adds default value for config with directives', () => { const config = cspConfig.schema.validate({ script_src: [`baz`], @@ -203,13 +90,6 @@ describe('CspDirectives', () => { }); const directives = CspDirectives.fromConfig(config); - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'unsafe-eval' 'self' baz", - "worker-src blob: 'self' foo", - "style-src 'unsafe-inline' 'self' bar dolly", - ] - `); expect(directives.getCspHeader()).toMatchInlineSnapshot( `"script-src 'unsafe-eval' 'self' baz; worker-src blob: 'self' foo; style-src 'unsafe-inline' 'self' bar dolly"` ); @@ -227,22 +107,9 @@ describe('CspDirectives', () => { report_to: [`report-to`], }); const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'unsafe-eval' 'self'", - "worker-src blob: 'self'", - "style-src 'unsafe-inline' 'self'", - "connect-src 'self' connect-src", - "default-src 'self' default-src", - "font-src 'self' font-src", - "frame-src 'self' frame-src", - "img-src 'self' img-src", - "frame-ancestors 'self' frame-ancestors", - "report-uri report-uri", - "report-to report-to", - ] - `); + expect(directives.getCspHeader()).toMatchInlineSnapshot( + `"script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'; connect-src 'self' connect-src; default-src 'self' default-src; font-src 'self' font-src; frame-src 'self' frame-src; img-src 'self' img-src; frame-ancestors 'self' frame-ancestors; report-uri report-uri; report-to report-to"` + ); }); it('adds single quotes for keywords in added directives', () => { @@ -250,14 +117,6 @@ describe('CspDirectives', () => { script_src: [`unsafe-hashes`], }); const directives = CspDirectives.fromConfig(config); - - expect(directives.getRules()).toMatchInlineSnapshot(` - Array [ - "script-src 'unsafe-eval' 'self' 'unsafe-hashes'", - "worker-src blob: 'self'", - "style-src 'unsafe-inline' 'self'", - ] - `); expect(directives.getCspHeader()).toMatchInlineSnapshot( `"script-src 'unsafe-eval' 'self' 'unsafe-hashes'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'"` ); diff --git a/src/core/server/csp/csp_directives.ts b/src/core/server/csp/csp_directives.ts index 9e3b60f7f1e4f..d656210a054f9 100644 --- a/src/core/server/csp/csp_directives.ts +++ b/src/core/server/csp/csp_directives.ts @@ -22,7 +22,7 @@ export type CspDirectiveName = | 'report-to'; /** - * The default rules that are always applied + * The default directives rules that are always applied */ export const defaultRules: Partial> = { 'script-src': [`'unsafe-eval'`, `'self'`], @@ -58,21 +58,18 @@ export class CspDirectives { } getCspHeader() { - return this.getRules().join('; '); - } - - getRules() { - return [...this.directives.entries()].map(([name, values]) => { - return [name, ...values].join(' '); - }); + return [...this.directives.entries()] + .map(([name, values]) => { + return [name, ...values].join(' '); + }) + .join('; '); } static fromConfig(config: CspConfigType): CspDirectives { const cspDirectives = new CspDirectives(); - // adding `csp.rules` or `default` rules - const initialRules = config.rules ? parseRules(config.rules) : { ...defaultRules }; - Object.entries(initialRules).forEach(([key, values]) => { + // combining `default` directive configurations + Object.entries(defaultRules).forEach(([key, values]) => { values?.forEach((value) => { cspDirectives.addDirectiveValue(key as CspDirectiveName, value); }); @@ -91,15 +88,6 @@ export class CspDirectives { } } -const parseRules = (rules: string[]): Partial> => { - const directives: Partial> = {}; - rules.forEach((rule) => { - const [name, ...values] = rule.replace(/\s+/g, ' ').trim().split(' '); - directives[name as CspDirectiveName] = values; - }); - return directives; -}; - const parseConfigDirectives = (cspConfig: CspConfigType): Map => { const map = new Map(); diff --git a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts index 6f4f3c9c6e985..3b254df929037 100644 --- a/src/core/server/http_resources/integration_tests/http_resources_service.test.ts +++ b/src/core/server/http_resources/integration_tests/http_resources_service.test.ts @@ -12,12 +12,10 @@ import * as kbnTestServer from '../../../test_helpers/kbn_server'; describe('http resources service', () => { describe('register', () => { let root: ReturnType; - const defaultCspRules = "script-src 'self'"; + const defaultCspRules = + "script-src 'unsafe-eval' 'self'; worker-src blob: 'self'; style-src 'unsafe-inline' 'self'"; beforeEach(async () => { root = kbnTestServer.createRoot({ - csp: { - rules: [defaultCspRules], - }, plugins: { initialize: false }, elasticsearch: { skipStartupConnectionCheck: true }, }); @@ -44,7 +42,7 @@ describe('http resources service', () => { expect(response.text.length).toBeGreaterThan(0); }); - it('attaches CSP header', async () => { + it('applies default CSP header', async () => { const { http, httpResources } = await root.setup(); const router = http.createRouter(''); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a1fd69f5e1c7e..fb16e889a19c7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -754,8 +754,6 @@ export class CspConfig implements ICspConfig { // (undocumented) readonly header: string; // (undocumented) - readonly rules: string[]; - // (undocumented) readonly strict: boolean; // (undocumented) readonly warnLegacyBrowsers: boolean; @@ -1135,7 +1133,6 @@ export type IContextProvider { expect((await collector.fetch(mockedFetchContext)).warnLegacyBrowsers).toEqual(false); }); - test('fetches whether the csp rules have been changed or not', async () => { + test("fetches whether the csp directives's rules have been changed or not", async () => { const collector = new Collector(logger, createCspCollector(httpMock)); expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(false); - updateCsp({ rules: ['not', 'default'] }); + updateCsp({ disableEmbedding: true }); expect((await collector.fetch(mockedFetchContext)).rulesChangedFromDefault).toEqual(true); }); test('does not include raw csp rules under any property names', async () => { const collector = new Collector(logger, createCspCollector(httpMock)); - // It's important that we do not send the value of csp.rules here as it + // It's important that we do not send the raw values of csp cirectives here as they // can be customized with values that can be identifiable to given // installs, such as URLs // - // We use a snapshot here to ensure csp.rules isn't finding its way into the + // We use a snapshot here to ensure raw values aren't finding their way into the // payload under some new and unexpected variable name (e.g. cspRules). expect(await collector.fetch(mockedFetchContext)).toMatchInlineSnapshot(` Object { From c926b14c32307a12b31938641594356655faf360 Mon Sep 17 00:00:00 2001 From: ymao1 Date: Mon, 11 Oct 2021 21:06:35 -0400 Subject: [PATCH 041/287] [Alerting] Showing last execution duration on Rule Management view (#113935) * Adding last duration to execution status and returning in alerting routes * Fixing types * Adding helper function to format duration * Returning rule timeout value in list rules API * Updating rules table to add duration column and tweaks to match mockup * Updating rules table to add duration column and tweaks to match mockup * i18n fix * Only showing duration warning if duration is long * Unit tests * i18n fix * Fixing functional test * Aligning warning icon * Reset last duration when rule is disabled then reenabled * Fixing functional test * Fixing functional test * Restoring muted badge. Fixing scss * Dont show muted badge if rule is disabled * Moving disabled icontip to right of rule name * Updating tooltips * Updating last run Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/alerting/common/alert.ts | 1 + x-pack/plugins/alerting/common/alert_type.ts | 1 + .../alerting/common/parse_duration.test.ts | 39 ++- .../plugins/alerting/common/parse_duration.ts | 16 + .../server/lib/alert_execution_status.test.ts | 49 +++ .../server/lib/alert_execution_status.ts | 20 +- .../alerting/server/routes/create_rule.ts | 3 +- .../alerting/server/routes/find_rules.ts | 3 +- .../alerting/server/routes/get_rule.ts | 3 +- .../alerting/server/routes/resolve_rule.ts | 3 +- .../alerting/server/routes/rule_types.test.ts | 3 + .../alerting/server/routes/rule_types.ts | 2 + .../alerting/server/routes/update_rule.ts | 1 + .../server/rule_type_registry.test.ts | 2 + .../alerting/server/rule_type_registry.ts | 3 + .../server/rules_client/rules_client.ts | 1 + .../server/rules_client/tests/enable.test.ts | 2 + .../server/saved_objects/mappings.json | 3 + .../server/task_runner/task_runner.test.ts | 2 + .../server/task_runner/task_runner.ts | 5 + x-pack/plugins/alerting/server/types.ts | 1 + .../translations/translations/ja-JP.json | 12 +- .../translations/translations/zh-CN.json | 14 +- .../lib/alert_api/common_transformations.ts | 2 + .../application/lib/alert_api/rule_types.ts | 2 + .../components/alert_status_filter.tsx | 4 +- .../alerts_list/components/alerts_list.scss | 11 + .../components/alerts_list.test.tsx | 57 +++- .../alerts_list/components/alerts_list.tsx | 311 ++++++++++++------ .../collapsed_item_actions.test.tsx | 2 +- .../components/rule_enabled_switch.test.tsx | 4 +- .../triggers_actions_ui/public/types.ts | 3 +- .../tests/alerting/rule_types.ts | 2 + .../spaces_only/tests/alerting/rule_types.ts | 2 + .../apps/triggers_actions_ui/alerts_list.ts | 130 +++----- .../page_objects/triggers_actions_ui_page.ts | 12 +- 36 files changed, 512 insertions(+), 219 deletions(-) diff --git a/x-pack/plugins/alerting/common/alert.ts b/x-pack/plugins/alerting/common/alert.ts index 1274e7b95b114..bf0c8e382c9d4 100644 --- a/x-pack/plugins/alerting/common/alert.ts +++ b/x-pack/plugins/alerting/common/alert.ts @@ -35,6 +35,7 @@ export enum AlertExecutionStatusErrorReasons { export interface AlertExecutionStatus { status: AlertExecutionStatuses; lastExecutionDate: Date; + lastDuration?: number; error?: { reason: AlertExecutionStatusErrorReasons; message: string; diff --git a/x-pack/plugins/alerting/common/alert_type.ts b/x-pack/plugins/alerting/common/alert_type.ts index d71540b4418e8..1b0ac28c9fa74 100644 --- a/x-pack/plugins/alerting/common/alert_type.ts +++ b/x-pack/plugins/alerting/common/alert_type.ts @@ -21,6 +21,7 @@ export interface AlertType< producer: string; minimumLicenseRequired: LicenseType; isExportable: boolean; + ruleTaskTimeout?: string; defaultScheduleInterval?: string; minimumScheduleInterval?: string; } diff --git a/x-pack/plugins/alerting/common/parse_duration.test.ts b/x-pack/plugins/alerting/common/parse_duration.test.ts index 9fbb662e21147..e68a3f479f228 100644 --- a/x-pack/plugins/alerting/common/parse_duration.test.ts +++ b/x-pack/plugins/alerting/common/parse_duration.test.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { parseDuration, getDurationNumberInItsUnit, getDurationUnitValue } from './parse_duration'; +import { + parseDuration, + formatDuration, + getDurationNumberInItsUnit, + getDurationUnitValue, +} from './parse_duration'; test('parses seconds', () => { const result = parseDuration('10s'); @@ -39,6 +44,38 @@ test('throws error when suffix is missing', () => { ); }); +test('formats seconds', () => { + const result = formatDuration('10s'); + expect(result).toEqual('10 sec'); +}); + +test('formats minutes', () => { + const result = formatDuration('10m'); + expect(result).toEqual('10 min'); +}); + +test('formats hours', () => { + const result = formatDuration('10h'); + expect(result).toEqual('10 hr'); +}); + +test('formats days', () => { + const result = formatDuration('10d'); + expect(result).toEqual('10 day'); +}); + +test('format throws error when the format is invalid', () => { + expect(() => formatDuration('10x')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"10x\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + +test('format throws error when suffix is missing', () => { + expect(() => formatDuration('1000')).toThrowErrorMatchingInlineSnapshot( + `"Invalid duration \\"1000\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` + ); +}); + test('throws error when 0 based', () => { expect(() => parseDuration('0s')).toThrowErrorMatchingInlineSnapshot( `"Invalid duration \\"0s\\". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d\\""` diff --git a/x-pack/plugins/alerting/common/parse_duration.ts b/x-pack/plugins/alerting/common/parse_duration.ts index 3494a48fc8ab9..af4f1d2c14099 100644 --- a/x-pack/plugins/alerting/common/parse_duration.ts +++ b/x-pack/plugins/alerting/common/parse_duration.ts @@ -27,6 +27,22 @@ export function parseDuration(duration: string): number { ); } +export function formatDuration(duration: string): string { + const parsed = parseInt(duration, 10); + if (isSeconds(duration)) { + return `${parsed} sec`; + } else if (isMinutes(duration)) { + return `${parsed} min`; + } else if (isHours(duration)) { + return `${parsed} hr`; + } else if (isDays(duration)) { + return `${parsed} day`; + } + throw new Error( + `Invalid duration "${duration}". Durations must be of the form {number}x. Example: 5s, 5m, 5h or 5d"` + ); +} + export function getDurationNumberInItsUnit(duration: string): number { return parseInt(duration.replace(/[^0-9.]/g, ''), 10); } diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts index 0a8d5632f169f..93cf0c656c692 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.test.ts @@ -81,6 +81,7 @@ describe('AlertExecutionStatus', () => { expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status })).toMatchInlineSnapshot(` Object { "error": null, + "lastDuration": 0, "lastExecutionDate": "2020-09-03T16:26:58.000Z", "status": "ok", } @@ -95,11 +96,24 @@ describe('AlertExecutionStatus', () => { "message": "wops", "reason": "decrypt", }, + "lastDuration": 0, "lastExecutionDate": "2020-09-03T16:26:58.000Z", "status": "ok", } `); }); + + test('status with a duration', () => { + expect(alertExecutionStatusToRaw({ lastExecutionDate: date, status, lastDuration: 1234 })) + .toMatchInlineSnapshot(` + Object { + "error": null, + "lastDuration": 1234, + "lastExecutionDate": "2020-09-03T16:26:58.000Z", + "status": "ok", + } + `); + }); }); describe('alertExecutionStatusFromRaw()', () => { @@ -177,6 +191,41 @@ describe('AlertExecutionStatus', () => { } `); }); + + test('valid status, date and duration', () => { + const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { + status, + lastExecutionDate: date, + lastDuration: 1234, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "lastDuration": 1234, + "lastExecutionDate": 2020-09-03T16:26:58.000Z, + "status": "active", + } + `); + }); + + test('valid status, date, error and duration', () => { + const result = alertExecutionStatusFromRaw(MockLogger, 'alert-id', { + status, + lastExecutionDate: date, + error, + lastDuration: 1234, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "message": "wops", + "reason": "execute", + }, + "lastDuration": 1234, + "lastExecutionDate": 2020-09-03T16:26:58.000Z, + "status": "active", + } + `); + }); }); }); diff --git a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts index 47dfc659307a2..82d8514331704 100644 --- a/x-pack/plugins/alerting/server/lib/alert_execution_status.ts +++ b/x-pack/plugins/alerting/server/lib/alert_execution_status.ts @@ -32,11 +32,13 @@ export function executionStatusFromError(error: Error): AlertExecutionStatus { export function alertExecutionStatusToRaw({ lastExecutionDate, + lastDuration, status, error, }: AlertExecutionStatus): RawAlertExecutionStatus { return { lastExecutionDate: lastExecutionDate.toISOString(), + lastDuration: lastDuration ?? 0, status, // explicitly setting to null (in case undefined) due to partial update concerns error: error ?? null, @@ -50,7 +52,7 @@ export function alertExecutionStatusFromRaw( ): AlertExecutionStatus | undefined { if (!rawAlertExecutionStatus) return undefined; - const { lastExecutionDate, status = 'unknown', error } = rawAlertExecutionStatus; + const { lastExecutionDate, lastDuration, status = 'unknown', error } = rawAlertExecutionStatus; let parsedDateMillis = lastExecutionDate ? Date.parse(lastExecutionDate) : Date.now(); if (isNaN(parsedDateMillis)) { @@ -60,12 +62,20 @@ export function alertExecutionStatusFromRaw( parsedDateMillis = Date.now(); } - const parsedDate = new Date(parsedDateMillis); + const executionStatus: AlertExecutionStatus = { + status, + lastExecutionDate: new Date(parsedDateMillis), + }; + + if (null != lastDuration) { + executionStatus.lastDuration = lastDuration; + } + if (error) { - return { lastExecutionDate: parsedDate, status, error }; - } else { - return { lastExecutionDate: parsedDate, status }; + executionStatus.error = error; } + + return executionStatus; } export const getAlertExecutionStatusPending = (lastExecutionDate: string) => ({ diff --git a/x-pack/plugins/alerting/server/routes/create_rule.ts b/x-pack/plugins/alerting/server/routes/create_rule.ts index 55a98eb56bee4..ed124bfbd3a2d 100644 --- a/x-pack/plugins/alerting/server/routes/create_rule.ts +++ b/x-pack/plugins/alerting/server/routes/create_rule.ts @@ -67,7 +67,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ notifyWhen, muteAll, mutedInstanceIds, - executionStatus: { lastExecutionDate, ...executionStatus }, + executionStatus: { lastExecutionDate, lastDuration, ...executionStatus }, ...rest }) => ({ ...rest, @@ -84,6 +84,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ execution_status: { ...executionStatus, last_execution_date: lastExecutionDate, + last_duration: lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/find_rules.ts b/x-pack/plugins/alerting/server/routes/find_rules.ts index a4a066728555d..7826e924acdc5 100644 --- a/x-pack/plugins/alerting/server/routes/find_rules.ts +++ b/x-pack/plugins/alerting/server/routes/find_rules.ts @@ -93,8 +93,9 @@ const rewriteBodyRes: RewriteResponseCase> = ({ muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/get_rule.ts b/x-pack/plugins/alerting/server/routes/get_rule.ts index c860ae725a253..4da9410517fe1 100644 --- a/x-pack/plugins/alerting/server/routes/get_rule.ts +++ b/x-pack/plugins/alerting/server/routes/get_rule.ts @@ -48,8 +48,9 @@ const rewriteBodyRes: RewriteResponseCase> = ({ muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/resolve_rule.ts b/x-pack/plugins/alerting/server/routes/resolve_rule.ts index 011d28780e718..a31cf5059b99f 100644 --- a/x-pack/plugins/alerting/server/routes/resolve_rule.ts +++ b/x-pack/plugins/alerting/server/routes/resolve_rule.ts @@ -48,8 +48,9 @@ const rewriteBodyRes: RewriteResponseCase muted_alert_ids: mutedInstanceIds, scheduled_task_id: scheduledTaskId, execution_status: executionStatus && { - ...omit(executionStatus, 'lastExecutionDate'), + ...omit(executionStatus, 'lastExecutionDate', 'lastDuration'), last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, actions: actions.map(({ group, id, actionTypeId, params }) => ({ group, diff --git a/x-pack/plugins/alerting/server/routes/rule_types.test.ts b/x-pack/plugins/alerting/server/routes/rule_types.test.ts index e4247c9de6cad..7deb2704fb7ec 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.test.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.test.ts @@ -49,6 +49,7 @@ describe('ruleTypesRoute', () => { defaultActionGroupId: 'default', minimumLicenseRequired: 'basic', isExportable: true, + ruleTaskTimeout: '10m', recoveryActionGroup: RecoveredActionGroup, authorizedConsumers: {}, actionVariables: { @@ -76,6 +77,7 @@ describe('ruleTypesRoute', () => { minimum_license_required: 'basic', minimum_schedule_interval: '1m', is_exportable: true, + rule_task_timeout: '10m', recovery_action_group: RecoveredActionGroup, authorized_consumers: {}, action_variables: { @@ -118,6 +120,7 @@ describe('ruleTypesRoute', () => { "id": "recovered", "name": "Recovered", }, + "rule_task_timeout": "10m", }, ], } diff --git a/x-pack/plugins/alerting/server/routes/rule_types.ts b/x-pack/plugins/alerting/server/routes/rule_types.ts index 72502b25e9aff..d1f24538d76d8 100644 --- a/x-pack/plugins/alerting/server/routes/rule_types.ts +++ b/x-pack/plugins/alerting/server/routes/rule_types.ts @@ -20,6 +20,7 @@ const rewriteBodyRes: RewriteResponseCase = (result defaultActionGroupId, minimumLicenseRequired, isExportable, + ruleTaskTimeout, actionVariables, authorizedConsumers, minimumScheduleInterval, @@ -33,6 +34,7 @@ const rewriteBodyRes: RewriteResponseCase = (result default_action_group_id: defaultActionGroupId, minimum_license_required: minimumLicenseRequired, is_exportable: isExportable, + rule_task_timeout: ruleTaskTimeout, action_variables: actionVariables, authorized_consumers: authorizedConsumers, minimum_schedule_interval: minimumScheduleInterval, diff --git a/x-pack/plugins/alerting/server/routes/update_rule.ts b/x-pack/plugins/alerting/server/routes/update_rule.ts index 6e8024a0ddbf5..007d24bb8251b 100644 --- a/x-pack/plugins/alerting/server/routes/update_rule.ts +++ b/x-pack/plugins/alerting/server/routes/update_rule.ts @@ -88,6 +88,7 @@ const rewriteBodyRes: RewriteResponseCase> = ({ execution_status: { status: executionStatus.status, last_execution_date: executionStatus.lastExecutionDate, + last_duration: executionStatus.lastDuration, }, } : {}), diff --git a/x-pack/plugins/alerting/server/rule_type_registry.test.ts b/x-pack/plugins/alerting/server/rule_type_registry.test.ts index beb5f264eb725..895a5047339ef 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.test.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.test.ts @@ -494,6 +494,7 @@ describe('list()', () => { ], defaultActionGroupId: 'testActionGroup', isExportable: true, + ruleTaskTimeout: '20m', minimumLicenseRequired: 'basic', executor: jest.fn(), producer: 'alerts', @@ -530,6 +531,7 @@ describe('list()', () => { "id": "recovered", "name": "Recovered", }, + "ruleTaskTimeout": "20m", }, } `); diff --git a/x-pack/plugins/alerting/server/rule_type_registry.ts b/x-pack/plugins/alerting/server/rule_type_registry.ts index db02edf4d19dd..452729a9a01e9 100644 --- a/x-pack/plugins/alerting/server/rule_type_registry.ts +++ b/x-pack/plugins/alerting/server/rule_type_registry.ts @@ -48,6 +48,7 @@ export interface RegistryRuleType | 'producer' | 'minimumLicenseRequired' | 'isExportable' + | 'ruleTaskTimeout' | 'minimumScheduleInterval' | 'defaultScheduleInterval' > { @@ -327,6 +328,7 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, }, @@ -340,6 +342,7 @@ export class RuleTypeRegistry { producer, minimumLicenseRequired, isExportable, + ruleTaskTimeout, minimumScheduleInterval, defaultScheduleInterval, enabledInLicense: !!this.licenseState.getLicenseCheckForAlertType( diff --git a/x-pack/plugins/alerting/server/rules_client/rules_client.ts b/x-pack/plugins/alerting/server/rules_client/rules_client.ts index 2228b5d27910f..2492517f4bdc3 100644 --- a/x-pack/plugins/alerting/server/rules_client/rules_client.ts +++ b/x-pack/plugins/alerting/server/rules_client/rules_client.ts @@ -1133,6 +1133,7 @@ export class RulesClient { updatedAt: new Date().toISOString(), executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: new Date().toISOString(), error: null, }, diff --git a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts index 7b8fbff4fca5a..5e3f148c2fc11 100644 --- a/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts +++ b/x-pack/plugins/alerting/server/rules_client/tests/enable.test.ts @@ -260,6 +260,7 @@ describe('enable()', () => { ], executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: '2019-02-12T21:01:22.479Z', error: null, }, @@ -369,6 +370,7 @@ describe('enable()', () => { ], executionStatus: { status: 'pending', + lastDuration: 0, lastExecutionDate: '2019-02-12T21:01:22.479Z', error: null, }, diff --git a/x-pack/plugins/alerting/server/saved_objects/mappings.json b/x-pack/plugins/alerting/server/saved_objects/mappings.json index 21d7a05f2a76d..05e221a47499c 100644 --- a/x-pack/plugins/alerting/server/saved_objects/mappings.json +++ b/x-pack/plugins/alerting/server/saved_objects/mappings.json @@ -101,6 +101,9 @@ "lastExecutionDate": { "type": "date" }, + "lastDuration": { + "type": "long" + }, "error": { "properties": { "reason": { diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index bc477136ec111..c5ccc909eff46 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -338,6 +338,7 @@ describe('Task Runner', () => { { executionStatus: { error: null, + lastDuration: 0, lastExecutionDate: '1970-01-01T00:00:00.000Z', status: 'ok', }, @@ -4394,6 +4395,7 @@ describe('Task Runner', () => { { executionStatus: { error: null, + lastDuration: 0, lastExecutionDate: '1970-01-01T00:00:00.000Z', status: 'ok', }, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index 28cc0f2dba4d0..edf9bfe1b4846 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -585,6 +585,11 @@ export class TaskRunner< event.kibana.alerting = event.kibana.alerting || {}; event.kibana.alerting.status = executionStatus.status; + // Copy duration into execution status if available + if (null != event.event?.duration) { + executionStatus.lastDuration = Math.round(event.event?.duration / Millis2Nanos); + } + // if executionStatus indicates an error, fill in fields in // event from it if (executionStatus.error) { diff --git a/x-pack/plugins/alerting/server/types.ts b/x-pack/plugins/alerting/server/types.ts index 1dc8291d28756..82bb94b121840 100644 --- a/x-pack/plugins/alerting/server/types.ts +++ b/x-pack/plugins/alerting/server/types.ts @@ -185,6 +185,7 @@ export interface AlertMeta extends SavedObjectAttributes { export interface RawAlertExecutionStatus extends SavedObjectAttributes { status: AlertExecutionStatuses; lastExecutionDate: string; + lastDuration?: number; error: null | { reason: AlertExecutionStatusErrorReasons; message: string; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index f36f3abe66a4c..fbd9352fb427f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25336,7 +25336,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "ルールの実行中にエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "不明な理由でエラーが発生しました。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "アクション", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle": "アクション", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "型", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "削除", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "削除", @@ -25347,7 +25346,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名前", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "次の間隔で実行", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "ステータス", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "タグ", "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "アクティブ", "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "エラー", "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "ステータス", @@ -25383,11 +25381,11 @@ "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "検索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "ルール", "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "{pageSize}/{totalItemCount}件のルールを表示しています。", - "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "有効:{totalStausesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "エラー:{totalStausesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "Ok:{totalStausesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "保留中:{totalStausesPending}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "不明:{totalStausesUnknown}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "有効:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "エラー:{totalStatusesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "Ok:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "保留中:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesUnknownDescription": "不明:{totalStatusesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "コネクタータイプを読み込めません", "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "ルールを読み込めません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 11b951b97ae05..9408bb85db879 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25763,7 +25763,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonRunning": "运行规则时发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertErrorReasonUnknown": "由于未知原因发生错误。", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTex": "操作", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle": "操作", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle": "类型", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteAriaLabel": "删除", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.deleteButtonTooltip": "删除", @@ -25774,7 +25773,6 @@ "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.nameTitle": "名称", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle": "运行间隔", "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle": "状态", - "xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText": "标签", "xpack.triggersActionsUI.sections.alertsList.alertStatusActive": "活动", "xpack.triggersActionsUI.sections.alertsList.alertStatusError": "错误", "xpack.triggersActionsUI.sections.alertsList.alertStatusFilterLabel": "状态", @@ -25782,7 +25780,7 @@ "xpack.triggersActionsUI.sections.alertsList.alertStatusOk": "确定", "xpack.triggersActionsUI.sections.alertsList.alertStatusPending": "待处理", "xpack.triggersActionsUI.sections.alertsList.alertStatusUnknown": "未知", - "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStausesError, plural, other {# 个规则}}中有错误。", + "xpack.triggersActionsUI.sections.alertsList.attentionBannerTitle": "{totalStatusesError, plural, other {# 个规则}}中有错误。", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.buttonTitle": "管理规则", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.deleteAllTitle": "删除", "xpack.triggersActionsUI.sections.alertsList.bulkActionPopover.disableAllTitle": "禁用", @@ -25811,11 +25809,11 @@ "xpack.triggersActionsUI.sections.alertsList.searchPlaceholderTitle": "搜索", "xpack.triggersActionsUI.sections.alertsList.singleTitle": "规则", "xpack.triggersActionsUI.sections.alertsList.totalItemsCountDescription": "正在显示:{pageSize} 个规则(共 {totalItemCount} 个)。", - "xpack.triggersActionsUI.sections.alertsList.totalStausesActiveDescription": "活动:{totalStausesActive}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesErrorDescription": "错误:{totalStausesError}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesOkDescription": "确定:{totalStausesOk}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesPendingDescription": "待处理:{totalStausesPending}", - "xpack.triggersActionsUI.sections.alertsList.totalStausesUnknownDescription": "未知:{totalStausesUnknown}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesActiveDescription": "活动:{totalStatusesActive}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesErrorDescription": "错误:{totalStatusesError}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesOkDescription": "确定:{totalStatusesOk}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesPendingDescription": "待处理:{totalStatusesPending}", + "xpack.triggersActionsUI.sections.alertsList.totalStatusesUnknownDescription": "未知:{totalStatusesUnknown}", "xpack.triggersActionsUI.sections.alertsList.typeFilterLabel": "类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadConnectorTypesMessage": "无法加载连接器类型", "xpack.triggersActionsUI.sections.alertsList.unableToLoadRulesMessage": "无法加载规则", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts index 5049a37c317dd..6369937e59377 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/common_transformations.ts @@ -22,9 +22,11 @@ const transformAction: RewriteRequestCase = ({ const transformExecutionStatus: RewriteRequestCase = ({ last_execution_date: lastExecutionDate, + last_duration: lastDuration, ...rest }) => ({ lastExecutionDate, + lastDuration, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts index 54369d7959c93..67d317643ec06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/lib/alert_api/rule_types.ts @@ -21,6 +21,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ minimum_license_required: minimumLicenseRequired, action_variables: actionVariables, authorized_consumers: authorizedConsumers, + rule_task_timeout: ruleTaskTimeout, ...rest }: AsApiContract) => ({ enabledInLicense, @@ -30,6 +31,7 @@ const rewriteBodyReq: RewriteRequestCase = ({ minimumLicenseRequired, actionVariables, authorizedConsumers, + ruleTaskTimeout, ...rest, }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx index aca111df97e34..50295548f9aa4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alert_status_filter.tsx @@ -99,10 +99,10 @@ export function getHealthColor(status: AlertExecutionStatuses) { case 'error': return 'danger'; case 'ok': - return 'subdued'; + return 'primary'; case 'pending': return 'accent'; default: - return 'warning'; + return 'subdued'; } } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss index c0e46b77b4156..138605421f202 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.scss @@ -31,3 +31,14 @@ opacity: 1; /* 2 */ } } + +.ruleDurationWarningIcon { + margin-bottom: $euiSizeXS; + margin-left: $euiSizeS; +} + +.ruleDisabledQuestionIcon { + bottom: $euiSizeXS; + margin-left: $euiSizeXS; + position: relative; +} \ No newline at end of file diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx index 958511128de04..53f5e25530e98 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.test.tsx @@ -17,6 +17,7 @@ import { AlertTypeModel, ValidationResult } from '../../../../types'; import { AlertExecutionStatusErrorReasons, ALERTS_FEATURE_ID, + parseDuration, } from '../../../../../../alerting/common'; import { useKibana } from '../../../../common/lib/kibana'; jest.mock('../../../../common/lib/kibana'); @@ -79,6 +80,7 @@ const alertTypeFromApi = { authorizedConsumers: { [ALERTS_FEATURE_ID]: { read: true, all: true }, }, + ruleTaskTimeout: '1m', }; ruleTypeRegistry.list.mockReturnValue([alertType]); actionTypeRegistry.list.mockReturnValue([]); @@ -170,6 +172,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'active', + lastDuration: 500, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -192,6 +195,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'ok', + lastDuration: 61000, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -214,6 +218,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'pending', + lastDuration: 30234, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: null, }, @@ -236,6 +241,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'error', + lastDuration: 122000, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: { reason: AlertExecutionStatusErrorReasons.Unknown, @@ -246,7 +252,7 @@ describe('alerts_list component with items', () => { { id: '5', name: 'test alert license error', - tags: ['tag1'], + tags: [], enabled: true, alertTypeId: 'test_alert_type', schedule: { interval: '5d' }, @@ -261,6 +267,7 @@ describe('alerts_list component with items', () => { mutedInstanceIds: [], executionStatus: { status: 'error', + lastDuration: 500, lastExecutionDate: new Date('2020-08-20T19:23:38Z'), error: { reason: AlertExecutionStatusErrorReasons.License, @@ -324,6 +331,53 @@ describe('alerts_list component with items', () => { await setup(); expect(wrapper.find('EuiBasicTable')).toHaveLength(1); expect(wrapper.find('EuiTableRow')).toHaveLength(mockedAlertsData.length); + + // Enabled switch column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-enabled"]').length + ).toEqual(mockedAlertsData.length); + + // Name and rule type column + const ruleNameColumns = wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-name"]'); + expect(ruleNameColumns.length).toEqual(mockedAlertsData.length); + mockedAlertsData.forEach((rule, index) => { + expect(ruleNameColumns.at(index).text()).toEqual(`Name${rule.name}${alertTypeFromApi.name}`); + }); + + // Tags column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-tagsPopover"]').length + ).toEqual(mockedAlertsData.length); + // only show tags popover if tags exist on rule + const tagsBadges = wrapper.find('EuiBadge[data-test-subj="ruleTagsBadge"]'); + expect(tagsBadges.length).toEqual( + mockedAlertsData.filter((data) => data.tags.length > 0).length + ); + + // Last run column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-lastExecutionDate"]').length + ).toEqual(mockedAlertsData.length); + + // Schedule interval column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-interval"]').length + ).toEqual(mockedAlertsData.length); + + // Duration column + expect( + wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-duration"]').length + ).toEqual(mockedAlertsData.length); + // show warning if duration is long + const durationWarningIcon = wrapper.find('EuiIconTip[data-test-subj="ruleDurationWarning"]'); + expect(durationWarningIcon.length).toEqual( + mockedAlertsData.filter( + (data) => + data.executionStatus.lastDuration > parseDuration(alertTypeFromApi.ruleTaskTimeout) + ).length + ); + + // Status column expect(wrapper.find('EuiTableRowCell[data-test-subj="alertsTableCell-status"]').length).toEqual( mockedAlertsData.length ); @@ -331,7 +385,6 @@ describe('alerts_list component with items', () => { expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-ok"]').length).toEqual(1); expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-pending"]').length).toEqual(1); expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-unknown"]').length).toEqual(0); - expect(wrapper.find('EuiHealth[data-test-subj="alertStatus-error"]').length).toEqual(2); expect(wrapper.find('[data-test-subj="alertStatus-error-tooltip"]').length).toEqual(2); expect( diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx index 1daaf3b996126..91b1b14083ed2 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/alerts_list.tsx @@ -8,7 +8,8 @@ /* eslint-disable react-hooks/exhaustive-deps */ import { i18n } from '@kbn/i18n'; -import { capitalize, sortBy } from 'lodash'; +import { capitalize, padStart, sortBy } from 'lodash'; +import moment from 'moment'; import { FormattedMessage } from '@kbn/i18n/react'; import React, { useEffect, useState } from 'react'; import { @@ -29,6 +30,10 @@ import { EuiToolTip, EuiTableSortingType, EuiButtonIcon, + EuiHorizontalRule, + EuiPopover, + EuiPopoverTitle, + EuiIcon, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; @@ -66,6 +71,8 @@ import { AlertExecutionStatusValues, ALERTS_FEATURE_ID, AlertExecutionStatusErrorReasons, + formatDuration, + parseDuration, } from '../../../../../../alerting/common'; import { alertsStatusesTranslationsMapping, ALERT_STATUS_LICENSE_ERROR } from '../translations'; import { useKibana } from '../../../../common/lib/kibana'; @@ -114,6 +121,7 @@ export const AlertsList: React.FunctionComponent = () => { const [dismissAlertErrors, setDismissAlertErrors] = useState(false); const [editFlyoutVisible, setEditFlyoutVisibility] = useState(false); const [currentRuleToEdit, setCurrentRuleToEdit] = useState(null); + const [tagPopoverOpenIndex, setTagPopoverOpenIndex] = useState(-1); const [sort, setSort] = useState['sort']>({ field: 'name', @@ -334,7 +342,7 @@ export const AlertsList: React.FunctionComponent = () => { 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.enabledTitle', { defaultMessage: 'Enabled' } ), - width: '90px', + width: '50px', render(_enabled: boolean | undefined, item: AlertTableItem) { return ( { const checkEnabledResult = checkAlertTypeEnabled(ruleType); const link = ( <> - { - history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); - }} - > - {name} - + + + + + { + history.push(routeToRuleDetails.replace(`:ruleId`, alert.id)); + }} + > + {name} + + + + {!checkEnabledResult.isEnabled && ( + + )} + + + + + + {alert.alertType} + + + ); - return checkEnabledResult.isEnabled ? ( - link - ) : ( + return ( <> {link} - + {alert.enabled && alert.muteAll && ( + + + + )} ); }, }, { - field: 'executionStatus.status', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', - { defaultMessage: 'Status' } - ), - sortable: true, - truncateText: false, - width: '120px', - 'data-test-subj': 'alertsTableCell-status', - render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { - return renderAlertExecutionStatus(item.executionStatus, item); - }, - }, - { - field: 'alertType', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.alertTypeTitle', - { defaultMessage: 'Type' } - ), + field: 'tags', + name: '', sortable: false, - truncateText: true, - render: (_count: number, item: AlertTableItem) => ( - {item.alertType} - ), - 'data-test-subj': 'alertsTableCell-alertType', + width: '50px', + 'data-test-subj': 'alertsTableCell-tagsPopover', + render: (tags: string[], item: AlertTableItem) => { + return tags.length > 0 ? ( + setTagPopoverOpenIndex(item.index)} + onClickAriaLabel="Tags" + iconOnClick={() => setTagPopoverOpenIndex(item.index)} + iconOnClickAriaLabel="Tags" + > + {tags.length} + + } + anchorPosition="upCenter" + isOpen={tagPopoverOpenIndex === item.index} + closePopover={() => setTagPopoverOpenIndex(-1)} + > + Tags +
+ {tags.map((tag: string, index: number) => ( + + {tag} + + ))} + + ) : null; + }, }, { - field: 'tagsText', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.tagsText', - { defaultMessage: 'Tags' } - ), - sortable: false, - 'data-test-subj': 'alertsTableCell-tagsText', - render: (_count: number, item: AlertTableItem) => ( -
- {item.tagsText} -
- ), + field: 'executionStatus.lastExecutionDate', + name: 'Last run', + sortable: true, + width: '15%', + 'data-test-subj': 'alertsTableCell-lastExecutionDate', + render: (date: Date) => { + if (date) { + return ( + <> + + + {moment(date).format('MMM D, YYYY HH:mm:ssa')} + + + + {moment(date).fromNow()} + + + + + ); + } + }, }, { field: 'schedule.interval', width: '6%', name: i18n.translate( 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.scheduleTitle', - { defaultMessage: 'Runs every' } + { defaultMessage: 'Interval' } ), sortable: false, truncateText: false, 'data-test-subj': 'alertsTableCell-interval', + render: (interval: string) => formatDuration(interval), }, { - width: '9%', - name: i18n.translate( - 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.actionsTitle', - { defaultMessage: 'Actions' } + field: 'executionStatus.lastDuration', + width: '12%', + name: ( + + + Duration{' '} + + + ), - render: (item: AlertTableItem) => { + sortable: true, + truncateText: false, + 'data-test-subj': 'alertsTableCell-duration', + render: (value: number, item: AlertTableItem) => { + const ruleTypeTimeout: string | undefined = alertTypesState.data.get( + item.alertTypeId + )?.ruleTaskTimeout; + const ruleTypeTimeoutMillis: number | undefined = ruleTypeTimeout + ? parseDuration(ruleTypeTimeout) + : undefined; + const showDurationWarning: boolean = ruleTypeTimeoutMillis + ? value > ruleTypeTimeoutMillis + : false; + const duration = moment.duration(value); + const durationString = [duration.hours(), duration.minutes(), duration.seconds()] + .map((v: number) => padStart(`${v}`, 2, '0')) + .join(':'); + + // add millis + const millisString = padStart(`${duration.milliseconds()}`, 3, '0'); return ( - - {item.actionsCount} - -
- {item.muteAll ? ( - - - - ) : null} -
-
-
+ <> + {`${durationString}.${millisString}`} + {showDurationWarning && ( + + )} + ); }, - 'data-test-subj': 'alertsTableCell-actions', + }, + { + field: 'executionStatus.status', + name: i18n.translate( + 'xpack.triggersActionsUI.sections.alertsList.alertsListTable.columns.statusTitle', + { defaultMessage: 'Status' } + ), + sortable: true, + truncateText: false, + width: '120px', + 'data-test-subj': 'alertsTableCell-status', + render: (_executionStatus: AlertExecutionStatus, item: AlertTableItem) => { + return renderAlertExecutionStatus(item.executionStatus, item); + }, }, { name: '', @@ -669,7 +777,7 @@ export const AlertsList: React.FunctionComponent = () => { - + {!dismissAlertErrors && alertsStatusesTotal.error > 0 ? ( @@ -679,9 +787,9 @@ export const AlertsList: React.FunctionComponent = () => { title={ } @@ -726,10 +834,10 @@ export const AlertsList: React.FunctionComponent = () => { @@ -737,46 +845,45 @@ export const AlertsList: React.FunctionComponent = () => { - + - + - {/* Large to remain consistent with ActionsList table spacing */} - + { setAlertsState({ ...alertsState, isLoading }); }} /> - + {loadedItems.length || isFilterApplied ? ( table ) : alertTypesState.isLoading || alertsState.isLoading ? ( @@ -958,10 +1065,10 @@ function convertAlertsToTableItems( ruleTypeIndex: RuleTypeIndex, canExecuteActions: boolean ) { - return alerts.map((alert) => ({ + return alerts.map((alert, index: number) => ({ ...alert, + index, actionsCount: alert.actions.length, - tagsText: alert.tags.join(', '), alertType: ruleTypeIndex.get(alert.alertTypeId)?.name ?? alert.alertTypeId, isEditable: hasAllPrivilege(alert, ruleTypeIndex.get(alert.alertTypeId)) && diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx index 5a06b03311cbe..807fe65a561f8 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/collapsed_item_actions.test.tsx @@ -75,7 +75,7 @@ describe('CollapsedItemActions', () => { lastExecutionDate: new Date('2020-08-20T19:23:38Z'), }, actionsCount: 1, - tagsText: 'tag1', + index: 0, alertType: 'Test Alert Type', isEditable: true, enabledInLicense: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx index bfa760b65ed4e..da75faeda95e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_list/components/rule_enabled_switch.test.tsx @@ -40,7 +40,7 @@ describe('RuleEnabledSwitch', () => { enabledInLicense: true, isEditable: false, notifyWhen: null, - tagsText: 'test', + index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), }, onAlertChanged: jest.fn(), @@ -84,7 +84,7 @@ describe('RuleEnabledSwitch', () => { enabledInLicense: true, isEditable: true, notifyWhen: null, - tagsText: 'test', + index: 0, updatedAt: new Date('2020-08-20T19:23:38Z'), }, }} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 2ef20f36b7ca9..a78d1d52de0bd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -197,6 +197,7 @@ export interface AlertType< | 'minimumLicenseRequired' | 'recoveryActionGroup' | 'defaultActionGroupId' + | 'ruleTaskTimeout' | 'defaultScheduleInterval' | 'minimumScheduleInterval' > { @@ -211,7 +212,7 @@ export type AlertUpdates = Omit; export interface AlertTableItem extends Alert { alertType: AlertType['name']; - tagsText: string; + index: number; actionsCount: number; isEditable: boolean; enabledInLicense: boolean; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts index f52f0977a630b..b070219410fd9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rule_types.ts @@ -36,6 +36,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { name: 'Recovered', }, enabled_in_license: true, + rule_task_timeout: '5m', }; const expectedRestrictedNoOpType = { @@ -59,6 +60,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimum_license_required: 'basic', is_exportable: true, enabled_in_license: true, + rule_task_timeout: '5m', }; describe('rule_types', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts index 86a0e269b26d6..77638ed90fbe4 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/rule_types.ts @@ -44,6 +44,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimum_license_required: 'basic', is_exportable: true, enabled_in_license: true, + rule_task_timeout: '5m', }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); @@ -129,6 +130,7 @@ export default function listAlertTypes({ getService }: FtrProviderContext) { minimumLicenseRequired: 'basic', isExportable: true, enabledInLicense: true, + ruleTaskTimeout: '5m', }); expect(Object.keys(authorizedConsumers)).to.contain('alertsFixture'); }); diff --git a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts index 3f36032523e8b..dede481669664 100644 --- a/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts +++ b/x-pack/test/functional_with_es_ssl/apps/triggers_actions_ui/alerts_list.ts @@ -88,9 +88,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); expect(searchResults).to.have.length(3); - expect(searchResults[0].name).to.eql('a'); - expect(searchResults[1].name).to.eql('b'); - expect(searchResults[2].name).to.eql('c'); + // rule list shows name and rule type id + expect(searchResults[0].name).to.eql('aTest: Noop'); + expect(searchResults[1].name).to.eql('bTest: Noop'); + expect(searchResults[2].name).to.eql('cTest: Noop'); }); it('should search for alert', async () => { @@ -99,67 +100,54 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await pageObjects.triggersActionsUI.searchAlerts(createdAlert.name); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('2'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should update alert list on the search clear button click', async () => { await createAlert({ name: 'b' }); - await createAlert({ name: 'c' }); + await createAlert({ name: 'c', tags: [] }); await refreshAlertsList(); await pageObjects.triggersActionsUI.searchAlerts('b'); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: 'b', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal('bTest: Noop'); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('2'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + const searchClearButton = await find.byCssSelector('.euiFormControlLayoutClearButton'); await searchClearButton.click(); await find.byCssSelector( '.euiBasicTable[data-test-subj="alertsList"]:not(.euiBasicTable-loading)' ); const searchResultsAfterClear = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResultsAfterClear).to.eql([ - { - name: 'b', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - { - name: 'c', - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResultsAfterClear.length).to.equal(2); + expect(searchResultsAfterClear[0].name).to.equal('bTest: Noop'); + expect(searchResultsAfterClear[0].interval).to.equal('1 min'); + expect(searchResultsAfterClear[0].tags).to.equal('2'); + expect(searchResultsAfterClear[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); + expect(searchResultsAfterClear[1].name).to.equal('cTest: Noop'); + expect(searchResultsAfterClear[1].interval).to.equal('1 min'); + expect(searchResultsAfterClear[1].tags).to.equal(''); + expect(searchResultsAfterClear[1].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should search for tags', async () => { - const createdAlert = await createAlert(); + const createdAlert = await createAlert({ tags: ['tag', 'tagtag', 'taggity tag'] }); await refreshAlertsList(); - await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} foo`); + await pageObjects.triggersActionsUI.searchAlerts(`${createdAlert.name} tag`); const searchResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(searchResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(searchResults.length).to.equal(1); + expect(searchResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(searchResults[0].interval).to.equal('1 min'); + expect(searchResults[0].tags).to.equal('3'); + expect(searchResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); it('should display an empty list when search did not return any alerts', async () => { @@ -385,15 +373,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterErrorOnlyResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); - expect(filterErrorOnlyResults).to.eql([ - { - name: failingAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Failing', - interval: '30s', - status: 'Error', - }, - ]); + expect(filterErrorOnlyResults.length).to.equal(1); + expect(filterErrorOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); + expect(filterErrorOnlyResults[0].interval).to.equal('30 sec'); + expect(filterErrorOnlyResults[0].status).to.equal('Error'); + expect(filterErrorOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); @@ -402,15 +386,11 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { await refreshAlertsList(); const refreshResults = await pageObjects.triggersActionsUI.getAlertsListWithStatus(); - expect(refreshResults).to.eql([ - { - name: createdAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - status: 'Ok', - }, - ]); + expect(refreshResults.length).to.equal(1); + expect(refreshResults[0].name).to.equal(`${createdAlert.name}Test: Noop`); + expect(refreshResults[0].interval).to.equal('1 min'); + expect(refreshResults[0].status).to.equal('Ok'); + expect(refreshResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); const alertsErrorBannerWhenNoErrors = await find.allByCssSelector( @@ -451,14 +431,10 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterFailingAlertOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(filterFailingAlertOnlyResults).to.eql([ - { - name: failingAlert.name, - tagsText: 'foo, bar', - alertType: 'Test: Failing', - interval: '30s', - }, - ]); + expect(filterFailingAlertOnlyResults.length).to.equal(1); + expect(filterFailingAlertOnlyResults[0].name).to.equal(`${failingAlert.name}Test: Failing`); + expect(filterFailingAlertOnlyResults[0].interval).to.equal('30 sec'); + expect(filterFailingAlertOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); @@ -480,14 +456,12 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { await retry.try(async () => { const filterWithSlackOnlyResults = await pageObjects.triggersActionsUI.getAlertsList(); - expect(filterWithSlackOnlyResults).to.eql([ - { - name: noopAlertWithAction.name, - tagsText: 'foo, bar', - alertType: 'Test: Noop', - interval: '1m', - }, - ]); + expect(filterWithSlackOnlyResults.length).to.equal(1); + expect(filterWithSlackOnlyResults[0].name).to.equal( + `${noopAlertWithAction.name}Test: Noop` + ); + expect(filterWithSlackOnlyResults[0].interval).to.equal('1 min'); + expect(filterWithSlackOnlyResults[0].duration).to.match(/\d{2}:\d{2}:\d{2}.\d{3}/); }); }); }); diff --git a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts index 890315698f74c..a49873c6d47b5 100644 --- a/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts +++ b/x-pack/test/functional_with_es_ssl/page_objects/triggers_actions_ui_page.ts @@ -22,18 +22,18 @@ export function TriggersActionsPageProvider({ getService }: FtrProviderContext) function getRowItemData(row: CustomCheerio, $: CustomCheerioStatic) { return { name: $(row).findTestSubject('alertsTableCell-name').find('.euiTableCellContent').text(), - tagsText: $(row) - .findTestSubject('alertsTableCell-tagsText') - .find('.euiTableCellContent') - .text(), - alertType: $(row) - .findTestSubject('alertsTableCell-alertType') + duration: $(row) + .findTestSubject('alertsTableCell-duration') .find('.euiTableCellContent') .text(), interval: $(row) .findTestSubject('alertsTableCell-interval') .find('.euiTableCellContent') .text(), + tags: $(row) + .findTestSubject('alertsTableCell-tagsPopover') + .find('.euiTableCellContent') + .text(), }; } From 243c2133aff98b21aef7e12ee65d50539dded4db Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Mon, 11 Oct 2021 21:35:00 -0500 Subject: [PATCH 042/287] [nit][pre-req] Strongly-type Shipper and Category (#114412) --- .../custom_integrations/common/index.ts | 61 ++++++++++++++++--- 1 file changed, 54 insertions(+), 7 deletions(-) diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index e2408d3124604..9af7c4ccd4633 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -9,13 +9,11 @@ export const PLUGIN_ID = 'customIntegrations'; export const PLUGIN_NAME = 'customIntegrations'; -export interface IntegrationCategoryCount { - count: number; - id: IntegrationCategory; -} - +/** + * A map of category names and their corresponding titles. + */ +// TODO: consider i18n export const INTEGRATION_CATEGORY_DISPLAY = { - // Known EPR aws: 'AWS', azure: 'Azure', cloud: 'Cloud', @@ -49,13 +47,62 @@ export const INTEGRATION_CATEGORY_DISPLAY = { updates_available: 'Updates available', }; +/** + * A category applicable to an Integration. + */ export type IntegrationCategory = keyof typeof INTEGRATION_CATEGORY_DISPLAY; +/** + * The list of all available categories. + */ +// This `as` is necessary, as Object.keys cannot be strongly typed. +// see: https://github.com/Microsoft/TypeScript/issues/12870 +export const category = Object.keys(INTEGRATION_CATEGORY_DISPLAY) as IntegrationCategory[]; + +/** + * An object containing the id of an `IntegrationCategory` and the count of all Integrations in that category. + */ +export interface IntegrationCategoryCount { + count: number; + id: IntegrationCategory; +} + +/** + * A map of shipper names and their corresponding titles. + */ +// TODO: consider i18n +export const SHIPPER_DISPLAY = { + beats: 'Beats', + language_clients: 'Language clients', + other: 'Other', + sample_data: 'Sample data', + tests: 'Tests', + tutorial: 'Tutorials', +}; + +/** + * A shipper-- an internal or external system capable of storing data in ES/Kibana-- applicable to an Integration. + */ +export type Shipper = keyof typeof SHIPPER_DISPLAY; + +/** + * The list of all known shippers. + */ +// This `as` is necessary, as Object.keys cannot be strongly typed. +// see: https://github.com/Microsoft/TypeScript/issues/12870 +export const shipper = Object.keys(SHIPPER_DISPLAY) as Shipper[]; + +/** + * An icon representing an Integration. + */ export interface CustomIntegrationIcon { src: string; type: 'eui' | 'svg'; } +/** + * A definition of a dataintegration, which can be registered with Kibana. + */ export interface CustomIntegration { id: string; title: string; @@ -65,7 +112,7 @@ export interface CustomIntegration { isBeta: boolean; icons: CustomIntegrationIcon[]; categories: IntegrationCategory[]; - shipper: string; + shipper: Shipper; eprOverlap?: string; // name of the equivalent Elastic Agent integration in EPR. e.g. a beat module can correspond to an EPR-package, or an APM-tutorial. When completed, Integrations-UX can preferentially show the EPR-package, rather than the custom-integration } From 7c087fea8226ab8bee96cb44e1a5111701b4a904 Mon Sep 17 00:00:00 2001 From: Spencer Date: Mon, 11 Oct 2021 23:01:02 -0500 Subject: [PATCH 043/287] [ci-stats] support sending meta with metrics (#114198) * [ci-stats] support sending meta with metrics * update kbn/pm dist * improve comments stat * update kbn/pm dist Co-authored-by: spalger Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../ci_stats_reporter/ci_stats_reporter.ts | 51 ++++++++++++++++--- packages/kbn-pm/dist/index.js | 25 +++++++-- 2 files changed, 64 insertions(+), 12 deletions(-) diff --git a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts index fe48ce99e6857..05f54e8d38c8c 100644 --- a/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts +++ b/packages/kbn-dev-utils/src/ci_stats_reporter/ci_stats_reporter.ts @@ -22,24 +22,43 @@ import { parseConfig, Config } from './ci_stats_config'; const BASE_URL = 'https://ci-stats.kibana.dev'; export interface CiStatsMetric { + /** Top-level categorization for the metric, e.g. "page load bundle size" */ group: string; + /** Specific sub-set of the "group", e.g. "dashboard" */ id: string; + /** integer value recorded as the value of this metric */ value: number; + /** optional limit which will generate an error on PRs when the metric exceeds the limit */ limit?: number; + /** + * path, relative to the repo, where the config file contianing limits + * is kept. Linked from PR comments instructing contributors how to fix + * their PRs. + */ limitConfigPath?: string; + /** Arbitrary key-value pairs which can be used for additional filtering/reporting */ + meta?: CiStatsMetadata; } -export interface CiStatsTimingMetadata { +export interface CiStatsMetadata { + /** + * Arbitrary key-value pairs which can be attached to CiStatsTiming and CiStatsMetric + * objects stored in the ci-stats service + */ [key: string]: string | string[] | number | boolean | undefined; } export interface CiStatsTiming { + /** Top-level categorization for the timing, e.g. "scripts/foo", process type, etc. */ group: string; + /** Specific timing (witin the "group" being tracked) e.g. "total" */ id: string; + /** time in milliseconds which should be recorded */ ms: number; - meta?: CiStatsTimingMetadata; + /** hash of key-value pairs which will be stored with the timing for additional filtering and reporting */ + meta?: CiStatsMetadata; } -export interface ReqOptions { +interface ReqOptions { auth: boolean; path: string; body: any; @@ -54,17 +73,34 @@ export interface TimingsOptions { /** value of data/uuid, automatically loaded if not specified */ kibanaUuid?: string | null; } + +export interface MetricsOptions { + /** Default metadata to add to each metric */ + defaultMeta?: CiStatsMetadata; +} export class CiStatsReporter { + /** + * Create a CiStatsReporter by inspecting the ENV for the necessary config + */ static fromEnv(log: ToolingLog) { return new CiStatsReporter(parseConfig(log), log); } constructor(private config: Config | undefined, private log: ToolingLog) {} + /** + * Determine if CI_STATS is explicitly disabled by the environment. To determine + * if the CiStatsReporter has enough information in the environment to send metrics + * for builds use #hasBuildConfig(). + */ isEnabled() { return process.env.CI_STATS_DISABLED !== 'true'; } + /** + * Determines if the CiStatsReporter is disabled by the environment, or properly + * configured and able to send stats + */ hasBuildConfig() { return this.isEnabled() && !!this.config?.apiToken && !!this.config?.buildId; } @@ -103,7 +139,7 @@ export class CiStatsReporter { const memUsage = process.memoryUsage(); const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false; - const defaultMetadata = { + const defaultMeta = { kibanaUuid, isElasticCommitter, committerHash: email @@ -127,7 +163,7 @@ export class CiStatsReporter { totalMem: Os.totalmem(), }; - this.log.debug('CIStatsReporter committerHash: %s', defaultMetadata.committerHash); + this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash); return await this.req({ auth: !!buildId, @@ -135,8 +171,8 @@ export class CiStatsReporter { body: { buildId, upstreamBranch, + defaultMeta, timings, - defaultMetadata, }, bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings`, }); @@ -146,7 +182,7 @@ export class CiStatsReporter { * Report metrics data to the ci-stats service. If running outside of CI this method * does nothing as metrics can only be reported when associated with a specific CI build. */ - async metrics(metrics: CiStatsMetric[]) { + async metrics(metrics: CiStatsMetric[], options?: MetricsOptions) { if (!this.hasBuildConfig()) { return; } @@ -162,6 +198,7 @@ export class CiStatsReporter { path: '/v1/metrics', body: { buildId, + defaultMeta: options?.defaultMeta, metrics, }, bodyDesc: `metrics: ${metrics diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index a231fe21ea838..3c0e3fa94be02 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -9010,6 +9010,9 @@ var _ci_stats_config = __webpack_require__(218); const BASE_URL = 'https://ci-stats.kibana.dev'; class CiStatsReporter { + /** + * Create a CiStatsReporter by inspecting the ENV for the necessary config + */ static fromEnv(log) { return new CiStatsReporter((0, _ci_stats_config.parseConfig)(log), log); } @@ -9018,10 +9021,21 @@ class CiStatsReporter { this.config = config; this.log = log; } + /** + * Determine if CI_STATS is explicitly disabled by the environment. To determine + * if the CiStatsReporter has enough information in the environment to send metrics + * for builds use #hasBuildConfig(). + */ + isEnabled() { return process.env.CI_STATS_DISABLED !== 'true'; } + /** + * Determines if the CiStatsReporter is disabled by the environment, or properly + * configured and able to send stats + */ + hasBuildConfig() { var _this$config, _this$config2; @@ -9069,7 +9083,7 @@ class CiStatsReporter { const memUsage = process.memoryUsage(); const isElasticCommitter = email && email.endsWith('@elastic.co') ? true : false; - const defaultMetadata = { + const defaultMeta = { kibanaUuid, isElasticCommitter, committerHash: email ? _crypto.default.createHash('sha256').update(email).digest('hex').substring(0, 20) : undefined, @@ -9090,15 +9104,15 @@ class CiStatsReporter { osRelease: _os.default.release(), totalMem: _os.default.totalmem() }; - this.log.debug('CIStatsReporter committerHash: %s', defaultMetadata.committerHash); + this.log.debug('CIStatsReporter committerHash: %s', defaultMeta.committerHash); return await this.req({ auth: !!buildId, path: '/v1/timings', body: { buildId, upstreamBranch, - timings, - defaultMetadata + defaultMeta, + timings }, bodyDesc: timings.length === 1 ? `${timings.length} timing` : `${timings.length} timings` }); @@ -9109,7 +9123,7 @@ class CiStatsReporter { */ - async metrics(metrics) { + async metrics(metrics, options) { var _this$config4; if (!this.hasBuildConfig()) { @@ -9127,6 +9141,7 @@ class CiStatsReporter { path: '/v1/metrics', body: { buildId, + defaultMeta: options === null || options === void 0 ? void 0 : options.defaultMeta, metrics }, bodyDesc: `metrics: ${metrics.map(({ From ab5ecc4e13a92c28980deabdae579b69c252dffe Mon Sep 17 00:00:00 2001 From: Clint Andrew Hall Date: Tue, 12 Oct 2021 00:07:16 -0500 Subject: [PATCH 044/287] [nit][pre-req] Split EPM Home into components (#114431) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/package_list_grid.stories.tsx | 4 +- .../epm/components/package_list_grid.tsx | 12 +- .../epm/screens/home/available_packages.tsx | 212 +++++++++++ .../epm/screens/home/category_facets.tsx | 16 +- .../sections/epm/screens/home/index.tsx | 360 +----------------- .../epm/screens/home/installed_packages.tsx | 159 ++++++++ 6 files changed, 408 insertions(+), 355 deletions(-) create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx create mode 100644 x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx index c355eb8607f45..e4bd1da842867 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { action } from '@storybook/addon-actions'; -import type { ListProps } from './package_list_grid'; +import type { Props } from './package_list_grid'; import { PackageListGrid } from './package_list_grid'; export default { @@ -17,7 +17,7 @@ export default { title: 'Sections/EPM/Package List Grid', }; -type Args = Pick; +type Args = Pick; const args: Args = { title: 'Installed integrations', diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx index 109f7500f160b..1cffd5292b6a2 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.tsx @@ -14,7 +14,6 @@ import { EuiLink, EuiSpacer, EuiTitle, - // @ts-ignore EuiSearchBar, EuiText, } from '@elastic/eui'; @@ -29,7 +28,7 @@ import type { IntegrationCardItem } from '../../../../../../common/types/models' import { PackageCard } from './package_card'; -export interface ListProps { +export interface Props { isLoading?: boolean; controls?: ReactNode; title: string; @@ -51,7 +50,7 @@ export function PackageListGrid({ setSelectedCategory, showMissingIntegrationMessage = false, callout, -}: ListProps) { +}: Props) { const [searchTerm, setSearchTerm] = useState(initialSearch || ''); const localSearchRef = useLocalSearch(list); @@ -107,7 +106,12 @@ export function PackageListGrid({ }} onChange={onQueryChange} /> - {callout ? callout : null} + {callout ? ( + <> + + {callout} + + ) : null} {gridContent} {showMissingIntegrationMessage && ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx new file mode 100644 index 0000000000000..8aef9121bf67d --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -0,0 +1,212 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { useLocation, useHistory, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; + +import { pagePathGetters } from '../../../../constants'; +import { + useGetCategories, + useGetPackages, + useBreadcrumbs, + useGetAppendCustomIntegrations, + useGetReplacementCustomIntegrations, + useLink, +} from '../../../../hooks'; +import { doesPackageHaveIntegrations } from '../../../../services'; +import type { PackageList } from '../../../../types'; +import { PackageListGrid } from '../../components/package_list_grid'; + +import type { CustomIntegration } from '../../../../../../../../../../src/plugins/custom_integrations/common'; + +import type { PackageListItem } from '../../../../types'; + +import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common'; + +import { useMergeEprPackagesWithReplacements } from '../../../../../../hooks/use_merge_epr_with_replacements'; + +import { mergeAndReplaceCategoryCounts } from './util'; +import { CategoryFacets } from './category_facets'; +import type { CategoryFacet } from './category_facets'; + +import type { CategoryParams } from '.'; +import { getParams, categoryExists, mapToCard } from '.'; + +// Packages can export multiple integrations, aka `policy_templates` +// In the case where packages ship >1 `policy_templates`, we flatten out the +// list of packages by bringing all integrations to top-level so that +// each integration is displayed as its own tile +const packageListToIntegrationsList = (packages: PackageList): PackageList => { + return packages.reduce((acc: PackageList, pkg) => { + const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg; + return [ + ...acc, + restOfPackage, + ...(doesPackageHaveIntegrations(pkg) + ? policyTemplates.map((integration) => { + const { name, title, description, icons } = integration; + return { + ...restOfPackage, + id: `${restOfPackage}-${name}`, + integration: name, + title, + description, + icons: icons || restOfPackage.icons, + }; + }) + : []), + ]; + }, []); +}; + +const title = i18n.translate('xpack.fleet.epmList.allTitle', { + defaultMessage: 'Browse by category', +}); + +// TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http` +// or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. +export const AvailablePackages: React.FC = memo(() => { + useBreadcrumbs('integrations_all'); + + const { selectedCategory, searchParam } = getParams( + useParams(), + useLocation().search + ); + + const history = useHistory(); + + const { getHref, getAbsolutePath } = useLink(); + + function setSelectedCategory(categoryId: string) { + const url = pagePathGetters.integrations_all({ + category: categoryId, + searchTerm: searchParam, + })[1]; + history.push(url); + } + + function setSearchTerm(search: string) { + // Use .replace so the browser's back button is not tied to single keystroke + history.replace( + pagePathGetters.integrations_all({ category: selectedCategory, searchTerm: search })[1] + ); + } + + const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({ + category: '', + }); + + const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ + category: selectedCategory, + }); + + const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({ + include_policy_templates: true, + }); + + const eprPackages = useMemo( + () => packageListToIntegrationsList(categoryPackagesRes?.response || []), + [categoryPackagesRes] + ); + + const allEprPackages = useMemo( + () => packageListToIntegrationsList(allCategoryPackagesRes?.response || []), + [allCategoryPackagesRes] + ); + + const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations(); + + const mergedEprPackages: Array = + useMergeEprPackagesWithReplacements( + eprPackages || [], + replacementCustomIntegrations || [], + selectedCategory as IntegrationCategory + ); + + const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } = + useGetAppendCustomIntegrations(); + + const filteredAddableIntegrations = appendCustomIntegrations + ? appendCustomIntegrations.filter((integration: CustomIntegration) => { + if (!selectedCategory) { + return true; + } + return integration.categories.indexOf(selectedCategory as IntegrationCategory) >= 0; + }) + : []; + + const eprAndCustomPackages: Array = [ + ...mergedEprPackages, + ...filteredAddableIntegrations, + ]; + + eprAndCustomPackages.sort((a, b) => { + return a.title.localeCompare(b.title); + }); + + const categories = useMemo(() => { + const eprAndCustomCategories: CategoryFacet[] = + isLoadingCategories || + isLoadingAppendCustomIntegrations || + !appendCustomIntegrations || + !categoriesRes + ? [] + : mergeAndReplaceCategoryCounts( + categoriesRes.response as CategoryFacet[], + appendCustomIntegrations + ); + + return [ + { + id: '', + count: (allEprPackages?.length || 0) + (appendCustomIntegrations?.length || 0), + }, + ...(eprAndCustomCategories ? eprAndCustomCategories : []), + ] as CategoryFacet[]; + }, [ + allEprPackages?.length, + appendCustomIntegrations, + categoriesRes, + isLoadingAppendCustomIntegrations, + isLoadingCategories, + ]); + + if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) { + history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]); + return null; + } + + const controls = categories ? ( + { + setSelectedCategory(id); + }} + /> + ) : null; + + const cards = eprAndCustomPackages.map((item) => { + return mapToCard(getAbsolutePath, getHref, item); + }); + + return ( + + ); +}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx index d60ccd93b8db1..3eba17d6627a1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/category_facets.tsx @@ -21,19 +21,21 @@ interface ALL_CATEGORY { export type CategoryFacet = IntegrationCategoryCount | ALL_CATEGORY; +export interface Props { + showCounts: boolean; + isLoading?: boolean; + categories: CategoryFacet[]; + selectedCategory: string; + onCategoryChange: (category: CategoryFacet) => unknown; +} + export function CategoryFacets({ showCounts, isLoading, categories, selectedCategory, onCategoryChange, -}: { - showCounts: boolean; - isLoading?: boolean; - categories: CategoryFacet[]; - selectedCategory: string; - onCategoryChange: (category: CategoryFacet) => unknown; -}) { +}: Props) { const controls = ( {isLoading ? ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index 06cf85699bf67..bbebf9e90b16c 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -5,34 +5,12 @@ * 2.0. */ -import React, { memo, useMemo, Fragment } from 'react'; -import { Switch, Route, useLocation, useHistory, useParams } from 'react-router-dom'; -import semverLt from 'semver/functions/lt'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { memo } from 'react'; +import { Switch, Route } from 'react-router-dom'; -import { EuiCallOut, EuiLink, EuiSpacer } from '@elastic/eui'; - -import { installationStatuses } from '../../../../../../../common/constants'; import type { DynamicPage, DynamicPagePathValues, StaticPage } from '../../../../constants'; -import { - INTEGRATIONS_ROUTING_PATHS, - INTEGRATIONS_SEARCH_QUERYPARAM, - pagePathGetters, -} from '../../../../constants'; -import { - useGetCategories, - useGetPackages, - useBreadcrumbs, - useGetAppendCustomIntegrations, - useGetReplacementCustomIntegrations, - useLink, - useStartServices, -} from '../../../../hooks'; -import { doesPackageHaveIntegrations } from '../../../../services'; +import { INTEGRATIONS_ROUTING_PATHS, INTEGRATIONS_SEARCH_QUERYPARAM } from '../../../../constants'; import { DefaultLayout } from '../../../../layouts'; -import type { PackageList } from '../../../../types'; -import { PackageListGrid } from '../../components/package_list_grid'; import type { CustomIntegration } from '../../../../../../../../../../src/plugins/custom_integrations/common'; @@ -40,47 +18,47 @@ import type { PackageListItem } from '../../../../types'; import type { IntegrationCardItem } from '../../../../../../../common/types/models'; -import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common'; - -import { useMergeEprPackagesWithReplacements } from '../../../../../../hooks/use_merge_epr_with_replacements'; - -import { mergeAndReplaceCategoryCounts } from './util'; -import { CategoryFacets } from './category_facets'; import type { CategoryFacet } from './category_facets'; +import { InstalledPackages } from './installed_packages'; +import { AvailablePackages } from './available_packages'; export interface CategoryParams { category?: string; } -function getParams(params: CategoryParams, search: string) { +export const getParams = (params: CategoryParams, search: string) => { const { category } = params; const selectedCategory = category || ''; const queryParams = new URLSearchParams(search); const searchParam = queryParams.get(INTEGRATIONS_SEARCH_QUERYPARAM) || ''; return { selectedCategory, searchParam }; -} +}; -function categoryExists(category: string, categories: CategoryFacet[]) { +export const categoryExists = (category: string, categories: CategoryFacet[]) => { return categories.some((c) => c.id === category); -} +}; -function mapToCard( +export const mapToCard = ( getAbsolutePath: (p: string) => string, getHref: (page: StaticPage | DynamicPage, values?: DynamicPagePathValues) => string, item: CustomIntegration | PackageListItem -): IntegrationCardItem { +): IntegrationCardItem => { let uiInternalPathUrl; + if (item.type === 'ui_link') { uiInternalPathUrl = getAbsolutePath(item.uiInternalPath); } else { let urlVersion = item.version; + if ('savedObject' in item) { urlVersion = item.savedObject.attributes.version || item.version; } + const url = getHref('integration_details_overview', { pkgkey: `${item.name}-${urlVersion}`, ...(item.integration ? { integration: item.integration } : {}), }); + uiInternalPathUrl = url; } @@ -88,14 +66,14 @@ function mapToCard( id: `${item.type === 'ui_link' ? 'ui_link' : 'epr'}-${item.id}`, description: item.description, icons: !item.icons || !item.icons.length ? [] : item.icons, + title: item.title, + url: uiInternalPathUrl, integration: 'integration' in item ? item.integration || '' : '', name: 'name' in item ? item.name || '' : '', - title: item.title, version: 'version' in item ? item.version || '' : '', release: 'release' in item ? item.release : undefined, - url: uiInternalPathUrl, }; -} +}; export const EPMHomePage: React.FC = memo(() => { return ( @@ -113,305 +91,3 @@ export const EPMHomePage: React.FC = memo(() => { ); }); - -// Packages can export multiple integrations, aka `policy_templates` -// In the case where packages ship >1 `policy_templates`, we flatten out the -// list of packages by bringing all integrations to top-level so that -// each integration is displayed as its own tile -const packageListToIntegrationsList = (packages: PackageList): PackageList => { - return packages.reduce((acc: PackageList, pkg) => { - const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg; - return [ - ...acc, - restOfPackage, - ...(doesPackageHaveIntegrations(pkg) - ? policyTemplates.map((integration) => { - const { name, title, description, icons } = integration; - return { - ...restOfPackage, - id: `${restOfPackage}-${name}`, - integration: name, - title, - description, - icons: icons || restOfPackage.icons, - }; - }) - : []), - ]; - }, []); -}; - -const InstalledPackages: React.FC = memo(() => { - useBreadcrumbs('integrations_installed'); - const { data: allPackages, isLoading: isLoadingPackages } = useGetPackages({ - experimental: true, - }); - const { getHref, getAbsolutePath } = useLink(); - const { docLinks } = useStartServices(); - - const { selectedCategory, searchParam } = getParams( - useParams(), - useLocation().search - ); - const history = useHistory(); - function setSelectedCategory(categoryId: string) { - const url = pagePathGetters.integrations_installed({ - category: categoryId, - searchTerm: searchParam, - })[1]; - history.push(url); - } - function setSearchTerm(search: string) { - // Use .replace so the browser's back button is not tied to single keystroke - history.replace( - pagePathGetters.integrations_installed({ - category: selectedCategory, - searchTerm: search, - })[1] - ); - } - - const allInstalledPackages = useMemo( - () => - (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), - [allPackages?.response] - ); - - const updatablePackages = useMemo( - () => - allInstalledPackages.filter( - (item) => - 'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version) - ), - [allInstalledPackages] - ); - - const title = useMemo( - () => - i18n.translate('xpack.fleet.epmList.installedTitle', { - defaultMessage: 'Installed integrations', - }), - [] - ); - - const categories: CategoryFacet[] = useMemo( - () => [ - { - id: '', - count: allInstalledPackages.length, - }, - { - id: 'updates_available', - count: updatablePackages.length, - }, - ], - [allInstalledPackages.length, updatablePackages.length] - ); - - if (!categoryExists(selectedCategory, categories)) { - history.replace( - pagePathGetters.integrations_installed({ category: '', searchTerm: searchParam })[1] - ); - return null; - } - - const controls = ( - setSelectedCategory(id)} - /> - ); - - const cards = ( - selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages - ).map((item) => { - return mapToCard(getAbsolutePath, getHref, item); - }); - - const link = ( - - {i18n.translate('xpack.fleet.epmList.availableCalloutBlogText', { - defaultMessage: 'announcement blog post', - })} - - ); - const calloutMessage = ( - - ); - - const callout = - selectedCategory === 'updates_available' ? null : ( - - - -

{calloutMessage}

-
-
- ); - - return ( - - ); -}); - -const AvailablePackages: React.FC = memo(() => { - useBreadcrumbs('integrations_all'); - const { selectedCategory, searchParam } = getParams( - useParams(), - useLocation().search - ); - const history = useHistory(); - const { getHref, getAbsolutePath } = useLink(); - - function setSelectedCategory(categoryId: string) { - const url = pagePathGetters.integrations_all({ - category: categoryId, - searchTerm: searchParam, - })[1]; - history.push(url); - } - function setSearchTerm(search: string) { - // Use .replace so the browser's back button is not tied to single keystroke - history.replace( - pagePathGetters.integrations_all({ category: selectedCategory, searchTerm: search })[1] - ); - } - - const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({ - category: '', - }); - const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ - category: selectedCategory, - }); - const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({ - include_policy_templates: true, - }); - - const eprPackages = useMemo( - () => packageListToIntegrationsList(categoryPackagesRes?.response || []), - [categoryPackagesRes] - ); - - const allEprPackages = useMemo( - () => packageListToIntegrationsList(allCategoryPackagesRes?.response || []), - [allCategoryPackagesRes] - ); - - const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations(); - - const mergedEprPackages: Array = - useMergeEprPackagesWithReplacements( - eprPackages || [], - replacementCustomIntegrations || [], - selectedCategory as IntegrationCategory - ); - - const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } = - useGetAppendCustomIntegrations(); - const filteredAddableIntegrations = appendCustomIntegrations - ? appendCustomIntegrations.filter((integration: CustomIntegration) => { - if (!selectedCategory) { - return true; - } - return integration.categories.indexOf(selectedCategory as IntegrationCategory) >= 0; - }) - : []; - - const title = useMemo( - () => - i18n.translate('xpack.fleet.epmList.allTitle', { - defaultMessage: 'Browse by category', - }), - [] - ); - - const eprAndCustomPackages: Array = [ - ...mergedEprPackages, - ...filteredAddableIntegrations, - ]; - eprAndCustomPackages.sort((a, b) => { - return a.title.localeCompare(b.title); - }); - - const categories = useMemo(() => { - const eprAndCustomCategories: CategoryFacet[] = - isLoadingCategories || - isLoadingAppendCustomIntegrations || - !appendCustomIntegrations || - !categoriesRes - ? [] - : mergeAndReplaceCategoryCounts( - categoriesRes.response as CategoryFacet[], - appendCustomIntegrations - ); - return [ - { - id: '', - count: (allEprPackages?.length || 0) + (appendCustomIntegrations?.length || 0), - }, - ...(eprAndCustomCategories ? eprAndCustomCategories : []), - ] as CategoryFacet[]; - }, [ - allEprPackages?.length, - appendCustomIntegrations, - categoriesRes, - isLoadingAppendCustomIntegrations, - isLoadingCategories, - ]); - - if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) { - history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]); - return null; - } - - const controls = categories ? ( - { - setSelectedCategory(id); - }} - /> - ) : null; - - const cards = eprAndCustomPackages.map((item) => { - return mapToCard(getAbsolutePath, getHref, item); - }); - - return ( - - ); -}); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx new file mode 100644 index 0000000000000..404e8820f90b7 --- /dev/null +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useMemo } from 'react'; +import { useLocation, useHistory, useParams } from 'react-router-dom'; +import semverLt from 'semver/functions/lt'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +import { EuiCallOut, EuiLink } from '@elastic/eui'; + +import { installationStatuses } from '../../../../../../../common/constants'; +import { pagePathGetters } from '../../../../constants'; +import { useGetPackages, useBreadcrumbs, useLink, useStartServices } from '../../../../hooks'; +import { PackageListGrid } from '../../components/package_list_grid'; + +import type { CategoryFacet } from './category_facets'; +import { CategoryFacets } from './category_facets'; + +import type { CategoryParams } from '.'; +import { getParams, categoryExists, mapToCard } from '.'; + +const AnnouncementLink = () => { + const { docLinks } = useStartServices(); + + return ( + + {i18n.translate('xpack.fleet.epmList.availableCalloutBlogText', { + defaultMessage: 'announcement blog post', + })} + + ); +}; + +const Callout = () => ( + +

+ , + }} + /> +

+
+); + +const title = i18n.translate('xpack.fleet.epmList.installedTitle', { + defaultMessage: 'Installed integrations', +}); + +// TODO: clintandrewhall - this component is hard to test due to the hooks, particularly those that use `http` +// or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. +export const InstalledPackages: React.FC = memo(() => { + useBreadcrumbs('integrations_installed'); + + const { data: allPackages, isLoading } = useGetPackages({ + experimental: true, + }); + + const { getHref, getAbsolutePath } = useLink(); + + const { selectedCategory, searchParam } = getParams( + useParams(), + useLocation().search + ); + + const history = useHistory(); + + function setSelectedCategory(categoryId: string) { + const url = pagePathGetters.integrations_installed({ + category: categoryId, + searchTerm: searchParam, + })[1]; + + history.push(url); + } + + function setSearchTerm(search: string) { + // Use .replace so the browser's back button is not tied to single keystroke + history.replace( + pagePathGetters.integrations_installed({ + category: selectedCategory, + searchTerm: search, + })[1] + ); + } + + const allInstalledPackages = useMemo( + () => + (allPackages?.response || []).filter((pkg) => pkg.status === installationStatuses.Installed), + [allPackages?.response] + ); + + const updatablePackages = useMemo( + () => + allInstalledPackages.filter( + (item) => + 'savedObject' in item && semverLt(item.savedObject.attributes.version, item.version) + ), + [allInstalledPackages] + ); + + const categories: CategoryFacet[] = useMemo( + () => [ + { + id: '', + count: allInstalledPackages.length, + }, + { + id: 'updates_available', + count: updatablePackages.length, + }, + ], + [allInstalledPackages.length, updatablePackages.length] + ); + + if (!categoryExists(selectedCategory, categories)) { + history.replace( + pagePathGetters.integrations_installed({ category: '', searchTerm: searchParam })[1] + ); + + return null; + } + + const controls = ( + setSelectedCategory(id)} + /> + ); + + const cards = ( + selectedCategory === 'updates_available' ? updatablePackages : allInstalledPackages + ).map((item) => mapToCard(getAbsolutePath, getHref, item)); + + const callout = selectedCategory === 'updates_available' ? null : ; + + return ( + + ); +}); From 8ea719ca8a25d5bd421d5fadc92698cb6545f98b Mon Sep 17 00:00:00 2001 From: Milton Hultgren Date: Tue, 12 Oct 2021 08:46:48 +0200 Subject: [PATCH 045/287] [Logs UI][Metrics UI] Replace usage of deprecated IndexPattern types (#114448) * [Logs UI][Metrics UI] Replace usage of deprecated IndexPattern types (#107887) --- .../common/dependency_mocks/index_patterns.ts | 18 ++++++---------- .../resolved_log_source_configuration.ts | 12 +++++------ .../components/expression_chart.test.tsx | 4 ++-- .../components/expression_chart.tsx | 4 ++-- .../hooks/use_metrics_explorer_chart_data.ts | 4 ++-- .../logs/log_filter/log_filter_state.ts | 5 +++-- .../containers/logs/log_source/log_source.ts | 4 ++-- .../containers/with_kuery_autocompletion.tsx | 6 ++++-- .../containers/with_source/with_source.tsx | 4 ++-- .../hooks/use_kibana_index_patterns.mock.tsx | 21 +++++++------------ .../indices_configuration_panel.stories.tsx | 3 +++ .../pages/logs/settings/validation_errors.ts | 13 ++++++------ .../infra/public/pages/metrics/index.tsx | 4 ++-- .../components/toolbars/toolbar.tsx | 4 ++-- .../metrics_explorer/components/kuery_bar.tsx | 9 +++----- .../metrics_explorer/components/toolbar.tsx | 5 +++-- .../hooks/use_metric_explorer_state.ts | 4 ++-- .../hooks/use_metrics_explorer_data.test.tsx | 4 ++-- .../hooks/use_metrics_explorer_data.ts | 4 ++-- .../pages/metrics/metrics_explorer/index.tsx | 4 ++-- x-pack/plugins/infra/public/utils/kuery.ts | 5 +++-- .../utils/logs_overview_fetches.test.ts | 1 + .../log_entries_search_strategy.test.ts | 1 + .../log_entry_search_strategy.test.ts | 1 + 24 files changed, 69 insertions(+), 75 deletions(-) diff --git a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts index 14215c1539473..03d3ec757bf55 100644 --- a/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts +++ b/x-pack/plugins/infra/common/dependency_mocks/index_patterns.ts @@ -7,17 +7,11 @@ import { from, of } from 'rxjs'; import { delay } from 'rxjs/operators'; -import { - fieldList, - FieldSpec, - IIndexPattern, - IndexPattern, - IndexPatternsContract, - RuntimeField, -} from 'src/plugins/data/common'; +import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; +import { fieldList, FieldSpec, RuntimeField } from '../../../../../src/plugins/data/common'; type IndexPatternMock = Pick< - IndexPattern, + DataView, | 'fields' | 'getComputedFields' | 'getFieldByName' @@ -27,7 +21,7 @@ type IndexPatternMock = Pick< | 'title' | 'type' >; -type IndexPatternMockSpec = Pick & { +type IndexPatternMockSpec = Pick & { fields: FieldSpec[]; }; @@ -71,8 +65,8 @@ export const createIndexPatternsMock = ( asyncDelay: number, indexPatterns: IndexPatternMock[] ): { - getIdsWithTitle: IndexPatternsContract['getIdsWithTitle']; - get: (...args: Parameters) => Promise; + getIdsWithTitle: DataViewsContract['getIdsWithTitle']; + get: (...args: Parameters) => Promise; } => { return { async getIdsWithTitle(_refresh?: boolean) { diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts index 9c41a216c7f92..567acf1fc4134 100644 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts @@ -6,7 +6,7 @@ */ import { estypes } from '@elastic/elasticsearch'; -import { IndexPattern, IndexPatternsContract } from '../../../../../src/plugins/data/common'; +import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; import { ObjectEntries } from '../utility_types'; import { ResolveLogSourceConfigurationError } from './errors'; import { @@ -21,14 +21,14 @@ export interface ResolvedLogSourceConfiguration { timestampField: string; tiebreakerField: string; messageField: string[]; - fields: IndexPattern['fields']; + fields: DataView['fields']; runtimeMappings: estypes.MappingRuntimeFields; columns: LogSourceColumnConfiguration[]; } export const resolveLogSourceConfiguration = async ( sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: IndexPatternsContract + indexPatternsService: DataViewsContract ): Promise => { if (sourceConfiguration.logIndices.type === 'index_name') { return await resolveLegacyReference(sourceConfiguration, indexPatternsService); @@ -39,7 +39,7 @@ export const resolveLogSourceConfiguration = async ( const resolveLegacyReference = async ( sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: IndexPatternsContract + indexPatternsService: DataViewsContract ): Promise => { if (sourceConfiguration.logIndices.type !== 'index_name') { throw new Error('This function can only resolve legacy references'); @@ -74,7 +74,7 @@ const resolveLegacyReference = async ( const resolveKibanaIndexPatternReference = async ( sourceConfiguration: LogSourceConfigurationProperties, - indexPatternsService: IndexPatternsContract + indexPatternsService: DataViewsContract ): Promise => { if (sourceConfiguration.logIndices.type !== 'index_pattern') { throw new Error('This function can only resolve Kibana Index Pattern references'); @@ -103,7 +103,7 @@ const resolveKibanaIndexPatternReference = async ( }; // this might take other sources of runtime fields into account in the future -const resolveRuntimeMappings = (indexPattern: IndexPattern): estypes.MappingRuntimeFields => { +const resolveRuntimeMappings = (indexPattern: DataView): estypes.MappingRuntimeFields => { const { runtimeFields } = indexPattern.getComputedFields(); const runtimeMappingsFromIndexPattern = ( diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index caf8e32814fe5..5aafd9b613d99 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -9,7 +9,7 @@ import { mountWithIntl, nextTick } from '@kbn/test/jest'; // We are using this inside a `jest.mock` call. Jest requires dynamic dependencies to be prefixed with `mock` import { coreMock as mockCoreMock } from 'src/core/public/mocks'; import { MetricExpression } from '../types'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import React from 'react'; import { ExpressionChart } from './expression_chart'; @@ -40,7 +40,7 @@ jest.mock('../hooks/use_metrics_explorer_chart_data', () => ({ describe('ExpressionChart', () => { async function setup(expression: MetricExpression, filterQuery?: string, groupBy?: string) { - const derivedIndexPattern: IIndexPattern = { + const derivedIndexPattern: DataViewBase = { title: 'metricbeat-*', fields: [], }; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx index e5558b961ab20..b176e3907228c 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.tsx @@ -10,7 +10,7 @@ import { Axis, Chart, niceTimeFormatter, Position, Settings } from '@elastic/cha import { first, last } from 'lodash'; import { EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { Color } from '../../../../common/color_palette'; import { MetricsExplorerRow, MetricsExplorerAggregation } from '../../../../common/http_api'; @@ -34,7 +34,7 @@ import { ThresholdAnnotations } from '../../common/criterion_preview_chart/thres interface Props { expression: MetricExpression; - derivedIndexPattern: IIndexPattern; + derivedIndexPattern: DataViewBase; source: MetricsSourceConfiguration | null; filterQuery?: string; groupBy?: string | string[]; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts index e3006993216ae..1ae1bacfed42e 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/hooks/use_metrics_explorer_chart_data.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { useMemo } from 'react'; import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { MetricExpression } from '../types'; @@ -14,7 +14,7 @@ import { useMetricsExplorerData } from '../../../pages/metrics/metrics_explorer/ export const useMetricsExplorerChartData = ( expression: MetricExpression, - derivedIndexPattern: IIndexPattern, + derivedIndexPattern: DataViewBase, source: MetricsSourceConfiguration | null, filterQuery?: string, groupBy?: string | string[] diff --git a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts index d4cb7ca90541f..f4576158b9a25 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_filter/log_filter_state.ts @@ -8,7 +8,8 @@ import createContainer from 'constate'; import { useCallback, useState } from 'react'; import useDebounce from 'react-use/lib/useDebounce'; -import { esQuery, IIndexPattern, Query } from '../../../../../../../src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; +import { esQuery, Query } from '../../../../../../../src/plugins/data/public'; type ParsedQuery = ReturnType; @@ -33,7 +34,7 @@ const initialLogFilterState: ILogFilterState = { const validationDebounceTimeout = 1000; // milliseconds -export const useLogFilterState = ({ indexPattern }: { indexPattern: IIndexPattern }) => { +export const useLogFilterState = ({ indexPattern }: { indexPattern: DataViewBase }) => { const [logFilterState, setLogFilterState] = useState(initialLogFilterState); const parseQuery = useCallback( diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts index 198d0d2efe44c..8f744a1d6df6d 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.ts @@ -8,7 +8,7 @@ import createContainer from 'constate'; import { useCallback, useMemo, useState } from 'react'; import type { HttpHandler } from 'src/core/public'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common'; +import { DataViewsContract } from '../../../../../../../src/plugins/data_views/public'; import { LogIndexField, LogSourceConfigurationPropertiesPatch, @@ -42,7 +42,7 @@ export const useLogSource = ({ }: { sourceId: string; fetch: HttpHandler; - indexPatternsService: IndexPatternsContract; + indexPatternsService: DataViewsContract; }) => { const [sourceConfiguration, setSourceConfiguration] = useState< LogSourceConfiguration | undefined diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index f50c629d521e5..0e5ef1ca78033 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -6,7 +6,8 @@ */ import React from 'react'; -import { QuerySuggestion, IIndexPattern, DataPublicPluginStart } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; +import { QuerySuggestion, DataPublicPluginStart } from 'src/plugins/data/public'; import { withKibana, KibanaReactContextValue, @@ -21,7 +22,7 @@ interface WithKueryAutocompletionLifecycleProps { loadSuggestions: (expression: string, cursorPosition: number, maxSuggestions?: number) => void; suggestions: QuerySuggestion[]; }>; - indexPattern: IIndexPattern; + indexPattern: DataViewBase; } interface WithKueryAutocompletionLifecycleState { @@ -82,6 +83,7 @@ class WithKueryAutocompletionComponent extends React.Component< query: expression, selectionStart: cursorPosition, selectionEnd: cursorPosition, + // @ts-expect-error (until data service updates to new types) indexPatterns: [indexPattern], boolFilter: [], })) || []; diff --git a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx index f3ca57a40c4c7..aa7ab18271d99 100644 --- a/x-pack/plugins/infra/public/containers/with_source/with_source.tsx +++ b/x-pack/plugins/infra/public/containers/with_source/with_source.tsx @@ -7,7 +7,7 @@ import React, { useContext } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfigurationProperties, PartialMetricsSourceConfigurationProperties, @@ -21,7 +21,7 @@ interface WithSourceProps { create: ( sourceProperties: PartialMetricsSourceConfigurationProperties ) => Promise | undefined; - createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; + createDerivedIndexPattern: (type: 'metrics') => DataViewBase; exists?: boolean; hasFailed: boolean; isLoading: boolean; diff --git a/x-pack/plugins/infra/public/hooks/use_kibana_index_patterns.mock.tsx b/x-pack/plugins/infra/public/hooks/use_kibana_index_patterns.mock.tsx index 9d3a611cff88d..99cd094b50e1e 100644 --- a/x-pack/plugins/infra/public/hooks/use_kibana_index_patterns.mock.tsx +++ b/x-pack/plugins/infra/public/hooks/use_kibana_index_patterns.mock.tsx @@ -10,23 +10,16 @@ import { from, of } from 'rxjs'; import { delay } from 'rxjs/operators'; import { CoreStart } from '../../../../../src/core/public'; import { FieldSpec } from '../../../../../src/plugins/data/common'; -import { - IIndexPattern, - IndexPattern, - IndexPatternField, - IndexPatternsContract, -} from '../../../../../src/plugins/data/public'; +import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/public'; +import { DataViewField } from '../../../../../src/plugins/data_views/common'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { Pick2 } from '../../common/utility_types'; type MockIndexPattern = Pick< - IndexPattern, + DataView, 'id' | 'title' | 'type' | 'getTimeField' | 'isTimeBased' | 'getFieldByName' | 'getComputedFields' >; -export type MockIndexPatternSpec = Pick< - IIndexPattern, - 'id' | 'title' | 'type' | 'timeFieldName' -> & { +export type MockIndexPatternSpec = Pick & { fields: FieldSpec[]; }; @@ -59,8 +52,8 @@ export const createIndexPatternsMock = ( asyncDelay: number, indexPatterns: MockIndexPattern[] ): { - getIdsWithTitle: IndexPatternsContract['getIdsWithTitle']; - get: (...args: Parameters) => Promise; + getIdsWithTitle: DataViewsContract['getIdsWithTitle']; + get: (...args: Parameters) => Promise; } => { return { async getIdsWithTitle(_refresh?: boolean) { @@ -85,7 +78,7 @@ export const createIndexPatternMock = ({ fields, timeFieldName, }: MockIndexPatternSpec): MockIndexPattern => { - const indexPatternFields = fields.map((fieldSpec) => new IndexPatternField(fieldSpec)); + const indexPatternFields = fields.map((fieldSpec) => new DataViewField(fieldSpec)); return { id, diff --git a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx index 8cc9f5b4357ef..546bb9aab0f33 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx +++ b/x-pack/plugins/infra/public/pages/logs/settings/indices_configuration_panel.stories.tsx @@ -131,6 +131,7 @@ const defaultArgs: IndicesConfigurationPanelStoryArgs = { id: 'INDEX_PATTERN_A', title: 'pattern-a-*', timeFieldName: '@timestamp', + type: undefined, fields: [ { name: '@timestamp', @@ -149,6 +150,8 @@ const defaultArgs: IndicesConfigurationPanelStoryArgs = { { id: 'INDEX_PATTERN_B', title: 'pattern-b-*', + timeFieldName: '@timestamp', + type: undefined, fields: [], }, ], diff --git a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts index 81b9297f8a70b..896fecc8d70dd 100644 --- a/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts +++ b/x-pack/plugins/infra/public/pages/logs/settings/validation_errors.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { IndexPattern, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; +import { DataView } from '../../../../../../../src/plugins/data_views/public'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; export interface GenericValidationError { type: 'generic'; @@ -67,7 +68,7 @@ export const validateStringNotEmpty = (fieldName: string, value: string): FormVa export const validateColumnListNotEmpty = (columns: unknown[]): FormValidationError[] => columns.length <= 0 ? [{ type: 'empty_column_list' }] : []; -export const validateIndexPattern = (indexPattern: IndexPattern): FormValidationError[] => { +export const validateIndexPattern = (indexPattern: DataView): FormValidationError[] => { return [ ...validateIndexPatternIsTimeBased(indexPattern), ...validateIndexPatternHasStringMessageField(indexPattern), @@ -75,9 +76,7 @@ export const validateIndexPattern = (indexPattern: IndexPattern): FormValidation ]; }; -export const validateIndexPatternIsTimeBased = ( - indexPattern: IndexPattern -): FormValidationError[] => +export const validateIndexPatternIsTimeBased = (indexPattern: DataView): FormValidationError[] => indexPattern.isTimeBased() ? [] : [ @@ -88,7 +87,7 @@ export const validateIndexPatternIsTimeBased = ( ]; export const validateIndexPatternHasStringMessageField = ( - indexPattern: IndexPattern + indexPattern: DataView ): FormValidationError[] => { const messageField = indexPattern.getFieldByName('message'); @@ -111,7 +110,7 @@ export const validateIndexPatternHasStringMessageField = ( } }; -export const validateIndexPatternIsntRollup = (indexPattern: IndexPattern): FormValidationError[] => +export const validateIndexPatternIsntRollup = (indexPattern: DataView): FormValidationError[] => indexPattern.type != null ? [ { diff --git a/x-pack/plugins/infra/public/pages/metrics/index.tsx b/x-pack/plugins/infra/public/pages/metrics/index.tsx index d4845a4dd9e44..ae375dc504e7a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/index.tsx @@ -11,7 +11,7 @@ import React, { useContext } from 'react'; import { Route, RouteComponentProps, Switch } from 'react-router-dom'; import { EuiErrorBoundary, EuiHeaderLinks, EuiHeaderLink } from '@elastic/eui'; -import { IIndexPattern } from 'src/plugins/data/common'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfigurationProperties } from '../../../common/metrics_sources'; import { DocumentTitle } from '../../components/document_title'; import { HelpCenterContent } from '../../components/help_center_content'; @@ -141,7 +141,7 @@ export const InfrastructurePage = ({ match }: RouteComponentProps) => { const PageContent = (props: { configuration: MetricsSourceConfigurationProperties; - createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; + createDerivedIndexPattern: (type: 'metrics') => DataViewBase; }) => { const { createDerivedIndexPattern, configuration } = props; const { options } = useContext(MetricsExplorerOptionsContainer.Context); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx index 1c79807f139c3..14f4177798be8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/toolbars/toolbar.tsx @@ -7,6 +7,7 @@ import React, { FunctionComponent } from 'react'; import { EuiFlexItem } from '@elastic/eui'; +import { DataViewBase } from '@kbn/es-query'; import { useSourceContext } from '../../../../../containers/metrics_source'; import { SnapshotMetricInput, @@ -18,13 +19,12 @@ import { findToolbar } from '../../../../../../common/inventory_models/toolbars' import { ToolbarWrapper } from './toolbar_wrapper'; import { InfraGroupByOptions } from '../../../../../lib/lib'; -import { IIndexPattern } from '../../../../../../../../../src/plugins/data/public'; import { InventoryItemType } from '../../../../../../common/inventory_models/types'; import { WaffleOptionsState, WaffleSortOption } from '../../hooks/use_waffle_options'; import { useInventoryMeta } from '../../hooks/use_inventory_meta'; export interface ToolbarProps extends Omit { - createDerivedIndexPattern: (type: 'metrics') => IIndexPattern; + createDerivedIndexPattern: (type: 'metrics') => DataViewBase; changeMetric: (payload: SnapshotMetricInput) => void; changeGroupBy: (payload: SnapshotGroupBy) => void; changeCustomOptions: (payload: InfraGroupByOptions[]) => void; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx index cf201c9a57f7c..2be5f14005b26 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/kuery_bar.tsx @@ -8,13 +8,10 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useState } from 'react'; +import { DataViewBase } from '@kbn/es-query'; import { WithKueryAutocompletion } from '../../../../containers/with_kuery_autocompletion'; import { AutocompleteField } from '../../../../components/autocomplete_field'; -import { - esKuery, - IIndexPattern, - QuerySuggestion, -} from '../../../../../../../../src/plugins/data/public'; +import { esKuery, QuerySuggestion } from '../../../../../../../../src/plugins/data/public'; type LoadSuggestionsFn = ( e: string, @@ -25,7 +22,7 @@ type LoadSuggestionsFn = ( export type CurryLoadSuggestionsType = (loadSuggestions: LoadSuggestionsFn) => LoadSuggestionsFn; interface Props { - derivedIndexPattern: IIndexPattern; + derivedIndexPattern: DataViewBase; onSubmit: (query: string) => void; onChange?: (query: string) => void; value?: string | null; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx index 64da554dee690..01b2d129a1f9e 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/toolbar.tsx @@ -8,7 +8,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, EuiText } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { IIndexPattern, UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; +import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/public'; import { MetricsExplorerMetric, MetricsExplorerAggregation, @@ -27,7 +28,7 @@ import { useKibanaUiSetting } from '../../../../utils/use_kibana_ui_setting'; import { mapKibanaQuickRangesToDatePickerRanges } from '../../../../utils/map_timepicker_quickranges_to_datepicker_ranges'; interface Props { - derivedIndexPattern: IIndexPattern; + derivedIndexPattern: DataViewBase; timeRange: MetricsExplorerTimeOptions; options: MetricsExplorerOptions; chartOptions: MetricsExplorerChartOptions; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts index a304c81ca1298..39eb7c928997b 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metric_explorer_state.ts @@ -6,7 +6,7 @@ */ import { useState, useCallback, useContext } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerMetric, @@ -29,7 +29,7 @@ export interface MetricExplorerViewState { export const useMetricsExplorerState = ( source: MetricsSourceConfigurationProperties, - derivedIndexPattern: IIndexPattern, + derivedIndexPattern: DataViewBase, shouldLoadImmediately = true ) => { const [refreshSignal, setRefreshSignal] = useState(0); diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx index 9a5e5fcf39ce4..0f903e4c4553a 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.test.tsx @@ -20,7 +20,7 @@ import { createSeries, } from '../../../../utils/fixtures/metrics_explorer'; import { MetricsExplorerOptions, MetricsExplorerTimeOptions } from './use_metrics_explorer_options'; -import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { HttpHandler } from 'kibana/public'; import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; @@ -39,7 +39,7 @@ const renderUseMetricsExplorerDataHook = () => { (props: { options: MetricsExplorerOptions; source: MetricsSourceConfigurationProperties | undefined; - derivedIndexPattern: IIndexPattern; + derivedIndexPattern: DataViewBase; timeRange: MetricsExplorerTimeOptions; afterKey: string | null | Record; signal: any; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index 95f98e172541a..6b7e98912fd49 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -8,7 +8,7 @@ import DateMath from '@elastic/datemath'; import { isEqual } from 'lodash'; import { useEffect, useState } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfigurationProperties } from '../../../../../common/metrics_sources'; import { MetricsExplorerResponse, @@ -27,7 +27,7 @@ function isSameOptions(current: MetricsExplorerOptions, next: MetricsExplorerOpt export function useMetricsExplorerData( options: MetricsExplorerOptions, source: MetricsSourceConfigurationProperties | undefined, - derivedIndexPattern: IIndexPattern, + derivedIndexPattern: DataViewBase, timerange: MetricsExplorerTimeOptions, afterKey: string | null | Record, signal: any, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx index f2345b0b8e020..dabf5db9afe92 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/index.tsx @@ -8,7 +8,7 @@ import { EuiErrorBoundary } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React, { useEffect, useContext } from 'react'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; import { MetricsSourceConfigurationProperties } from '../../../../common/metrics_sources'; import { useTrackPageview } from '../../../../../observability/public'; import { useMetricsBreadcrumbs } from '../../../hooks/use_metrics_breadcrumbs'; @@ -25,7 +25,7 @@ import { SavedViewsToolbarControls } from '../../../components/saved_views/toolb interface MetricsExplorerPageProps { source: MetricsSourceConfigurationProperties; - derivedIndexPattern: IIndexPattern; + derivedIndexPattern: DataViewBase; } export const MetricsExplorerPage = ({ source, derivedIndexPattern }: MetricsExplorerPageProps) => { diff --git a/x-pack/plugins/infra/public/utils/kuery.ts b/x-pack/plugins/infra/public/utils/kuery.ts index 19706d7664c22..c7528b237cce5 100644 --- a/x-pack/plugins/infra/public/utils/kuery.ts +++ b/x-pack/plugins/infra/public/utils/kuery.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { esKuery, IIndexPattern } from '../../../../../src/plugins/data/public'; +import { DataViewBase } from '@kbn/es-query'; +import { esKuery } from '../../../../../src/plugins/data/public'; export const convertKueryToElasticSearchQuery = ( kueryExpression: string, - indexPattern: IIndexPattern + indexPattern: DataViewBase ) => { try { return kueryExpression diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index 6dfb400567717..4fcb83fd02754 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -48,6 +48,7 @@ function setup() { id: 'test-index-pattern', title: 'log-indices-*', timeFieldName: '@timestamp', + type: undefined, fields: [ { name: 'event.dataset', diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index 876aad4ef8d02..85a1b95cf70aa 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -342,6 +342,7 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ id: 'test-index-pattern', title: 'log-indices-*', timeFieldName: '@timestamp', + type: undefined, fields: [ { name: 'event.dataset', diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 88c9d956dbe0a..ec3d4aa52a6b5 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -297,6 +297,7 @@ const createDataPluginMock = (esSearchStrategyMock: ISearchStrategy): any => ({ id: 'test-index-pattern', title: 'log-indices-*', timeFieldName: '@timestamp', + type: undefined, fields: [ { name: 'event.dataset', From b5771c81ba190031a1dca1876b57faf46d4c99a1 Mon Sep 17 00:00:00 2001 From: Kevin Lacabane Date: Tue, 12 Oct 2021 10:55:53 +0200 Subject: [PATCH 046/287] [Stack Monitoring] functional tests - properly set test-subj attribute (#114491) * properly set test-subj attribute * fix prettier error * Fixing ESLinting issue Co-authored-by: Chris Cowan Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/pages/kibana/instance.tsx | 9 ++------- .../public/application/pages/kibana/instances.tsx | 7 +------ .../public/application/pages/kibana/kibana_template.tsx | 2 ++ .../public/application/pages/kibana/overview.tsx | 7 +------ 4 files changed, 6 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx index 444794d118b0f..2d2fe99758ff7 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instance.tsx @@ -164,13 +164,8 @@ export const KibanaInstancePage: React.FC = ({ clusters }) => { }, [ccs, clusterUuid, instance, services.data?.query.timefilter.timefilter, services.http]); return ( - -
+ +
diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx index ae0237ea40472..076e9413216fb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/instances.tsx @@ -87,12 +87,7 @@ export const KibanaInstancesPage: React.FC = ({ clusters }) => { ]); return ( - +
= ({ ...props }) => { defaultMessage: 'Overview', }), route: '/kibana', + testSubj: 'kibanaOverviewPage', }, { id: 'instances', @@ -23,6 +24,7 @@ export const KibanaTemplate: React.FC = ({ ...props }) => { defaultMessage: 'Instances', }), route: '/kibana/instances', + testSubj: 'kibanaInstancesPage', }, ]; diff --git a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx index a47da048e1936..4c480bf1fbb33 100644 --- a/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/kibana/overview.tsx @@ -107,12 +107,7 @@ export const KibanaOverviewPage: React.FC = ({ clusters }) => { }, [ccs, clusterUuid, services.data?.query.timefilter.timefilter, services.http]); return ( - + ); From a0d36e7a699bc212a5de0b1ad305f33d05b3f73a Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 12 Oct 2021 11:24:55 +0200 Subject: [PATCH 047/287] [Discover/Reporting] Fix potential time drift with relative time when requesting a report (#114274) * updated Discover getSharingData to accept flag for absolute time * update snapshots * simplify fallback title * more appropriate title? * remove old translations * implement PR feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/top_nav/get_top_nav_links.ts | 4 +-- .../apps/main/utils/get_sharing_data.test.ts | 36 +++---------------- .../apps/main/utils/get_sharing_data.ts | 16 ++++++--- .../panel_actions/get_csv_panel_action.tsx | 4 +-- .../register_csv_reporting.tsx | 13 +++++-- .../reporting_panel_content.tsx | 4 +-- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 8 files changed, 32 insertions(+), 47 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts index ba4cd8c3cd524..81be662470306 100644 --- a/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts +++ b/src/plugins/discover/public/application/apps/main/components/top_nav/get_top_nav_links.ts @@ -7,7 +7,6 @@ */ import { i18n } from '@kbn/i18n'; -import moment from 'moment'; import type { IndexPattern, ISearchSource } from 'src/plugins/data/common'; import { showOpenSearchPanel } from './show_open_search_panel'; import { getSharingData, showPublicUrlSwitch } from '../../utils/get_sharing_data'; @@ -128,8 +127,7 @@ export const getTopNavLinks = ({ title: savedSearch.title || i18n.translate('discover.localMenu.fallbackReportTitle', { - defaultMessage: 'Discover search [{date}]', - values: { date: moment().toISOString(true) }, + defaultMessage: 'Untitled discover search', }), }, isDirty: !savedSearch.id || state.isAppStateDirty(), diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts index e7205c3f9bc69..9b518c23a5f89 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.test.ts @@ -41,14 +41,7 @@ describe('getSharingData', () => { expect(result).toMatchInlineSnapshot(` Object { "columns": Array [], - "searchSource": Object { - "index": "the-index-pattern-id", - "sort": Array [ - Object { - "_score": "desc", - }, - ], - }, + "getSearchSource": [Function], } `); }); @@ -66,14 +59,7 @@ describe('getSharingData', () => { "column_a", "column_b", ], - "searchSource": Object { - "index": "the-index-pattern-id", - "sort": Array [ - Object { - "_score": "desc", - }, - ], - }, + "getSearchSource": [Function], } `); }); @@ -108,14 +94,7 @@ describe('getSharingData', () => { "cool-field-5", "cool-field-6", ], - "searchSource": Object { - "index": "the-index-pattern-id", - "sort": Array [ - Object { - "_doc": "desc", - }, - ], - }, + "getSearchSource": [Function], } `); }); @@ -158,14 +137,7 @@ describe('getSharingData', () => { "cool-field-5", "cool-field-6", ], - "searchSource": Object { - "index": "the-index-pattern-id", - "sort": Array [ - Object { - "_doc": false, - }, - ], - }, + "getSearchSource": [Function], } `); }); diff --git a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts index 420ff0fa11eeb..21292fabdd13f 100644 --- a/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts +++ b/src/plugins/discover/public/application/apps/main/utils/get_sharing_data.ts @@ -9,7 +9,7 @@ import type { Capabilities } from 'kibana/public'; import type { IUiSettingsClient } from 'src/core/public'; import type { DataPublicPluginStart } from 'src/plugins/data/public'; -import type { ISearchSource } from 'src/plugins/data/common'; +import type { ISearchSource, SearchSourceFields } from 'src/plugins/data/common'; import { DOC_HIDE_TIME_COLUMN_SETTING, SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; import type { SavedSearch, SortOrder } from '../../../../saved_searches/types'; import { getSortForSearchSource } from '../components/doc_table'; @@ -31,8 +31,8 @@ export async function getSharingData( 'sort', getSortForSearchSource(state.sort as SortOrder[], index, config.get(SORT_DEFAULT_ORDER_SETTING)) ); - // When sharing externally we preserve relative time values - searchSource.setField('filter', data.query.timefilter.timefilter.createRelativeFilter(index)); + + searchSource.removeField('filter'); searchSource.removeField('highlight'); searchSource.removeField('highlightAll'); searchSource.removeField('aggs'); @@ -54,7 +54,15 @@ export async function getSharingData( } return { - searchSource: searchSource.getSerializedFields(true), + getSearchSource: (absoluteTime?: boolean): SearchSourceFields => { + const filter = absoluteTime + ? data.query.timefilter.timefilter.createFilter(index) + : data.query.timefilter.timefilter.createRelativeFilter(index); + + searchSource.setField('filter', filter); + + return searchSource.getSerializedFields(true); + }, columns, }; } diff --git a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx index ef32e64741765..115d7599c6bc8 100644 --- a/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx +++ b/x-pack/plugins/reporting/public/panel_actions/get_csv_panel_action.tsx @@ -117,10 +117,10 @@ export class ReportingCsvPanelAction implements ActionDefinition } const savedSearch = embeddable.getSavedSearch(); - const { columns, searchSource } = await this.getSearchSource(savedSearch, embeddable); + const { columns, getSearchSource } = await this.getSearchSource(savedSearch, embeddable); const immediateJobParams = this.apiClient.getDecoratedJobParams({ - searchSource, + searchSource: getSearchSource(true), columns, title: savedSearch.title, objectType: 'downloadCsv', // FIXME: added for typescript, but immediate download job does not need objectType diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx index 040a1646ec1ba..8859d01e4fe9a 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_csv_reporting.tsx @@ -48,14 +48,23 @@ export const ReportingCsvShareProvider = ({ return []; } + const getSearchSource = sharingData.getSearchSource as ( + absoluteTime?: boolean + ) => SearchSourceFields; + const jobParams = { title: sharingData.title as string, objectType, - searchSource: sharingData.searchSource as SearchSourceFields, columns: sharingData.columns as string[] | undefined, }; - const getJobParams = () => jobParams; + const getJobParams = (forShareUrl?: boolean) => { + const absoluteTime = !forShareUrl; + return { + ...jobParams, + searchSource: getSearchSource(absoluteTime), + }; + }; const shareActions = []; diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index 59afa91aaa9c3..6ed6f2d0c5f49 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -40,7 +40,7 @@ export interface ReportingPanelProps { requiresSavedState: boolean; // Whether the report to be generated requires saved state that is not captured in the URL submitted to the report generator. layoutId?: string; objectId?: string; - getJobParams: () => Omit; + getJobParams: (forShareUrl?: boolean) => Omit; options?: ReactElement | null; isDirty?: boolean; onClose?: () => void; @@ -75,7 +75,7 @@ class ReportingPanelContentUi extends Component { private getAbsoluteReportGenerationUrl = (props: Props) => { const relativePath = this.props.apiClient.getReportingJobPath( props.reportType, - this.props.apiClient.getDecoratedJobParams(this.props.getJobParams()) + this.props.apiClient.getDecoratedJobParams(this.props.getJobParams(true)) ); return url.resolve(window.location.href, relativePath); // FIXME: '(from: string, to: string): string' is deprecated }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index fbd9352fb427f..642668bcecf34 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2522,7 +2522,6 @@ "discover.loadingDocuments": "ドキュメントを読み込み中", "discover.loadingJSON": "JSONを読み込んでいます", "discover.loadingResults": "結果を読み込み中", - "discover.localMenu.fallbackReportTitle": "Discover検索[{date}]", "discover.localMenu.inspectTitle": "検査", "discover.localMenu.localMenu.newSearchTitle": "新規", "discover.localMenu.localMenu.optionsTitle": "オプション", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 9408bb85db879..4d86994c0fb84 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2548,7 +2548,6 @@ "discover.loadingDocuments": "正在加载文档", "discover.loadingJSON": "正在加载 JSON", "discover.loadingResults": "正在加载结果", - "discover.localMenu.fallbackReportTitle": "Discover 搜索 [{date}]", "discover.localMenu.inspectTitle": "检查", "discover.localMenu.localMenu.newSearchTitle": "新建", "discover.localMenu.localMenu.optionsTitle": "选项", From 14cde79e8327430b0a16a7f266f3d6bbfc971226 Mon Sep 17 00:00:00 2001 From: Walter Rafelsberger Date: Tue, 12 Oct 2021 12:23:46 +0200 Subject: [PATCH 048/287] [ML] APM Correlations: Show trace samples even when overall histogram chart fails to load. (#114247) In the trace samples tab, the trace samples will not be shown if the overall histogram chart fails to load. This PR removes that limitation and will show the trace samples even when the chart fails to load. --- .../distribution/index.tsx | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index ca162be92cdae..97f38a8123a4e 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -222,18 +222,14 @@ export function TransactionDistribution({ status={status} /> - {hasData && ( - <> - - - - - )} + + +
); } From 7d6aba5188de1ac2e2608982399c68ee56cd8de1 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 12 Oct 2021 12:28:21 +0200 Subject: [PATCH 049/287] [Uptime] Make uptime settings saved object exportable (#114235) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/uptime/server/lib/saved_objects.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/x-pack/plugins/uptime/server/lib/saved_objects.ts b/x-pack/plugins/uptime/server/lib/saved_objects.ts index 3cd246175d0be..3e9888df55aa0 100644 --- a/x-pack/plugins/uptime/server/lib/saved_objects.ts +++ b/x-pack/plugins/uptime/server/lib/saved_objects.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import { DYNAMIC_SETTINGS_DEFAULTS } from '../../common/constants'; import { DynamicSettings } from '../../common/runtime_types'; import { SavedObjectsType, SavedObjectsErrorHelpers } from '../../../../../src/core/server'; @@ -43,6 +44,14 @@ export const umDynamicSettings: SavedObjectsType = { */ }, }, + management: { + importableAndExportable: true, + icon: 'uptimeApp', + getTitle: () => + i18n.translate('xpack.uptime.uptimeSettings.index', { + defaultMessage: 'Uptime Settings - Index', + }), + }, }; export const savedObjectsAdapter: UMSavedObjectsAdapter = { From 7617dc118514ffcfa0db583f00c755cd2dca3e91 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 12 Oct 2021 12:47:43 +0100 Subject: [PATCH 050/287] skip flaky suite (#114396) --- .../public/components/common/uptime_date_picker.test.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx index 997831442faa2..cd122eb5d5fc5 100644 --- a/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx +++ b/x-pack/plugins/uptime/public/components/common/uptime_date_picker.test.tsx @@ -12,7 +12,8 @@ import { createMemoryHistory } from 'history'; import { render } from '../../lib/helper/rtl_helpers'; import { fireEvent } from '@testing-library/dom'; -describe('UptimeDatePicker component', () => { +// FLAKY: https://github.com/elastic/kibana/issues/114396 +describe.skip('UptimeDatePicker component', () => { it('renders properly with mock data', async () => { const { findByText } = render(); expect(await findByText('Last 15 minutes')).toBeInTheDocument(); From a11c3374c44144624916f360636d1af23df8faa8 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 12 Oct 2021 07:53:26 -0400 Subject: [PATCH 051/287] Handle deletes (#114545) --- .../curation_suggestion_logic.test.ts | 72 +++++++++++++++++++ .../curation_suggestion_logic.ts | 47 +++++++++--- 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts index 2ace55133d6fd..7a91171cc2cc7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.test.ts @@ -357,6 +357,42 @@ describe('CurationSuggestionLogic', () => { ); }); + describe('when a suggestion is a "delete" suggestion', () => { + const deleteSuggestion = { + ...suggestion, + operation: 'delete', + promoted: [], + curation_id: 'cur-6155e69c7a2f2e4f756303fd', + }; + + it('will show a confirm message before applying, and redirect a user back to the curations page, rather than the curation details page', async () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); + http.put.mockReturnValueOnce( + Promise.resolve({ + results: [{ ...suggestion, status: 'accepted', curation_id: undefined }], + }) + ); + mountLogic({ + suggestion: deleteSuggestion, + }); + CurationSuggestionLogic.actions.acceptSuggestion(); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); + }); + + it('will do nothing if the user does not confirm', async () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(false); + mountLogic({ + suggestion: deleteSuggestion, + }); + CurationSuggestionLogic.actions.acceptSuggestion(); + await nextTick(); + expect(http.put).not.toHaveBeenCalled(); + expect(navigateToUrl).not.toHaveBeenCalled(); + }); + }); + itHandlesErrors(http.put, () => { CurationSuggestionLogic.actions.acceptSuggestion(); }); @@ -404,6 +440,42 @@ describe('CurationSuggestionLogic', () => { ); }); + describe('when a suggestion is a "delete" suggestion', () => { + const deleteSuggestion = { + ...suggestion, + operation: 'delete', + promoted: [], + curation_id: 'cur-6155e69c7a2f2e4f756303fd', + }; + + it('will show a confirm message before applying, and redirect a user back to the curations page, rather than the curation details page', async () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(true); + http.put.mockReturnValueOnce( + Promise.resolve({ + results: [{ ...suggestion, status: 'accepted', curation_id: undefined }], + }) + ); + mountLogic({ + suggestion: deleteSuggestion, + }); + CurationSuggestionLogic.actions.acceptAndAutomateSuggestion(); + await nextTick(); + + expect(navigateToUrl).toHaveBeenCalledWith('/engines/some-engine/curations'); + }); + + it('will do nothing if the user does not confirm', async () => { + jest.spyOn(global, 'confirm').mockReturnValueOnce(false); + mountLogic({ + suggestion: deleteSuggestion, + }); + CurationSuggestionLogic.actions.acceptAndAutomateSuggestion(); + await nextTick(); + expect(http.put).not.toHaveBeenCalled(); + expect(navigateToUrl).not.toHaveBeenCalled(); + }); + }); + itHandlesErrors(http.put, () => { CurationSuggestionLogic.actions.acceptAndAutomateSuggestion(); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts index 6749b510edeba..4ca1b0adb7814 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion_logic.ts @@ -141,6 +141,11 @@ export const CurationSuggestionLogic = kea< const { engineName } = EngineLogic.values; const { suggestion } = values; + if (suggestion!.operation === 'delete') { + const confirmed = await confirmDialog('Are you sure you want to delete this curation?'); + if (!confirmed) return; + } + try { const updatedSuggestion = await updateSuggestion( http, @@ -155,11 +160,16 @@ export const CurationSuggestionLogic = kea< { defaultMessage: 'Suggestion was succefully applied.' } ) ); - KibanaLogic.values.navigateToUrl( - generateEnginePath(ENGINE_CURATION_PATH, { - curationId: updatedSuggestion.curation_id, - }) - ); + if (suggestion!.operation === 'delete') { + // Because if a curation is deleted, there will be no curation detail page to navigate to afterwards. + KibanaLogic.values.navigateToUrl(generateEnginePath(ENGINE_CURATIONS_PATH)); + } else { + KibanaLogic.values.navigateToUrl( + generateEnginePath(ENGINE_CURATION_PATH, { + curationId: updatedSuggestion.curation_id, + }) + ); + } } catch (e) { flashAPIErrors(e); } @@ -169,6 +179,11 @@ export const CurationSuggestionLogic = kea< const { engineName } = EngineLogic.values; const { suggestion } = values; + if (suggestion!.operation === 'delete') { + const confirmed = await confirmDialog('Are you sure you want to delete this curation?'); + if (!confirmed) return; + } + try { const updatedSuggestion = await updateSuggestion( http, @@ -187,11 +202,16 @@ export const CurationSuggestionLogic = kea< } ) ); - KibanaLogic.values.navigateToUrl( - generateEnginePath(ENGINE_CURATION_PATH, { - curationId: updatedSuggestion.curation_id, - }) - ); + if (suggestion!.operation === 'delete') { + // Because if a curation is deleted, there will be no curation detail page to navigate to afterwards. + KibanaLogic.values.navigateToUrl(generateEnginePath(ENGINE_CURATIONS_PATH)); + } else { + KibanaLogic.values.navigateToUrl( + generateEnginePath(ENGINE_CURATION_PATH, { + curationId: updatedSuggestion.curation_id, + }) + ); + } } catch (e) { flashAPIErrors(e); } @@ -325,3 +345,10 @@ const getCuration = async (http: HttpSetup, engineName: string, curationId: stri query: { skip_record_analytics: 'true' }, }); }; + +const confirmDialog = (msg: string) => { + return new Promise(function (resolve) { + const confirmed = window.confirm(msg); + return resolve(confirmed); + }); +}; From eb5e46a094bfea7b9e2dc56d849762c3a4a0bcd0 Mon Sep 17 00:00:00 2001 From: Jason Stoltzfus Date: Tue, 12 Oct 2021 07:53:48 -0400 Subject: [PATCH 052/287] Check platinum license (#114549) --- .../curations/views/curations_overview.test.tsx | 16 ++++++++++++++++ .../curations/views/curations_overview.tsx | 5 +++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx index 32ea59c8192ba..ff6ee66d8cb10 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.test.tsx @@ -15,6 +15,8 @@ import { shallow } from 'enzyme'; import { CurationsTable, EmptyState } from '../components'; +import { SuggestionsTable } from '../components/suggestions_table'; + import { CurationsOverview } from './curations_overview'; describe('CurationsOverview', () => { @@ -44,4 +46,18 @@ describe('CurationsOverview', () => { expect(wrapper.find(CurationsTable)).toHaveLength(1); }); + + it('renders a suggestions table when the user has a platinum license', () => { + setMockValues({ curations: [], hasPlatinumLicense: true }); + const wrapper = shallow(); + + expect(wrapper.find(SuggestionsTable).exists()).toBe(true); + }); + + it('doesn\t render a suggestions table when the user has no platinum license', () => { + setMockValues({ curations: [], hasPlatinumLicense: false }); + const wrapper = shallow(); + + expect(wrapper.find(SuggestionsTable).exists()).toBe(false); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx index 7d3db5dedb262..079f0046cb9bf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_overview.tsx @@ -11,15 +11,16 @@ import { useValues } from 'kea'; import { EuiSpacer } from '@elastic/eui'; +import { LicensingLogic } from '../../../../shared/licensing'; import { CurationsTable, EmptyState } from '../components'; import { SuggestionsTable } from '../components/suggestions_table'; import { CurationsLogic } from '../curations_logic'; export const CurationsOverview: React.FC = () => { const { curations } = useValues(CurationsLogic); + const { hasPlatinumLicense } = useValues(LicensingLogic); - // TODO - const shouldShowSuggestions = true; + const shouldShowSuggestions = hasPlatinumLicense; return ( <> From adb49b7d37a3a368f938307c7df09729fdba43c7 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Tue, 12 Oct 2021 14:06:31 +0200 Subject: [PATCH 053/287] refactor (#114603) --- .../cypress/integration/timelines/pagination.spec.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts index b569ea7cc082f..ce7b589f53c5a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/pagination.spec.ts @@ -24,7 +24,7 @@ import { HOSTS_URL } from '../../urls/navigation'; const defaultPageSize = 25; describe('Pagination', () => { - beforeEach(() => { + before(() => { cleanKibana(); loginAndWaitForPage(HOSTS_URL); openTimelineUsingToggle(); @@ -41,20 +41,13 @@ describe('Pagination', () => { it('should be able to change items count per page with the dropdown', () => { const itemsPerPage = 100; - cy.intercept('POST', '/internal/bsearch').as('refetch'); - cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_BTN).first().click(); cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE_OPTION(itemsPerPage)).click(); - cy.wait('@refetch').its('response.statusCode').should('eq', 200); cy.get(TIMELINE_EVENTS_COUNT_PER_PAGE).should('contain.text', itemsPerPage); }); it('should be able to go to next / previous page', () => { - cy.intercept('POST', '/internal/bsearch').as('refetch'); cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_NEXT_PAGE}`).first().click(); - cy.wait('@refetch').its('response.statusCode').should('eq', 200); - cy.get(`${TIMELINE_FLYOUT} ${TIMELINE_EVENTS_COUNT_PREV_PAGE}`).first().click(); - cy.wait('@refetch').its('response.statusCode').should('eq', 200); }); }); From 9e65b12c4b6bd8b048994e92a7d4a970862e2372 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Tue, 12 Oct 2021 06:29:36 -0600 Subject: [PATCH 054/287] [Maps] Fix apis Maps endpoints getTile should return vector tile containing document (#114509) * [Maps] Fix apis Maps endpoints getTile should return vector tile containing document * use find instead of if statement * eslint * can not use layer.feature.find is layer.feature is not an array * unskip other test where fix has been merged Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../api_integration/apis/maps/get_tile.js | 27 ++++++++++++++----- .../maps/documents_source/docvalue_fields.js | 3 +-- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index b153cc1ff030c..a1d4f10ca7be8 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -10,11 +10,19 @@ import Protobuf from 'pbf'; import expect from '@kbn/expect'; import { MVT_SOURCE_LAYER_NAME } from '../../../../plugins/maps/common/constants'; +function findFeature(layer, callbackFn) { + for (let i = 0; i < layer.length; i++) { + const feature = layer.feature(i); + if (callbackFn(feature)) { + return feature; + } + } +} + export default function ({ getService }) { const supertest = getService('supertest'); - // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/114471 - describe.skip('getTile', () => { + describe('getTile', () => { it('should return vector tile containing document', async () => { const resp = await supertest .get( @@ -32,8 +40,12 @@ export default function ({ getService }) { const layer = jsonTile.layers[MVT_SOURCE_LAYER_NAME]; expect(layer.length).to.be(3); // 2 docs + the metadata feature - // 1st doc - const feature = layer.feature(0); + // Verify ES document + + const feature = findFeature(layer, (feature) => { + return feature.properties._id === 'AU_x3_BsGFA8no6Qjjug'; + }); + expect(feature).not.to.be(undefined); expect(feature.type).to.be(1); expect(feature.extent).to.be(4096); expect(feature.id).to.be(undefined); @@ -46,8 +58,11 @@ export default function ({ getService }) { }); expect(feature.loadGeometry()).to.eql([[{ x: 44, y: 2382 }]]); - // Metadata feature - const metadataFeature = layer.feature(2); + // Verify metadata feature + const metadataFeature = findFeature(layer, (feature) => { + return feature.properties.__kbn_metadata_feature__; + }); + expect(metadataFeature).not.to.be(undefined); expect(metadataFeature.type).to.be(3); expect(metadataFeature.extent).to.be(4096); expect(metadataFeature.id).to.be(undefined); diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index d1da193ac50a5..e24ba5b673169 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -11,8 +11,7 @@ export default function ({ getPageObjects, getService }) { const PageObjects = getPageObjects(['maps']); const security = getService('security'); - // FLAKY: https://github.com/elastic/kibana/issues/114418 - describe.skip('docvalue_fields', () => { + describe('docvalue_fields', () => { before(async () => { await security.testUser.setRoles(['global_maps_read', 'test_logstash_reader'], false); }); From e1133e11ac42ede0579312dda98fa58b3790259b Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Tue, 12 Oct 2021 14:35:14 +0200 Subject: [PATCH 055/287] [Discover] Improve context code (#114284) --- .../__mocks__/index_pattern_with_timefield.ts | 2 + .../apps/context/context_app.test.tsx | 1 - .../application/apps/context/context_app.tsx | 22 +- .../apps/context/context_app_content.tsx | 8 +- .../apps/context/context_app_route.tsx | 2 +- .../apps/context/services/_stubs.ts | 13 - .../apps/context/services/anchor.test.ts | 49 ++-- .../apps/context/services/anchor.ts | 51 ++-- .../services/context.predecessors.test.ts | 236 +++++++----------- .../services/context.successors.test.ts | 225 ++++++----------- .../apps/context/services/context.ts | 148 ++++++----- .../context/services/context_state.test.ts | 31 +-- .../apps/context/services/context_state.ts | 26 +- .../utils/use_context_app_fetch.test.ts | 33 ++- .../context/utils/use_context_app_fetch.tsx | 47 ++-- .../context/utils/use_context_app_state.ts | 12 +- 16 files changed, 342 insertions(+), 564 deletions(-) diff --git a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts index f1f1d74f3af3a..0f64a6c67741d 100644 --- a/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts +++ b/src/plugins/discover/public/__mocks__/index_pattern_with_timefield.ts @@ -72,6 +72,8 @@ const indexPattern = { getFieldByName: (name: string) => fields.getByName(name), timeFieldName: 'timestamp', getFormatterForField: () => ({ convert: () => 'formatted' }), + isTimeNanosBased: () => false, + popularizeField: () => {}, } as unknown as IndexPattern; indexPattern.flattenHit = indexPatterns.flattenHitWrapper(indexPattern, indexPattern.metaFields); diff --git a/src/plugins/discover/public/application/apps/context/context_app.test.tsx b/src/plugins/discover/public/application/apps/context/context_app.test.tsx index d54a4f8bed247..0e50f8f714a2c 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.test.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app.test.tsx @@ -26,7 +26,6 @@ const mockNavigationPlugin = { ui: { TopNavMenu: mockTopNavMenu } }; describe('ContextApp test', () => { const defaultProps = { indexPattern: indexPatternMock, - indexPatternId: 'the-index-pattern-id', anchorId: 'mocked_anchor_id', }; diff --git a/src/plugins/discover/public/application/apps/context/context_app.tsx b/src/plugins/discover/public/application/apps/context/context_app.tsx index 070391edae71c..9d39c93d250f2 100644 --- a/src/plugins/discover/public/application/apps/context/context_app.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app.tsx @@ -12,7 +12,7 @@ import classNames from 'classnames'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; import { cloneDeep } from 'lodash'; -import { esFilters, SortDirection } from '../../../../../data/public'; +import { esFilters } from '../../../../../data/public'; import { DOC_TABLE_LEGACY, SEARCH_FIELDS_FROM_SOURCE } from '../../../../common'; import { ContextErrorMessage } from './components/context_error_message'; import { IndexPattern, IndexPatternField } from '../../../../../data/common'; @@ -31,21 +31,20 @@ const ContextAppContentMemoized = memo(ContextAppContent); export interface ContextAppProps { indexPattern: IndexPattern; - indexPatternId: string; anchorId: string; } -export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAppProps) => { +export const ContextApp = ({ indexPattern, anchorId }: ContextAppProps) => { const services = getServices(); - const { uiSettings: config, capabilities, indexPatterns, navigation, filterManager } = services; + const { uiSettings, capabilities, indexPatterns, navigation, filterManager } = services; - const isLegacy = useMemo(() => config.get(DOC_TABLE_LEGACY), [config]); - const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); + const isLegacy = useMemo(() => uiSettings.get(DOC_TABLE_LEGACY), [uiSettings]); + const useNewFieldsApi = useMemo(() => !uiSettings.get(SEARCH_FIELDS_FROM_SOURCE), [uiSettings]); /** * Context app state */ - const { appState, setAppState } = useContextAppState({ indexPattern, services }); + const { appState, setAppState } = useContextAppState({ services }); const prevAppState = useRef(); /** @@ -54,7 +53,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp const { fetchedState, fetchContextRows, fetchAllRows, fetchSurroundingRows } = useContextAppFetch( { anchorId, - indexPatternId, indexPattern, appState, useNewFieldsApi, @@ -79,7 +77,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp prevAppState.current = cloneDeep(appState); }, [ appState, - indexPatternId, anchorId, fetchContextRows, fetchAllRows, @@ -89,7 +86,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, - config, + config: uiSettings, indexPattern, indexPatterns, state: appState, @@ -112,7 +109,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp field, values, operation, - indexPatternId + indexPattern.id! ); filterManager.addFilters(newFilters); if (indexPatterns) { @@ -120,7 +117,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp await popularizeField(indexPattern, fieldName, indexPatterns, capabilities); } }, - [filterManager, indexPatternId, indexPatterns, indexPattern, capabilities] + [filterManager, indexPatterns, indexPattern, capabilities] ); const TopNavMenu = navigation.ui.TopNavMenu; @@ -166,7 +163,6 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} - sort={appState.sort as [[string, SortDirection]]} predecessorCount={appState.predecessorCount} successorCount={appState.successorCount} setAppState={setAppState} diff --git a/src/plugins/discover/public/application/apps/context/context_app_content.tsx b/src/plugins/discover/public/application/apps/context/context_app_content.tsx index 19b6bfac2876c..2d4d3cab250f3 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_content.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_content.tsx @@ -22,6 +22,7 @@ import { DiscoverServices } from '../../../build_services'; import { MAX_CONTEXT_SIZE, MIN_CONTEXT_SIZE } from './utils/constants'; import { DocTableContext } from '../main/components/doc_table/doc_table_context'; import { EsHitRecordList } from '../../types'; +import { SortPairArr } from '../main/components/doc_table/lib/get_sort'; export interface ContextAppContentProps { columns: string[]; @@ -33,7 +34,6 @@ export interface ContextAppContentProps { predecessorCount: number; successorCount: number; rows: EsHitRecordList; - sort: [[string, SortDirection]]; predecessors: EsHitRecordList; successors: EsHitRecordList; anchorStatus: LoadingStatus; @@ -65,7 +65,6 @@ export function ContextAppContent({ predecessorCount, successorCount, rows, - sort, predecessors, successors, anchorStatus, @@ -111,6 +110,9 @@ export function ContextAppContent({ }, [setAppState] ); + const sort = useMemo(() => { + return [[indexPattern.timeFieldName!, SortDirection.desc]]; + }, [indexPattern]); return ( @@ -149,7 +151,7 @@ export function ContextAppContent({ expandedDoc={expandedDoc} isLoading={isAnchorLoading} sampleSize={0} - sort={sort} + sort={sort as SortPairArr[]} isSortEnabled={false} showTimeCol={showTimeCol} services={services} diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/apps/context/context_app_route.tsx index 4bade3d03d993..d124fd6cfa395 100644 --- a/src/plugins/discover/public/application/apps/context/context_app_route.tsx +++ b/src/plugins/discover/public/application/apps/context/context_app_route.tsx @@ -49,5 +49,5 @@ export function ContextAppRoute(props: ContextAppProps) { return ; } - return ; + return ; } diff --git a/src/plugins/discover/public/application/apps/context/services/_stubs.ts b/src/plugins/discover/public/application/apps/context/services/_stubs.ts index 7e1473b876afc..e8d09e548c07a 100644 --- a/src/plugins/discover/public/application/apps/context/services/_stubs.ts +++ b/src/plugins/discover/public/application/apps/context/services/_stubs.ts @@ -9,7 +9,6 @@ import sinon from 'sinon'; import moment from 'moment'; -import { IndexPatternsContract } from '../../../../../../data/public'; import { EsHitRecordList } from '../../../types'; type SortHit = { @@ -18,18 +17,6 @@ type SortHit = { sort: [number, number]; }; -export function createIndexPatternsStub() { - return { - get: sinon.spy((indexPatternId) => - Promise.resolve({ - id: indexPatternId, - isTimeNanosBased: () => false, - popularizeField: () => {}, - }) - ), - } as unknown as IndexPatternsContract; -} - /** * A stubbed search source with a `fetch` method that returns all of `_stubHits`. */ diff --git a/src/plugins/discover/public/application/apps/context/services/anchor.test.ts b/src/plugins/discover/public/application/apps/context/services/anchor.test.ts index b4a76fa45ec2f..8886c8ab11f64 100644 --- a/src/plugins/discover/public/application/apps/context/services/anchor.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/anchor.test.ts @@ -6,30 +6,29 @@ * Side Public License, v 1. */ -import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; -import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; -import { fetchAnchorProvider, updateSearchSource } from './anchor'; +import { IndexPattern, SortDirection } from '../../../../../../data/public'; +import { createSearchSourceStub } from './_stubs'; +import { fetchAnchor, updateSearchSource } from './anchor'; import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { EsHitRecord, EsHitRecordList } from '../../../types'; +import { EsHitRecordList } from '../../../types'; describe('context app', function () { - let fetchAnchor: ( - indexPatternId: string, - anchorId: string, - sort: EsQuerySortValue[] - ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let searchSourceStub: any; + const indexPattern = { + id: 'INDEX_PATTERN_ID', + isTimeNanosBased: () => false, + popularizeField: () => {}, + } as unknown as IndexPattern; describe('function fetchAnchor', function () { beforeEach(() => { searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }] as unknown as EsHitRecordList); - fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); it('should use the `fetch` method of the SearchSource', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -38,7 +37,7 @@ describe('context app', function () { }); it('should configure the SearchSource to not inherit from the implicit root', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -49,7 +48,7 @@ describe('context app', function () { }); it('should set the SearchSource index pattern', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -59,7 +58,7 @@ describe('context app', function () { }); it('should set the SearchSource version flag to true', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -70,7 +69,7 @@ describe('context app', function () { }); it('should set the SearchSource size to 1', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -81,7 +80,7 @@ describe('context app', function () { }); it('should set the SearchSource query to an ids query', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -103,7 +102,7 @@ describe('context app', function () { }); it('should set the SearchSource sort order', function () { - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then(() => { @@ -145,7 +144,7 @@ describe('context app', function () { it('should reject with an error when no hits were found', function () { searchSourceStub._stubHits = []; - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then( @@ -161,7 +160,7 @@ describe('context app', function () { it('should return the first hit after adding an anchor marker', function () { searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ + return fetchAnchor('id', indexPattern, searchSourceStub, [ { '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }, ]).then((anchorDocument) => { @@ -174,16 +173,18 @@ describe('context app', function () { describe('useNewFields API', () => { beforeEach(() => { searchSourceStub = createSearchSourceStub([{ _id: 'hit1' }] as unknown as EsHitRecordList); - fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); it('should request fields if useNewFieldsApi set', function () { searchSourceStub._stubHits = [{ property1: 'value1' }, { property2: 'value2' }]; - return fetchAnchor('INDEX_PATTERN_ID', 'id', [ - { '@timestamp': SortDirection.desc }, - { _doc: SortDirection.desc }, - ]).then(() => { + return fetchAnchor( + 'id', + indexPattern, + searchSourceStub, + [{ '@timestamp': SortDirection.desc }, { _doc: SortDirection.desc }], + true + ).then(() => { const setFieldsSpy = searchSourceStub.setField.withArgs('fields'); const removeFieldsSpy = searchSourceStub.removeField.withArgs('fieldsFromSource'); expect(setFieldsSpy.calledOnce).toBe(true); diff --git a/src/plugins/discover/public/application/apps/context/services/anchor.ts b/src/plugins/discover/public/application/apps/context/services/anchor.ts index 2d64f7526ffdd..f262d440b8a28 100644 --- a/src/plugins/discover/public/application/apps/context/services/anchor.ts +++ b/src/plugins/discover/public/application/apps/context/services/anchor.ts @@ -6,46 +6,35 @@ * Side Public License, v 1. */ -import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; -import { - ISearchSource, - IndexPatternsContract, - EsQuerySortValue, - IndexPattern, -} from '../../../../../../data/public'; +import { ISearchSource, EsQuerySortValue, IndexPattern } from '../../../../../../data/public'; import { EsHitRecord } from '../../../types'; -export function fetchAnchorProvider( - indexPatterns: IndexPatternsContract, +export async function fetchAnchor( + anchorId: string, + indexPattern: IndexPattern, searchSource: ISearchSource, + sort: EsQuerySortValue[], useNewFieldsApi: boolean = false -) { - return async function fetchAnchor( - indexPatternId: string, - anchorId: string, - sort: EsQuerySortValue[] - ): Promise { - const indexPattern = await indexPatterns.get(indexPatternId); - updateSearchSource(searchSource, anchorId, sort, useNewFieldsApi, indexPattern); +): Promise { + updateSearchSource(searchSource, anchorId, sort, useNewFieldsApi, indexPattern); - const response = await searchSource.fetch(); - const doc = get(response, ['hits', 'hits', 0]); + const response = await searchSource.fetch(); + const doc = response.hits?.hits?.[0]; - if (!doc) { - throw new Error( - i18n.translate('discover.context.failedToLoadAnchorDocumentErrorDescription', { - defaultMessage: 'Failed to load anchor document.', - }) - ); - } + if (!doc) { + throw new Error( + i18n.translate('discover.context.failedToLoadAnchorDocumentErrorDescription', { + defaultMessage: 'Failed to load anchor document.', + }) + ); + } - return { - ...doc, - isAnchor: true, - } as EsHitRecord; - }; + return { + ...doc, + isAnchor: true, + } as EsHitRecord; } export function updateSearchSource( diff --git a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts index 028dec7b9fe19..9bcf6f9c90d2c 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.predecessors.test.ts @@ -8,9 +8,9 @@ import moment from 'moment'; import { get, last } from 'lodash'; -import { SortDirection } from 'src/plugins/data/common'; -import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; -import { fetchContextProvider, SurrDocType } from './context'; +import { IndexPattern, SortDirection } from 'src/plugins/data/common'; +import { createContextSearchSourceStub } from './_stubs'; +import { fetchSurroundingDocs, SurrDocType } from './context'; import { setServices } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; @@ -30,9 +30,6 @@ interface Timestamp { describe('context predecessors', function () { let fetchPredecessors: ( - indexPatternId: string, - timeField: string, - sortDir: SortDirection, timeValIso: string, timeValNr: number, tieBreakerField: string, @@ -41,6 +38,12 @@ describe('context predecessors', function () { ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; + const indexPattern = { + id: 'INDEX_PATTERN_ID', + timeFieldName: '@timestamp', + isTimeNanosBased: () => false, + popularizeField: () => {}, + } as unknown as IndexPattern; describe('function fetchPredecessors', function () { beforeEach(() => { @@ -56,30 +59,20 @@ describe('context predecessors', function () { }, } as unknown as DiscoverServices); - fetchPredecessors = ( - indexPatternId, - timeField, - sortDir, - timeValIso, - timeValNr, - tieBreakerField, - tieBreakerValue, - size = 10 - ) => { + fetchPredecessors = (timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size = 10) => { const anchor = { _source: { - [timeField]: timeValIso, + [indexPattern.timeFieldName!]: timeValIso, }, sort: [timeValNr, tieBreakerValue], }; - return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( + return fetchSurroundingDocs( SurrDocType.PREDECESSORS, - indexPatternId, + indexPattern, anchor as EsHitRecord, - timeField, tieBreakerField, - sortDir, + SortDirection.desc, size, [] ); @@ -95,19 +88,12 @@ describe('context predecessors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 3 - ).then((hits: EsHitRecordList) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); - expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( + (hits: EsHitRecordList) => { + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); + } + ); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function () { @@ -119,33 +105,26 @@ describe('context predecessors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 6 - ).then((hits: EsHitRecordList) => { - const intervals: Timestamp[] = mockSearchSource.setField.args - .filter(([property]: string) => property === 'query') - .map(([, { query }]: [string, { query: Query }]) => - get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) - ); - - expect( - intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) - ).toBe(true); - // should have started at the given time - expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); - // should have ended with a half-open interval - expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'gte']); - expect(intervals.length).toBeGreaterThan(1); - - expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 6).then( + (hits: EsHitRecordList) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: string) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => + get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) + ); + + expect( + intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) + ).toBe(true); + // should have started at the given time + expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); + // should have ended with a half-open interval + expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'gte']); + expect(intervals.length).toBeGreaterThan(1); + + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); + } + ); }); it('should perform multiple queries until the expected hit count is returned', function () { @@ -156,57 +135,41 @@ describe('context predecessors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_1000, - MS_PER_DAY * 1000, - '_doc', - 0, - 3 - ).then((hits: EsHitRecordList) => { - const intervals: Timestamp[] = mockSearchSource.setField.args - .filter(([property]: string) => property === 'query') - .map(([, { query }]: [string, { query: Query }]) => { - return get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']); - }); - - // should have started at the given time - expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 1000).toISOString()); - // should have stopped before reaching MS_PER_DAY * 1700 - expect(moment(last(intervals)?.lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); - expect(intervals.length).toBeGreaterThan(1); - expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_1000, MS_PER_DAY * 1000, '_doc', 0, 3).then( + (hits: EsHitRecordList) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: string) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => { + return get(query, [ + 'bool', + 'must', + 'constant_score', + 'filter', + 'range', + '@timestamp', + ]); + }); + + // should have started at the given time + expect(intervals[0].gte).toEqual(moment(MS_PER_DAY * 1000).toISOString()); + // should have stopped before reaching MS_PER_DAY * 1700 + expect(moment(last(intervals)?.lte).valueOf()).toBeLessThan(MS_PER_DAY * 1700); + expect(intervals.length).toBeGreaterThan(1); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); + } + ); }); it('should return an empty array when no hits were found', function () { - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3, - MS_PER_DAY * 3, - '_doc', - 0, - 3 - ).then((hits: EsHitRecordList) => { - expect(hits).toEqual([]); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then( + (hits: EsHitRecordList) => { + expect(hits).toEqual([]); + } + ); }); it('should configure the SearchSource to not inherit from the implicit root', function () { - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3, - MS_PER_DAY * 3, - '_doc', - 0, - 3 - ).then(() => { + return fetchPredecessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then(() => { const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); @@ -214,16 +177,7 @@ describe('context predecessors', function () { }); it('should set the tiebreaker sort order to the opposite as the time field', function () { - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP, - MS_PER_DAY, - '_doc', - 0, - 3 - ).then(() => { + return fetchPredecessors(ANCHOR_TIMESTAMP, MS_PER_DAY, '_doc', 0, 3).then(() => { expect( mockSearchSource.setField.calledWith('sort', [ { '@timestamp': { order: 'asc', format: 'strict_date_optional_time' } }, @@ -248,32 +202,23 @@ describe('context predecessors', function () { }, } as unknown as DiscoverServices); - fetchPredecessors = ( - indexPatternId, - timeField, - sortDir, - timeValIso, - timeValNr, - tieBreakerField, - tieBreakerValue, - size = 10 - ) => { + fetchPredecessors = (timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size = 10) => { const anchor = { _source: { - [timeField]: timeValIso, + [indexPattern.timeFieldName!]: timeValIso, }, sort: [timeValNr, tieBreakerValue], }; - return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( + return fetchSurroundingDocs( SurrDocType.PREDECESSORS, - indexPatternId, + indexPattern, anchor as EsHitRecord, - timeField, tieBreakerField, - sortDir, + SortDirection.desc, size, - [] + [], + true ); }; }); @@ -287,23 +232,16 @@ describe('context predecessors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; - return fetchPredecessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 3 - ).then((hits: EsHitRecordList) => { - const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); - const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); - expect(mockSearchSource.fetch.calledOnce).toBe(true); - expect(removeFieldsSpy.calledOnce).toBe(true); - expect(setFieldsSpy.calledOnce).toBe(true); - expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); - }); + return fetchPredecessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( + (hits: EsHitRecordList) => { + const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); + const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(removeFieldsSpy.calledOnce).toBe(true); + expect(setFieldsSpy.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 3)); + } + ); }); }); }); diff --git a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts index 656491f01f9cf..169d969753645 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.successors.test.ts @@ -8,11 +8,11 @@ import moment from 'moment'; import { get, last } from 'lodash'; -import { SortDirection } from 'src/plugins/data/common'; -import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; +import { IndexPattern, SortDirection } from 'src/plugins/data/common'; +import { createContextSearchSourceStub } from './_stubs'; import { setServices } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; -import { fetchContextProvider, SurrDocType } from './context'; +import { fetchSurroundingDocs, SurrDocType } from './context'; import { DiscoverServices } from '../../../../build_services'; import { EsHitRecord, EsHitRecordList } from '../../../types'; @@ -29,9 +29,6 @@ interface Timestamp { describe('context successors', function () { let fetchSuccessors: ( - indexPatternId: string, - timeField: string, - sortDir: SortDirection, timeValIso: string, timeValNr: number, tieBreakerField: string, @@ -40,6 +37,12 @@ describe('context successors', function () { ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let mockSearchSource: any; + const indexPattern = { + id: 'INDEX_PATTERN_ID', + timeFieldName: '@timestamp', + isTimeNanosBased: () => false, + popularizeField: () => {}, + } as unknown as IndexPattern; describe('function fetchSuccessors', function () { beforeEach(() => { @@ -55,30 +58,20 @@ describe('context successors', function () { }, } as unknown as DiscoverServices); - fetchSuccessors = ( - indexPatternId, - timeField, - sortDir, - timeValIso, - timeValNr, - tieBreakerField, - tieBreakerValue, - size - ) => { + fetchSuccessors = (timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size) => { const anchor = { _source: { - [timeField]: timeValIso, + [indexPattern.timeFieldName!]: timeValIso, }, sort: [timeValNr, tieBreakerValue], }; - return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( + return fetchSurroundingDocs( SurrDocType.SUCCESSORS, - indexPatternId, + indexPattern, anchor as EsHitRecord, - timeField, tieBreakerField, - sortDir, + SortDirection.desc, size, [] ); @@ -94,19 +87,12 @@ describe('context successors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), ]; - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 3 - ).then((hits) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); - expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); - }); + return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( + (hits) => { + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); + } + ); }); it('should perform multiple queries with the last being unrestricted when too few hits are returned', function () { @@ -118,33 +104,26 @@ describe('context successors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 2990), ]; - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 6 - ).then((hits) => { - const intervals: Timestamp[] = mockSearchSource.setField.args - .filter(([property]: [string]) => property === 'query') - .map(([, { query }]: [string, { query: Query }]) => - get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) - ); - - expect( - intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) - ).toBe(true); - // should have started at the given time - expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); - // should have ended with a half-open interval - expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'lte']); - expect(intervals.length).toBeGreaterThan(1); - - expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); - }); + return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 6).then( + (hits) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: [string]) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => + get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) + ); + + expect( + intervals.every(({ gte, lte }) => (gte && lte ? moment(gte).isBefore(lte) : true)) + ).toBe(true); + // should have started at the given time + expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); + // should have ended with a half-open interval + expect(Object.keys(last(intervals) ?? {})).toEqual(['format', 'lte']); + expect(intervals.length).toBeGreaterThan(1); + + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); + } + ); }); it('should perform multiple queries until the expected hit count is returned', function () { @@ -157,58 +136,33 @@ describe('context successors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 1000), ]; - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 4 - ).then((hits) => { - const intervals: Timestamp[] = mockSearchSource.setField.args - .filter(([property]: [string]) => property === 'query') - .map(([, { query }]: [string, { query: Query }]) => - get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) - ); - - // should have started at the given time - expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); - // should have stopped before reaching MS_PER_DAY * 2200 - expect(moment(last(intervals)?.gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); - expect(intervals.length).toBeGreaterThan(1); - - expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); - }); + return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 4).then( + (hits) => { + const intervals: Timestamp[] = mockSearchSource.setField.args + .filter(([property]: [string]) => property === 'query') + .map(([, { query }]: [string, { query: Query }]) => + get(query, ['bool', 'must', 'constant_score', 'filter', 'range', '@timestamp']) + ); + + // should have started at the given time + expect(intervals[0].lte).toEqual(moment(MS_PER_DAY * 3000).toISOString()); + // should have stopped before reaching MS_PER_DAY * 2200 + expect(moment(last(intervals)?.gte).valueOf()).toBeGreaterThan(MS_PER_DAY * 2200); + expect(intervals.length).toBeGreaterThan(1); + + expect(hits).toEqual(mockSearchSource._stubHits.slice(0, 4)); + } + ); }); it('should return an empty array when no hits were found', function () { - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3, - MS_PER_DAY * 3, - '_doc', - 0, - 3 - ).then((hits) => { + return fetchSuccessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then((hits) => { expect(hits).toEqual([]); }); }); it('should configure the SearchSource to not inherit from the implicit root', function () { - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3, - MS_PER_DAY * 3, - '_doc', - 0, - 3 - ).then(() => { + return fetchSuccessors(ANCHOR_TIMESTAMP_3, MS_PER_DAY * 3, '_doc', 0, 3).then(() => { const setParentSpy = mockSearchSource.setParent; expect(setParentSpy.alwaysCalledWith(undefined)).toBe(true); expect(setParentSpy.called).toBe(true); @@ -216,16 +170,7 @@ describe('context successors', function () { }); it('should set the tiebreaker sort order to the same as the time field', function () { - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP, - MS_PER_DAY, - '_doc', - 0, - 3 - ).then(() => { + return fetchSuccessors(ANCHOR_TIMESTAMP, MS_PER_DAY, '_doc', 0, 3).then(() => { expect( mockSearchSource.setField.calledWith('sort', [ { '@timestamp': { order: SortDirection.desc, format: 'strict_date_optional_time' } }, @@ -250,32 +195,23 @@ describe('context successors', function () { }, } as unknown as DiscoverServices); - fetchSuccessors = ( - indexPatternId, - timeField, - sortDir, - timeValIso, - timeValNr, - tieBreakerField, - tieBreakerValue, - size - ) => { + fetchSuccessors = (timeValIso, timeValNr, tieBreakerField, tieBreakerValue, size) => { const anchor = { _source: { - [timeField]: timeValIso, + [indexPattern.timeFieldName!]: timeValIso, }, sort: [timeValNr, tieBreakerValue], }; - return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( + return fetchSurroundingDocs( SurrDocType.SUCCESSORS, - indexPatternId, + indexPattern, anchor as EsHitRecord, - timeField, tieBreakerField, - sortDir, + SortDirection.desc, size, - [] + [], + true ); }; }); @@ -289,23 +225,16 @@ describe('context successors', function () { mockSearchSource._createStubHit(MS_PER_DAY * 3000 - 2), ]; - return fetchSuccessors( - 'INDEX_PATTERN_ID', - '@timestamp', - SortDirection.desc, - ANCHOR_TIMESTAMP_3000, - MS_PER_DAY * 3000, - '_doc', - 0, - 3 - ).then((hits) => { - expect(mockSearchSource.fetch.calledOnce).toBe(true); - expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); - const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); - const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); - expect(removeFieldsSpy.calledOnce).toBe(true); - expect(setFieldsSpy.calledOnce).toBe(true); - }); + return fetchSuccessors(ANCHOR_TIMESTAMP_3000, MS_PER_DAY * 3000, '_doc', 0, 3).then( + (hits) => { + expect(mockSearchSource.fetch.calledOnce).toBe(true); + expect(hits).toEqual(mockSearchSource._stubHits.slice(-3)); + const setFieldsSpy = mockSearchSource.setField.withArgs('fields'); + const removeFieldsSpy = mockSearchSource.removeField.withArgs('fieldsFromSource'); + expect(removeFieldsSpy.calledOnce).toBe(true); + expect(setFieldsSpy.calledOnce).toBe(true); + } + ); }); }); }); diff --git a/src/plugins/discover/public/application/apps/context/services/context.ts b/src/plugins/discover/public/application/apps/context/services/context.ts index 237de8e52e656..b76b5ac648c22 100644 --- a/src/plugins/discover/public/application/apps/context/services/context.ts +++ b/src/plugins/discover/public/application/apps/context/services/context.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { Filter, IndexPattern, IndexPatternsContract, SearchSource } from 'src/plugins/data/public'; +import { Filter, IndexPattern, SearchSource } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { convertIsoToMillis, extractNanos } from './utils/date_conversion'; import { fetchHitsInInterval } from './utils/fetch_hits_in_interval'; @@ -25,88 +25,82 @@ const DAY_MILLIS = 24 * 60 * 60 * 1000; // look from 1 day up to 10000 days into the past and future const LOOKUP_OFFSETS = [0, 1, 7, 30, 365, 10000].map((days) => days * DAY_MILLIS); -function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFieldsApi?: boolean) { - return { - fetchSurroundingDocs, - }; - - /** - * Fetch successor or predecessor documents of a given anchor document - * - * @param {SurrDocType} type - `successors` or `predecessors` - * @param {string} indexPatternId - * @param {EsHitRecord} anchor - anchor record - * @param {string} timeField - name of the timefield, that's sorted on - * @param {string} tieBreakerField - name of the tie breaker, the 2nd sort field - * @param {SortDirection} sortDir - direction of sorting - * @param {number} size - number of records to retrieve - * @param {Filter[]} filters - to apply in the elastic query - * @returns {Promise} - */ - async function fetchSurroundingDocs( - type: SurrDocType, - indexPatternId: string, - anchor: EsHitRecord, - timeField: string, - tieBreakerField: string, - sortDir: SortDirection, - size: number, - filters: Filter[] - ): Promise { - if (typeof anchor !== 'object' || anchor === null || !size) { - return []; - } - const indexPattern = await indexPatterns.get(indexPatternId); - const { data } = getServices(); - const searchSource = data.search.searchSource.createEmpty() as SearchSource; - updateSearchSource(searchSource, indexPattern, filters, Boolean(useNewFieldsApi)); - const sortDirToApply = type === SurrDocType.SUCCESSORS ? sortDir : reverseSortDir(sortDir); - - const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor.fields[timeField][0]) : ''; - const timeValueMillis = - nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0]; - - const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); - let documents: EsHitRecordList = []; - - for (const interval of intervals) { - const remainingSize = size - documents.length; - - if (remainingSize <= 0) { - break; - } +/** + * Fetch successor or predecessor documents of a given anchor document + * + * @param {SurrDocType} type - `successors` or `predecessors` + * @param {IndexPattern} indexPattern + * @param {EsHitRecord} anchor - anchor record + * @param {string} tieBreakerField - name of the tie breaker, the 2nd sort field + * @param {SortDirection} sortDir - direction of sorting + * @param {number} size - number of records to retrieve + * @param {Filter[]} filters - to apply in the elastic query + * @param {boolean} useNewFieldsApi + * @returns {Promise} + */ +export async function fetchSurroundingDocs( + type: SurrDocType, + indexPattern: IndexPattern, + anchor: EsHitRecord, + tieBreakerField: string, + sortDir: SortDirection, + size: number, + filters: Filter[], + useNewFieldsApi?: boolean +): Promise { + if (typeof anchor !== 'object' || anchor === null || !size) { + return []; + } + const { data } = getServices(); + const timeField = indexPattern.timeFieldName!; + const searchSource = data.search.searchSource.createEmpty() as SearchSource; + updateSearchSource(searchSource, indexPattern, filters, Boolean(useNewFieldsApi)); + const sortDirToApply = type === SurrDocType.SUCCESSORS ? sortDir : reverseSortDir(sortDir); - const searchAfter = getEsQuerySearchAfter( - type, - documents, - timeField, - anchor, - nanos, - useNewFieldsApi - ); + const nanos = indexPattern.isTimeNanosBased() ? extractNanos(anchor.fields[timeField][0]) : ''; + const timeValueMillis = + nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0]; - const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply, nanos); + const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); + let documents: EsHitRecordList = []; - const hits = await fetchHitsInInterval( - searchSource, - timeField, - sort, - sortDirToApply, - interval, - searchAfter, - remainingSize, - nanos, - anchor._id - ); + for (const interval of intervals) { + const remainingSize = size - documents.length; - documents = - type === SurrDocType.SUCCESSORS - ? [...documents, ...hits] - : [...hits.slice().reverse(), ...documents]; + if (remainingSize <= 0) { + break; } - return documents; + const searchAfter = getEsQuerySearchAfter( + type, + documents, + timeField, + anchor, + nanos, + useNewFieldsApi + ); + + const sort = getEsQuerySort(timeField, tieBreakerField, sortDirToApply, nanos); + + const hits = await fetchHitsInInterval( + searchSource, + timeField, + sort, + sortDirToApply, + interval, + searchAfter, + remainingSize, + nanos, + anchor._id + ); + + documents = + type === SurrDocType.SUCCESSORS + ? [...documents, ...hits] + : [...hits.slice().reverse(), ...documents]; } + + return documents; } export function updateSearchSource( @@ -125,5 +119,3 @@ export function updateSearchSource( .setField('filter', filters) .setField('trackTotalHits', false); } - -export { fetchContextProvider }; diff --git a/src/plugins/discover/public/application/apps/context/services/context_state.test.ts b/src/plugins/discover/public/application/apps/context/services/context_state.test.ts index 3e5acccff634e..3df8ab710729f 100644 --- a/src/plugins/discover/public/application/apps/context/services/context_state.test.ts +++ b/src/plugins/discover/public/application/apps/context/services/context_state.test.ts @@ -24,7 +24,6 @@ describe('Test Discover Context State', () => { history.push('/'); state = getState({ defaultSize: 4, - timeFieldName: 'time', history, uiSettings: { get: (key: string) => @@ -44,12 +43,6 @@ describe('Test Discover Context State', () => { ], "filters": Array [], "predecessorCount": 4, - "sort": Array [ - Array [ - "time", - "desc", - ], - ], "successorCount": 4, } `); @@ -62,41 +55,29 @@ describe('Test Discover Context State', () => { state.setAppState({ predecessorCount: 10 }); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(!(time,desc)),successorCount:4)"` + `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,successorCount:4)"` ); }); test('getState -> url to appState syncing', async () => { - history.push( - '/#?_a=(columns:!(_source),predecessorCount:1,sort:!(time,desc),successorCount:1)' - ); + history.push('/#?_a=(columns:!(_source),predecessorCount:1,successorCount:1)'); expect(state.appState.getState()).toMatchInlineSnapshot(` Object { "columns": Array [ "_source", ], "predecessorCount": 1, - "sort": Array [ - "time", - "desc", - ], "successorCount": 1, } `); }); test('getState -> url to appState syncing with return to a url without state', async () => { - history.push( - '/#?_a=(columns:!(_source),predecessorCount:1,sort:!(time,desc),successorCount:1)' - ); + history.push('/#?_a=(columns:!(_source),predecessorCount:1,successorCount:1)'); expect(state.appState.getState()).toMatchInlineSnapshot(` Object { "columns": Array [ "_source", ], "predecessorCount": 1, - "sort": Array [ - "time", - "desc", - ], "successorCount": 1, } `); @@ -107,10 +88,6 @@ describe('Test Discover Context State', () => { "_source", ], "predecessorCount": 1, - "sort": Array [ - "time", - "desc", - ], "successorCount": 1, } `); @@ -183,7 +160,7 @@ describe('Test Discover Context State', () => { `); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match_phrase:(extension:(query:jpg))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match_phrase:(extension:(query:png))))),predecessorCount:4,sort:!(!(time,desc)),successorCount:4)"` + `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match_phrase:(extension:(query:jpg))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match_phrase:(extension:(query:png))))),predecessorCount:4,successorCount:4)"` ); }); }); diff --git a/src/plugins/discover/public/application/apps/context/services/context_state.ts b/src/plugins/discover/public/application/apps/context/services/context_state.ts index 582ca196e3484..87f7cf00bafcf 100644 --- a/src/plugins/discover/public/application/apps/context/services/context_state.ts +++ b/src/plugins/discover/public/application/apps/context/services/context_state.ts @@ -16,7 +16,7 @@ import { withNotifyOnErrors, ReduxLikeStateContainer, } from '../../../../../../kibana_utils/public'; -import { esFilters, FilterManager, Filter, SortDirection } from '../../../../../../data/public'; +import { esFilters, FilterManager, Filter } from '../../../../../../data/public'; import { handleSourceColumnState } from '../../../helpers/state_helpers'; export interface AppState { @@ -32,14 +32,16 @@ export interface AppState { * Number of records to be fetched before anchor records (newer records) */ predecessorCount: number; - /** - * Sorting of the records to be fetched, assumed to be a legacy parameter - */ - sort: string[][]; /** * Number of records to be fetched after the anchor records (older records) */ successorCount: number; + /** + * Array of the used sorting [[field,direction],...] + * this is actually not needed in Discover Context, there's no sorting + * but it's used in the DocTable component + */ + sort?: string[][]; } interface GlobalState { @@ -54,10 +56,6 @@ export interface GetStateParams { * Number of records to be fetched when 'Load' link/button is clicked */ defaultSize: number; - /** - * The timefield used for sorting - */ - timeFieldName: string; /** * Determins the use of long vs. short/hashed urls */ @@ -124,7 +122,6 @@ const APP_STATE_URL_KEY = '_a'; */ export function getState({ defaultSize, - timeFieldName, storeInSessionStorage = false, history, toasts, @@ -140,12 +137,7 @@ export function getState({ const globalStateContainer = createStateContainer(globalStateInitial); const appStateFromUrl = stateStorage.get(APP_STATE_URL_KEY) as AppState; - const appStateInitial = createInitialAppState( - defaultSize, - timeFieldName, - appStateFromUrl, - uiSettings - ); + const appStateInitial = createInitialAppState(defaultSize, appStateFromUrl, uiSettings); const appStateContainer = createStateContainer(appStateInitial); const { start, stop } = syncStates([ @@ -267,7 +259,6 @@ function getFilters(state: AppState | GlobalState): Filter[] { */ function createInitialAppState( defaultSize: number, - timeFieldName: string, urlState: AppState, uiSettings: IUiSettingsClient ): AppState { @@ -276,7 +267,6 @@ function createInitialAppState( filters: [], predecessorCount: defaultSize, successorCount: defaultSize, - sort: [[timeFieldName, SortDirection.desc]], }; if (typeof urlState !== 'object') { return defaultState; diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts index 5efd5e1195c5d..b3626f9c06f10 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.test.ts @@ -8,12 +8,9 @@ import { act, renderHook } from '@testing-library/react-hooks'; import { setServices, getServices } from '../../../../kibana_services'; -import { SortDirection } from '../../../../../../data/public'; import { createFilterManagerMock } from '../../../../../../data/public/query/filter_manager/filter_manager.mock'; import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; import { DiscoverServices } from '../../../../build_services'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; -import { indexPatternsMock } from '../../../../__mocks__/index_patterns'; import { FailureReason, LoadingStatus } from '../services/context_query_state'; import { ContextAppFetchProps, useContextAppFetch } from './use_context_app_fetch'; import { @@ -21,6 +18,9 @@ import { mockPredecessorHits, mockSuccessorHits, } from '../__mocks__/use_context_app_fetch'; +import { indexPatternWithTimefieldMock } from '../../../../__mocks__/index_pattern_with_timefield'; +import { createContextSearchSourceStub } from '../services/_stubs'; +import { IndexPattern } from '../../../../../../data_views/common'; const mockFilterManager = createFilterManagerMock(); @@ -28,20 +28,19 @@ jest.mock('../services/context', () => { const originalModule = jest.requireActual('../services/context'); return { ...originalModule, - fetchContextProvider: () => ({ - fetchSurroundingDocs: (type: string, indexPatternId: string) => { - if (!indexPatternId) { - throw new Error(); - } - return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; - }, - }), + + fetchSurroundingDocs: (type: string, indexPattern: IndexPattern) => { + if (!indexPattern || !indexPattern.id) { + throw new Error(); + } + return type === 'predecessors' ? mockPredecessorHits : mockSuccessorHits; + }, }; }); jest.mock('../services/anchor', () => ({ - fetchAnchorProvider: () => (indexPatternId: string) => { - if (!indexPatternId) { + fetchAnchor: (anchorId: string, indexPattern: IndexPattern) => { + if (!indexPattern.id || !anchorId) { throw new Error(); } return mockAnchorHit; @@ -50,16 +49,16 @@ jest.mock('../services/anchor', () => ({ const initDefaults = (tieBreakerFields: string[], indexPatternId = 'the-index-pattern-id') => { const dangerNotification = jest.fn(); + const mockSearchSource = createContextSearchSourceStub('timestamp'); setServices({ data: { search: { searchSource: { - createEmpty: jest.fn(), + createEmpty: jest.fn().mockImplementation(() => mockSearchSource), }, }, }, - indexPatterns: indexPatternsMock, toastNotifications: { addDanger: dangerNotification }, core: { notifications: { toasts: [] } }, history: () => {}, @@ -77,10 +76,8 @@ const initDefaults = (tieBreakerFields: string[], indexPatternId = 'the-index-pa dangerNotification, props: { anchorId: 'mock_anchor_id', - indexPatternId, - indexPattern: indexPatternMock, + indexPattern: { ...indexPatternWithTimefieldMock, id: indexPatternId }, appState: { - sort: [['order_date', SortDirection.desc]], predecessorCount: 2, successorCount: 2, }, diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx index fa6a761397335..ed3b4e8ed5b5a 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_fetch.tsx @@ -7,11 +7,10 @@ */ import React, { useCallback, useMemo, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { fromPairs } from 'lodash'; import { CONTEXT_TIE_BREAKER_FIELDS_SETTING } from '../../../../../common'; import { DiscoverServices } from '../../../../build_services'; -import { fetchAnchorProvider } from '../services/anchor'; -import { fetchContextProvider, SurrDocType } from '../services/context'; +import { fetchAnchor } from '../services/anchor'; +import { fetchSurroundingDocs, SurrDocType } from '../services/context'; import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; import { IndexPattern, SortDirection } from '../../../../../../data/public'; import { @@ -30,7 +29,6 @@ const createError = (statusKey: string, reason: FailureReason, error?: Error) => export interface ContextAppFetchProps { anchorId: string; - indexPatternId: string; indexPattern: IndexPattern; appState: AppState; useNewFieldsApi: boolean; @@ -39,13 +37,12 @@ export interface ContextAppFetchProps { export function useContextAppFetch({ anchorId, - indexPatternId, indexPattern, appState, useNewFieldsApi, services, }: ContextAppFetchProps) { - const { uiSettings: config, data, indexPatterns, toastNotifications, filterManager } = services; + const { uiSettings: config, data, toastNotifications, filterManager } = services; const searchSource = useMemo(() => { return data.search.searchSource.createEmpty(); @@ -54,13 +51,6 @@ export function useContextAppFetch({ () => getFirstSortableField(indexPattern, config.get(CONTEXT_TIE_BREAKER_FIELDS_SETTING)), [config, indexPattern] ); - const fetchAnchor = useMemo(() => { - return fetchAnchorProvider(indexPatterns, searchSource, useNewFieldsApi); - }, [indexPatterns, searchSource, useNewFieldsApi]); - const { fetchSurroundingDocs } = useMemo( - () => fetchContextProvider(indexPatterns, useNewFieldsApi), - [indexPatterns, useNewFieldsApi] - ); const [fetchedState, setFetchedState] = useState( getInitialContextQueryState() @@ -71,8 +61,6 @@ export function useContextAppFetch({ }, []); const fetchAnchorRow = useCallback(async () => { - const { sort } = appState; - const [[, sortDir]] = sort; const errorTitle = i18n.translate('discover.context.unableToLoadAnchorDocumentDescription', { defaultMessage: 'Unable to load the anchor document', }); @@ -94,10 +82,11 @@ export function useContextAppFetch({ try { setState({ anchorStatus: { value: LoadingStatus.LOADING } }); - const anchor = await fetchAnchor(indexPatternId, anchorId, [ - fromPairs(sort), - { [tieBreakerField]: sortDir }, - ]); + const sort = [ + { [indexPattern.timeFieldName!]: SortDirection.desc }, + { [tieBreakerField]: SortDirection.desc }, + ]; + const anchor = await fetchAnchor(anchorId, indexPattern, searchSource, sort, useNewFieldsApi); setState({ anchor, anchorStatus: { value: LoadingStatus.LOADED } }); return anchor; } catch (error) { @@ -108,20 +97,18 @@ export function useContextAppFetch({ }); } }, [ - appState, tieBreakerField, setState, toastNotifications, - fetchAnchor, - indexPatternId, + indexPattern, anchorId, + searchSource, + useNewFieldsApi, ]); const fetchSurroundingRows = useCallback( async (type: SurrDocType, fetchedAnchor?: EsHitRecord) => { const filters = filterManager.getFilters(); - const { sort } = appState; - const [[sortField, sortDir]] = sort; const count = type === SurrDocType.PREDECESSORS ? appState.predecessorCount : appState.successorCount; @@ -135,13 +122,13 @@ export function useContextAppFetch({ setState({ [statusKey]: { value: LoadingStatus.LOADING } }); const rows = await fetchSurroundingDocs( type, - indexPatternId, + indexPattern, anchor as EsHitRecord, - sortField, tieBreakerField, - sortDir as SortDirection, + SortDirection.desc, count, - filters + filters, + useNewFieldsApi ); setState({ [type]: rows, [statusKey]: { value: LoadingStatus.LOADED } }); } catch (error) { @@ -158,9 +145,9 @@ export function useContextAppFetch({ fetchedState.anchor, tieBreakerField, setState, - fetchSurroundingDocs, - indexPatternId, + indexPattern, toastNotifications, + useNewFieldsApi, ] ); diff --git a/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts b/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts index 3e968b5dfb82e..56701f17c7a63 100644 --- a/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts +++ b/src/plugins/discover/public/application/apps/context/utils/use_context_app_state.ts @@ -9,29 +9,21 @@ import { useEffect, useMemo, useState } from 'react'; import { cloneDeep } from 'lodash'; import { CONTEXT_DEFAULT_SIZE_SETTING } from '../../../../../common'; -import { IndexPattern } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { AppState, getState } from '../services/context_state'; -export function useContextAppState({ - indexPattern, - services, -}: { - indexPattern: IndexPattern; - services: DiscoverServices; -}) { +export function useContextAppState({ services }: { services: DiscoverServices }) { const { uiSettings: config, history, core, filterManager } = services; const stateContainer = useMemo(() => { return getState({ defaultSize: parseInt(config.get(CONTEXT_DEFAULT_SIZE_SETTING), 10), - timeFieldName: indexPattern.timeFieldName!, storeInSessionStorage: config.get('state:storeInSessionStorage'), history: history(), toasts: core.notifications.toasts, uiSettings: config, }); - }, [config, history, indexPattern, core.notifications.toasts]); + }, [config, history, core.notifications.toasts]); const [appState, setState] = useState(stateContainer.appState.getState()); From c3f1e0de541901679de4cb7b2317b5b1f6b58e4f Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Tue, 12 Oct 2021 14:35:45 +0200 Subject: [PATCH 056/287] [FieldFormats] Remove SerializedFieldFormat copy from expressions plugin (#114245) --- src/plugins/data/common/search/aggs/agg_config.ts | 7 ++----- src/plugins/data/common/search/aggs/agg_type.ts | 3 ++- .../data/common/search/aggs/utils/get_aggs_formats.ts | 2 +- src/plugins/data_views/common/data_views/data_view.ts | 7 +++++-- src/plugins/data_views/common/types.ts | 3 +-- .../apps/main/components/chart/point_series.ts | 2 +- .../common/expression_types/specs/datatable.ts | 2 +- src/plugins/expressions/common/types/common.ts | 11 ----------- src/plugins/expressions/common/types/index.ts | 8 +------- src/plugins/expressions/public/index.ts | 1 - src/plugins/expressions/server/index.ts | 1 - src/plugins/expressions/tsconfig.json | 1 + src/plugins/vis_types/pie/public/types/types.ts | 3 ++- .../common/expression_functions/xy_dimension.ts | 2 +- src/plugins/visualizations/public/vis_schemas.ts | 2 +- x-pack/plugins/lens/common/types.ts | 7 +++++-- .../public/xy_visualization/axes_configuration.ts | 7 +++++-- 17 files changed, 29 insertions(+), 40 deletions(-) diff --git a/src/plugins/data/common/search/aggs/agg_config.ts b/src/plugins/data/common/search/aggs/agg_config.ts index 1a70a41e72dd5..4cb091787c058 100644 --- a/src/plugins/data/common/search/aggs/agg_config.ts +++ b/src/plugins/data/common/search/aggs/agg_config.ts @@ -13,11 +13,8 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { Assign, Ensure } from '@kbn/utility-types'; import { ISearchOptions, ISearchSource } from 'src/plugins/data/public'; -import { - ExpressionAstExpression, - ExpressionAstArgument, - SerializedFieldFormat, -} from 'src/plugins/expressions/common'; +import { ExpressionAstExpression, ExpressionAstArgument } from 'src/plugins/expressions/common'; +import type { SerializedFieldFormat } from 'src/plugins/field_formats/common'; import { IAggType } from './agg_type'; import { writeParams } from './agg_params'; diff --git a/src/plugins/data/common/search/aggs/agg_type.ts b/src/plugins/data/common/search/aggs/agg_type.ts index 48ce54bbd61bd..ebc1705f6c01b 100644 --- a/src/plugins/data/common/search/aggs/agg_type.ts +++ b/src/plugins/data/common/search/aggs/agg_type.ts @@ -10,8 +10,9 @@ import { constant, noop, identity } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ISearchSource } from 'src/plugins/data/public'; -import { DatatableColumnType, SerializedFieldFormat } from 'src/plugins/expressions/common'; +import { DatatableColumnType } from 'src/plugins/expressions/common'; import type { RequestAdapter } from 'src/plugins/inspector/common'; +import type { SerializedFieldFormat } from 'src/plugins/field_formats/common'; import { estypes } from '@elastic/elasticsearch'; import { initParams } from './agg_params'; diff --git a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts index 2aead866c6b60..1652f51477e64 100644 --- a/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts +++ b/src/plugins/data/common/search/aggs/utils/get_aggs_formats.ts @@ -9,12 +9,12 @@ /* eslint-disable max-classes-per-file */ import { i18n } from '@kbn/i18n'; -import { SerializedFieldFormat } from 'src/plugins/expressions/common/types'; import { FieldFormat, FieldFormatInstanceType, FieldFormatsContentType, IFieldFormat, + SerializedFieldFormat, } from '../../../../../field_formats/common'; import { DateRange } from '../../expressions'; import { convertDateRangeToString } from '../buckets/lib/date_range'; diff --git a/src/plugins/data_views/common/data_views/data_view.ts b/src/plugins/data_views/common/data_views/data_view.ts index 00b96cda32ad7..5768ebe635729 100644 --- a/src/plugins/data_views/common/data_views/data_view.ts +++ b/src/plugins/data_views/common/data_views/data_view.ts @@ -19,9 +19,12 @@ import { IIndexPattern, IFieldType } from '../../common'; import { DataViewField, IIndexPatternFieldList, fieldList } from '../fields'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; -import { FieldFormatsStartCommon, FieldFormat } from '../../../field_formats/common'; +import { + FieldFormatsStartCommon, + FieldFormat, + SerializedFieldFormat, +} from '../../../field_formats/common'; import { DataViewSpec, TypeMeta, SourceFilter, DataViewFieldMap } from '../types'; -import { SerializedFieldFormat } from '../../../expressions/common'; interface DataViewDeps { spec?: DataViewSpec; diff --git a/src/plugins/data_views/common/types.ts b/src/plugins/data_views/common/types.ts index 2b184bc1ef2a4..bbc5ad374636f 100644 --- a/src/plugins/data_views/common/types.ts +++ b/src/plugins/data_views/common/types.ts @@ -13,9 +13,8 @@ import type { SavedObject } from 'src/core/server'; import { KBN_FIELD_TYPES } from '@kbn/field-types'; import { IFieldType } from './fields'; import { RUNTIME_FIELD_TYPES } from './constants'; -import { SerializedFieldFormat } from '../../expressions/common'; import { DataViewField } from './fields'; -import { FieldFormat } from '../../field_formats/common'; +import { FieldFormat, SerializedFieldFormat } from '../../field_formats/common'; export type FieldFormatMap = Record; diff --git a/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts b/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts index 1245b712e6fd7..ee057fcb48c6a 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts +++ b/src/plugins/discover/public/application/apps/main/components/chart/point_series.ts @@ -9,7 +9,7 @@ import { uniq } from 'lodash'; import { Duration, Moment } from 'moment'; import { Unit } from '@elastic/datemath'; -import { SerializedFieldFormat } from '../../../../../../../expressions/common'; +import type { SerializedFieldFormat } from '../../../../../../../field_formats/common'; export interface Column { id: string; diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index b45c36950f870..a07f103d12e06 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -8,11 +8,11 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { map, pick, zipObject } from 'lodash'; +import type { SerializedFieldFormat } from 'src/plugins/field_formats/common'; import { ExpressionTypeDefinition, ExpressionValueBoxed } from '../types'; import { PointSeries, PointSeriesColumn } from './pointseries'; import { ExpressionValueRender } from './render'; -import { SerializedFieldFormat } from '../../types'; const name = 'datatable'; diff --git a/src/plugins/expressions/common/types/common.ts b/src/plugins/expressions/common/types/common.ts index 64b3d00895f56..b28ff27a79ac1 100644 --- a/src/plugins/expressions/common/types/common.ts +++ b/src/plugins/expressions/common/types/common.ts @@ -46,14 +46,3 @@ export type TypeString = KnownTypeToString< * `date` is typed as a number or string, and represents a date */ export type UnmappedTypeStrings = 'date' | 'filter'; - -/** - * JSON representation of a field formatter configuration. - * Is used to carry information about how to format data in - * a data table as part of the column definition. - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export interface SerializedFieldFormat> { - id?: string; - params?: TParams; -} diff --git a/src/plugins/expressions/common/types/index.ts b/src/plugins/expressions/common/types/index.ts index 7d672df2401ff..00a79289c0b5f 100644 --- a/src/plugins/expressions/common/types/index.ts +++ b/src/plugins/expressions/common/types/index.ts @@ -6,13 +6,7 @@ * Side Public License, v 1. */ -export { - TypeToString, - KnownTypeToString, - TypeString, - UnmappedTypeStrings, - SerializedFieldFormat, -} from './common'; +export { TypeToString, KnownTypeToString, TypeString, UnmappedTypeStrings } from './common'; export * from './style'; export * from './registry'; diff --git a/src/plugins/expressions/public/index.ts b/src/plugins/expressions/public/index.ts index 02656943cac3e..6e8d220a3ca0c 100644 --- a/src/plugins/expressions/public/index.ts +++ b/src/plugins/expressions/public/index.ts @@ -97,7 +97,6 @@ export { PointSeriesRow, Range, SerializedDatatable, - SerializedFieldFormat, Style, TextAlignment, TextDecoration, diff --git a/src/plugins/expressions/server/index.ts b/src/plugins/expressions/server/index.ts index c09a8bf0104af..7c0662ad54e4b 100644 --- a/src/plugins/expressions/server/index.ts +++ b/src/plugins/expressions/server/index.ts @@ -88,7 +88,6 @@ export { PointSeriesRow, Range, SerializedDatatable, - SerializedFieldFormat, Style, TextAlignment, TextDecoration, diff --git a/src/plugins/expressions/tsconfig.json b/src/plugins/expressions/tsconfig.json index 6716149d6b9c7..d9991ff791e83 100644 --- a/src/plugins/expressions/tsconfig.json +++ b/src/plugins/expressions/tsconfig.json @@ -11,5 +11,6 @@ { "path": "../../core/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, { "path": "../inspector/tsconfig.json" }, + { "path": "../field_formats/tsconfig.json" } ] } diff --git a/src/plugins/vis_types/pie/public/types/types.ts b/src/plugins/vis_types/pie/public/types/types.ts index a1f41e80fae28..fb5efb5971805 100644 --- a/src/plugins/vis_types/pie/public/types/types.ts +++ b/src/plugins/vis_types/pie/public/types/types.ts @@ -8,7 +8,8 @@ import { Position } from '@elastic/charts'; import { UiCounterMetricType } from '@kbn/analytics'; -import { DatatableColumn, SerializedFieldFormat } from '../../../../expressions/public'; +import { DatatableColumn } from '../../../../expressions/public'; +import type { SerializedFieldFormat } from '../../../../field_formats/common'; import { ExpressionValueVisDimension } from '../../../../visualizations/public'; import { ExpressionValuePieLabels } from '../expression_functions/pie_labels'; import { PaletteOutput, ChartsPluginSetup } from '../../../../charts/public'; diff --git a/src/plugins/visualizations/common/expression_functions/xy_dimension.ts b/src/plugins/visualizations/common/expression_functions/xy_dimension.ts index 82538fea8605a..5bbddd48e9b8b 100644 --- a/src/plugins/visualizations/common/expression_functions/xy_dimension.ts +++ b/src/plugins/visualizations/common/expression_functions/xy_dimension.ts @@ -14,8 +14,8 @@ import type { ExpressionValueBoxed, Datatable, DatatableColumn, - SerializedFieldFormat, } from '../../../expressions/common'; +import type { SerializedFieldFormat } from '../../../field_formats/common'; export interface DateHistogramParams { date: boolean; diff --git a/src/plugins/visualizations/public/vis_schemas.ts b/src/plugins/visualizations/public/vis_schemas.ts index 115e13ece45ff..f80f85fb55a60 100644 --- a/src/plugins/visualizations/public/vis_schemas.ts +++ b/src/plugins/visualizations/public/vis_schemas.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { SerializedFieldFormat } from '../../expressions/public'; +import type { SerializedFieldFormat } from '../../field_formats/common'; import { IAggConfig, search } from '../../data/public'; import { Vis, VisToExpressionAstParams } from './types'; diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 79450b76190fb..659d3c0eced26 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -6,8 +6,11 @@ */ import type { Filter, FilterMeta } from '@kbn/es-query'; -import type { IFieldFormat } from '../../../../src/plugins/field_formats/common'; -import type { Datatable, SerializedFieldFormat } from '../../../../src/plugins/expressions/common'; +import type { + IFieldFormat, + SerializedFieldFormat, +} from '../../../../src/plugins/field_formats/common'; +import type { Datatable } from '../../../../src/plugins/expressions/common'; export type FormatFactory = (mapping?: SerializedFieldFormat) => IFieldFormat; diff --git a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts index 9c83e2c58146e..9ac0171a51084 100644 --- a/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts +++ b/x-pack/plugins/lens/public/xy_visualization/axes_configuration.ts @@ -7,8 +7,11 @@ import { FormatFactory } from '../../common'; import { AxisExtentConfig, XYLayerConfig } from '../../common/expressions'; -import { Datatable, SerializedFieldFormat } from '../../../../../src/plugins/expressions/public'; -import type { IFieldFormat } from '../../../../../src/plugins/field_formats/common'; +import { Datatable } from '../../../../../src/plugins/expressions/public'; +import type { + IFieldFormat, + SerializedFieldFormat, +} from '../../../../../src/plugins/field_formats/common'; interface FormattedMetric { layer: string; From 4436ed2f7127f1bb31d9ee6fdc0dae54c889ccd5 Mon Sep 17 00:00:00 2001 From: Paul Tavares <56442535+paul-tavares@users.noreply.github.com> Date: Tue, 12 Oct 2021 09:20:11 -0400 Subject: [PATCH 057/287] [Security Solution][Endpoint] Bug fixes and unit test cases for the Trusted Apps list view under Policy Details (#113865) * test: rename mock utility * Tests: artifact grid test coverage * Fix: CardCompressedHeader should use CardCompressedHeaderLayout * Tests: ArtifactEntryCollapsibleCard test coverage * Context Menu adjustments to test ids and to avoid react console errors/warnings * add test id to truncate wrapper in ContextMenuItemWithRouterSupport * Tests for ContextMenuWithRouterSupport * new mocks test utils * HTTP mocks for Policy Details Trusted apps list page * tests for policy trusted apps selectors * Refactor: move reusable fleet http mocks to `page/mocks` * HTTP mocks for fleet get package policy and Agent status + mock for all Policy Details APIs * Tests: Policy Details Trusted Apps List * Moved `seededUUIDv4()` to `BaseDataGenerator` and changed trusted apps generator to use it * change `createStartServicesMock` to optionally accept `coreStart` as input * Show api load errors on policy TA list --- .../data_generators/base_data_generator.ts | 5 + .../data_generators/trusted_app_generator.ts | 2 +- .../common/endpoint/generate_data.ts | 16 +- .../use_endpoint_privileges.test.ts | 2 +- .../common/lib/kibana/kibana_react.mock.ts | 5 +- .../mock/endpoint/app_context_render.tsx | 7 +- .../actions_context_menu.tsx | 4 +- .../artifact_card_grid.test.tsx | 152 ++++++----- .../components/grid_header.tsx | 2 +- .../artifact_entry_card.test.tsx | 10 +- .../artifact_entry_card_minified.test.tsx | 6 +- .../artifact_entry_collapsible_card.test.tsx | 104 +++++++ .../components/card_compressed_header.tsx | 68 ++--- .../artifact_entry_card/test_utils.ts | 24 +- .../context_menu_item_nav_by_router.tsx | 3 + .../context_menu_with_router_support.test.tsx | 133 +++++++++ .../context_menu_with_router_support.tsx | 13 +- .../context_menu_with_router_support/index.ts | 1 + .../management/pages/endpoint_hosts/index.tsx | 2 + .../management/pages/endpoint_hosts/mocks.ts | 120 ++------- .../management/pages/mocks/fleet_mocks.ts | 164 ++++++++++++ .../public/management/pages/mocks/index.ts | 8 + .../selectors/trusted_apps_selectors.test.ts | 185 ++++++++++++- .../selectors/trusted_apps_selectors.ts | 9 + .../pages/policy/test_utils/index.ts | 23 +- .../pages/policy/test_utils/mocks.ts | 123 +++++++++ .../list/policy_trusted_apps_list.test.tsx | 253 ++++++++++++++++++ .../list/policy_trusted_apps_list.tsx | 34 ++- 28 files changed, 1212 insertions(+), 266 deletions(-) create mode 100644 x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.test.tsx create mode 100644 x-pack/plugins/security_solution/public/management/pages/mocks/fleet_mocks.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/mocks/index.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts create mode 100644 x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts index da3f387016b9f..6fbe54578f469 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/base_data_generator.ts @@ -127,6 +127,11 @@ export class BaseDataGenerator { return uuid.v4(); } + /** generate a seeded random UUID v4 */ + protected seededUUIDv4(): string { + return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); + } + /** Generate a random number up to the max provided */ protected randomN(max: number): number { return Math.floor(this.random() * max); diff --git a/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts b/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts index be0178b83be90..91c2e17a1e12d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts +++ b/x-pack/plugins/security_solution/common/endpoint/data_generators/trusted_app_generator.ts @@ -45,7 +45,7 @@ export class TrustedAppGenerator extends BaseDataGenerator { return merge( this.generateTrustedAppForCreate(), { - id: this.randomUUID(), + id: this.seededUUIDv4(), version: this.randomString(5), created_at: this.randomPastDate(), updated_at: new Date().toISOString(), diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 1492e0e8c82c9..3e94dfaebc7fe 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -5,7 +5,6 @@ * 2.0. */ -import uuid from 'uuid'; import seedrandom from 'seedrandom'; import semverLte from 'semver/functions/lte'; import { assertNever } from '@kbn/std'; @@ -32,9 +31,10 @@ import { import { GetAgentPoliciesResponseItem, GetPackagesResponse, -} from '../../../fleet/common/types/rest_spec'; -import { EsAssetReference, KibanaAssetReference } from '../../../fleet/common/types/models'; -import { agentPolicyStatuses } from '../../../fleet/common/constants'; + EsAssetReference, + KibanaAssetReference, + agentPolicyStatuses, +} from '../../../fleet/common'; import { firstNonNullValue } from './models/ecs_safety_helpers'; import { EventOptions } from './types/generator'; import { BaseDataGenerator } from './data_generators/base_data_generator'; @@ -406,6 +406,7 @@ const alertsDefaultDataStream = { export class EndpointDocGenerator extends BaseDataGenerator { commonInfo: HostInfo; sequence: number = 0; + /** * The EndpointDocGenerator parameters * @@ -523,6 +524,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { data_stream: metadataDataStream, }; } + /** * Creates a malware alert from the simulated host represented by this EndpointDocGenerator * @param ts - Timestamp to put in the event @@ -744,6 +746,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { } return newAlert; } + /** * Creates an alert from the simulated host represented by this EndpointDocGenerator * @param ts - Timestamp to put in the event @@ -900,6 +903,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { }; return newAlert; } + /** * Returns the default DLLs used in alerts */ @@ -1871,10 +1875,6 @@ export class EndpointDocGenerator extends BaseDataGenerator { }; } - private seededUUIDv4(): string { - return uuid.v4({ random: [...this.randomNGenerator(255, 16)] }); - } - private randomHostPolicyResponseActionNames(): string[] { return this.randomArray(this.randomN(8), () => this.randomChoice([ diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts index 8e9dae9f12ad5..a05d1ac8d3588 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts @@ -8,10 +8,10 @@ import { renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; import { useHttp, useCurrentUser } from '../../lib/kibana'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; -import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/endpoint_hosts/mocks'; import { securityMock } from '../../../../../security/public/mocks'; import { appRoutesService } from '../../../../../fleet/common'; import { AuthenticatedUser } from '../../../../../security/common'; +import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks'; jest.mock('../../lib/kibana'); diff --git a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts index df821c7ac5f6d..b98618ac76412 100644 --- a/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts +++ b/x-pack/plugins/security_solution/public/common/lib/kibana/kibana_react.mock.ts @@ -91,8 +91,9 @@ export const createUseUiSetting$Mock = () => { ]; }; -export const createStartServicesMock = (): StartServices => { - const core = coreMock.createStart(); +export const createStartServicesMock = ( + core: ReturnType = coreMock.createStart() +): StartServices => { core.uiSettings.get.mockImplementation(createUseUiSettingMock()); const { storage } = createSecuritySolutionStorageMock(); const data = dataPluginMock.createStartContract(); diff --git a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx index f8a77d97b8700..20d411a0437c2 100644 --- a/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/endpoint/app_context_render.tsx @@ -24,8 +24,8 @@ import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { PLUGIN_ID } from '../../../../../fleet/common'; import { APP_ID, APP_PATH } from '../../../../common/constants'; import { KibanaContextProvider, KibanaServices } from '../../lib/kibana'; -import { fleetGetPackageListHttpMock } from '../../../management/pages/endpoint_hosts/mocks'; import { getDeepLinks } from '../../../app/deep_links'; +import { fleetGetPackageListHttpMock } from '../../../management/pages/mocks'; type UiRender = (ui: React.ReactElement, options?: RenderOptions) => RenderResult; @@ -98,10 +98,7 @@ export const createAppRootMockRenderer = (): AppContextTestRender => { const depsStart = depsStartMock(); const middlewareSpy = createSpyMiddleware(); const { storage } = createSecuritySolutionStorageMock(); - const startServices: StartServices = { - ...createStartServicesMock(), - ...coreStart, - }; + const startServices: StartServices = createStartServicesMock(coreStart); const storeReducer = { ...SUB_PLUGINS_REDUCER, diff --git a/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx b/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx index c2f9e32f61afb..c2511a31a73ae 100644 --- a/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/management/components/actions_context_menu/actions_context_menu.tsx @@ -15,10 +15,11 @@ import { EuiIconProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import uuid from 'uuid'; import { ContextMenuItemNavByRouter, ContextMenuItemNavByRouterProps, -} from '../context_menu_with_router_support/context_menu_item_nav_by_router'; +} from '../context_menu_with_router_support'; import { useTestIdGenerator } from '../hooks/use_test_id_generator'; export interface ActionsContextMenuProps { @@ -48,6 +49,7 @@ export const ActionsContextMenu = memo( return ( { handleCloseMenu(); if (itemProps.onClick) { diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx index a44076c8ad112..d360ca8fa168f 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/artifact_card_grid.test.tsx @@ -5,58 +5,20 @@ * 2.0. */ -import { TrustedAppGenerator } from '../../../../common/endpoint/data_generators/trusted_app_generator'; -import { cloneDeep } from 'lodash'; -import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; import React from 'react'; import { ArtifactCardGrid, ArtifactCardGridProps } from './artifact_card_grid'; - -// FIXME:PT refactor helpers below after merge of PR https://github.com/elastic/kibana/pull/113363 - -const getCommonItemDataOverrides = () => { - return { - name: 'some internal app', - description: 'this app is trusted by the company', - created_at: new Date('2021-07-01').toISOString(), - }; -}; - -const getTrustedAppProvider = () => - new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides()); - -const getExceptionProvider = () => { - // cloneDeep needed because exception mock generator uses state across instances - return cloneDeep( - getExceptionListItemSchemaMock({ - ...getCommonItemDataOverrides(), - os_types: ['windows'], - updated_at: new Date().toISOString(), - created_by: 'Justa', - updated_by: 'Mara', - entries: [ - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: '1234234659af249ddf3e40864e9fb241', - }, - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: '/one/two/three', - }, - ], - tags: ['policy:all'], - }) - ); -}; +import { fireEvent, act } from '@testing-library/react'; +import { + getExceptionProviderMock, + getTrustedAppProviderMock, +} from '../artifact_entry_card/test_utils'; +import { AnyArtifact } from '../artifact_entry_card'; describe.each([ - ['trusted apps', getTrustedAppProvider], - ['exceptions/event filters', getExceptionProvider], -])('when using the ArtifactCardGrid component %s', (_, generateItem) => { + ['trusted apps', getTrustedAppProviderMock], + ['exceptions/event filters', getExceptionProviderMock], +])('when using the ArtifactCardGrid component with %s', (_, generateItem) => { let appTestContext: AppContextTestRender; let renderResult: ReturnType; let render: ( @@ -64,34 +26,45 @@ describe.each([ ) => ReturnType; let items: ArtifactCardGridProps['items']; let pageChangeHandler: jest.Mock; - let expandCollapseHandler: jest.Mock; - let cardComponentPropsProvider: Required['cardComponentProps']; + let expandCollapseHandler: jest.MockedFunction; + let cardComponentPropsProvider: jest.MockedFunction< + Required['cardComponentProps'] + >; beforeEach(() => { items = Array.from({ length: 5 }, () => generateItem()); pageChangeHandler = jest.fn(); expandCollapseHandler = jest.fn(); - cardComponentPropsProvider = jest.fn().mockReturnValue({}); + cardComponentPropsProvider = jest.fn((item) => { + return { + 'data-test-subj': `card-${items.indexOf(item as AnyArtifact)}`, + }; + }); appTestContext = createAppRootMockRenderer(); render = (props = {}) => { - renderResult = appTestContext.render( - - ); + const gridProps: ArtifactCardGridProps = { + items, + onPageChange: pageChangeHandler!, + onExpandCollapse: expandCollapseHandler!, + cardComponentProps: cardComponentPropsProvider, + pagination: { + pageSizeOptions: [5, 10], + pageSize: 5, + totalItemCount: items.length, + pageIndex: 0, + }, + 'data-test-subj': 'testGrid', + ...props, + }; + + renderResult = appTestContext.render(); return renderResult; }; }); it('should render the cards', () => { + cardComponentPropsProvider.mockImplementation(() => ({})); render(); expect(renderResult.getAllByTestId('testGrid-card')).toHaveLength(5); @@ -100,24 +73,59 @@ describe.each([ it.each([ ['header', 'testGrid-header'], ['expand/collapse placeholder', 'testGrid-header-expandCollapsePlaceHolder'], - ['name column', 'testGrid-header-layout-title'], - ['description column', 'testGrid-header-layout-description'], + ['name column', 'testGrid-header-layout-titleHolder'], + ['description column', 'testGrid-header-layout-descriptionHolder'], ['description column', 'testGrid-header-layout-cardActionsPlaceholder'], - ])('should display the Grid Header - %s', (__, selector) => { + ])('should display the Grid Header - %s', (__, testSubjId) => { render(); - expect(renderResult.getByTestId(selector)).not.toBeNull(); + expect(renderResult.getByTestId(testSubjId)).not.toBeNull(); }); - it.todo('should call onPageChange callback when paginating'); + it('should call onPageChange callback when paginating', () => { + items = Array.from({ length: 15 }, () => generateItem()); + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('pagination-button-next')); + }); - it.todo('should use the props provided by cardComponentProps callback'); + expect(pageChangeHandler).toHaveBeenCalledWith({ pageIndex: 1, pageSize: 5 }); + }); - describe('and when cards are expanded/collapsed', () => { - it.todo('should call onExpandCollapse callback'); + it('should pass along the props provided by cardComponentProps callback', () => { + cardComponentPropsProvider.mockReturnValue({ 'data-test-subj': 'test-card' }); + render(); - it.todo('should provide list of cards that are expanded and collapsed'); + expect(renderResult.getAllByTestId('test-card')).toHaveLength(5); + }); - it.todo('should show card expanded if card props defined it as such'); + describe('and when cards are expanded/collapsed', () => { + it('should call onExpandCollapse callback', () => { + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('card-0-header-expandCollapse')); + }); + + expect(expandCollapseHandler).toHaveBeenCalledWith({ + expanded: [items[0]], + collapsed: items.slice(1), + }); + }); + + it('should show card expanded if card props defined it as such', () => { + const originalPropsProvider = cardComponentPropsProvider.getMockImplementation(); + cardComponentPropsProvider.mockImplementation((item) => { + const props = originalPropsProvider!(item); + + if (items.indexOf(item as AnyArtifact) === 1) { + props.expanded = true; + } + + return props; + }); + render(); + + expect(renderResult.getByTestId('card-1-criteriaConditions')).not.toBeNull(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx index 03fde724b89a5..fb198e86fc386 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_card_grid/components/grid_header.tsx @@ -63,7 +63,7 @@ export const GridHeader = memo(({ 'data-test-subj': dataTestSub } - actionMenu={false} + actionMenu={true} /> ); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx index 52f0eb5fc8982..52299679ec87b 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx @@ -11,11 +11,11 @@ import { ArtifactEntryCard, ArtifactEntryCardProps } from './artifact_entry_card import { act, fireEvent, getByTestId } from '@testing-library/react'; import { AnyArtifact } from './types'; import { isTrustedApp } from './utils'; -import { getTrustedAppProvider, getExceptionProvider } from './test_utils'; +import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils'; describe.each([ - ['trusted apps', getTrustedAppProvider], - ['exceptions/event filters', getExceptionProvider], + ['trusted apps', getTrustedAppProviderMock], + ['exceptions/event filters', getExceptionProviderMock], ])('when using the ArtifactEntryCard component with %s', (_, generateItem) => { let item: AnyArtifact; let appTestContext: AppContextTestRender; @@ -48,10 +48,10 @@ describe.each([ 'some internal app' ); expect(renderResult.getByTestId('testCard-subHeader-touchedBy-createdBy').textContent).toEqual( - 'Created byJJusta' + 'Created byMMarty' ); expect(renderResult.getByTestId('testCard-subHeader-touchedBy-updatedBy').textContent).toEqual( - 'Updated byMMara' + 'Updated byEEllamae' ); }); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.test.tsx index 1178e4b07e5bd..508dc3103ca12 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card_minified.test.tsx @@ -13,11 +13,11 @@ import { } from './artifact_entry_card_minified'; import { act, fireEvent } from '@testing-library/react'; import { AnyArtifact } from './types'; -import { getTrustedAppProvider, getExceptionProvider } from './test_utils'; +import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils'; describe.each([ - ['trusted apps', getTrustedAppProvider], - ['exceptions/event filters', getExceptionProvider], + ['trusted apps', getTrustedAppProviderMock], + ['exceptions/event filters', getExceptionProviderMock], ])('when using the ArtifactEntryCardMinified component with %s', (_, generateItem) => { let item: AnyArtifact; let appTestContext: AppContextTestRender; diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.test.tsx new file mode 100644 index 0000000000000..84860b1144f08 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_collapsible_card.test.tsx @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { act, fireEvent } from '@testing-library/react'; +import { AnyArtifact } from './types'; +import { getTrustedAppProviderMock, getExceptionProviderMock } from './test_utils'; +import { + ArtifactEntryCollapsibleCard, + ArtifactEntryCollapsibleCardProps, +} from './artifact_entry_collapsible_card'; + +describe.each([ + ['trusted apps', getTrustedAppProviderMock], + ['exceptions/event filters', getExceptionProviderMock], +])('when using the ArtifactEntryCard component with %s', (_, generateItem) => { + let item: AnyArtifact; + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: ( + props?: Partial + ) => ReturnType; + let handleOnExpandCollapse: jest.MockedFunction< + ArtifactEntryCollapsibleCardProps['onExpandCollapse'] + >; + + beforeEach(() => { + item = generateItem(); + appTestContext = createAppRootMockRenderer(); + handleOnExpandCollapse = jest.fn(); + render = (props = {}) => { + const cardProps: ArtifactEntryCollapsibleCardProps = { + item, + onExpandCollapse: handleOnExpandCollapse, + 'data-test-subj': 'testCard', + ...props, + }; + + renderResult = appTestContext.render(); + return renderResult; + }; + }); + + it.each([ + ['expandCollapse button', 'testCard-header-expandCollapse'], + ['name', 'testCard-header-titleHolder'], + ['description', 'testCard-header-descriptionHolder'], + ['assignment', 'testCard-header-effectScope'], + ])('should show %s', (__, testSubjId) => { + render(); + + expect(renderResult.getByTestId(testSubjId)).not.toBeNull(); + }); + + it('should NOT show actions menu if none are defined', async () => { + render(); + + expect(renderResult.queryByTestId('testCard-header-actions')).toBeNull(); + }); + + it('should render card collapsed', () => { + render(); + + expect(renderResult.queryByTestId('testCard-header-criteriaConditions')).toBeNull(); + }); + + it('should render card expanded', () => { + render({ expanded: true }); + + expect(renderResult.getByTestId('testCard-criteriaConditions')).not.toBeNull(); + }); + + it('should call `onExpandCollapse` callback when button is clicked', () => { + render(); + act(() => { + fireEvent.click(renderResult.getByTestId('testCard-header-expandCollapse')); + }); + + expect(handleOnExpandCollapse).toHaveBeenCalled(); + }); + + it.each([ + ['title', 'testCard-header-titleHolder'], + ['description', 'testCard-header-descriptionHolder'], + ])('should truncate %s text when collapsed', (__, testSubjId) => { + render(); + + expect(renderResult.getByTestId(testSubjId).classList.contains('eui-textTruncate')).toBe(true); + }); + + it.each([ + ['title', 'testCard-header-titleHolder'], + ['description', 'testCard-header-descriptionHolder'], + ])('should NOT truncate %s text when expanded', (__, testSubjId) => { + render({ expanded: true }); + + expect(renderResult.getByTestId(testSubjId).classList.contains('eui-textTruncate')).toBe(false); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx index 6141437779d7d..a4928ffe40674 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/components/card_compressed_header.tsx @@ -38,7 +38,6 @@ export const CardCompressedHeader = memo( 'data-test-subj': dataTestSubj, }) => { const getTestId = useTestIdGenerator(dataTestSubj); - const cssClassNames = useCollapsedCssClassNames(expanded); const policyNavLinks = usePolicyNavLinks(artifact, policies); const handleExpandCollapseClick = useCallback(() => { @@ -46,37 +45,31 @@ export const CardCompressedHeader = memo( }, [onExpandCollapse]); return ( - - + - - - - - - {artifact.name} - - - - - {artifact.description || getEmptyValue()} - - - - - - - - - + } + name={ + + {artifact.name} + + } + description={ + + {artifact.description || getEmptyValue()} + + } + effectScope={ + + } + actionMenu={} + /> ); } ); @@ -106,8 +99,11 @@ export interface CardCompressedHeaderLayoutProps extends Pick( data-test-subj={dataTestSubj} className={flushTopCssClassname} > - + {expandToggle} @@ -145,27 +145,27 @@ export const CardCompressedHeaderLayout = memo( {name} {description} {effectScope} - {actionMenu === false ? ( + {actionMenu === true ? ( { +const getCommonItemDataOverrides = () => { return { name: 'some internal app', description: 'this app is trusted by the company', @@ -17,18 +19,26 @@ export const getCommonItemDataOverrides = () => { }; }; -export const getTrustedAppProvider = () => +export const getTrustedAppProviderMock = (): TrustedApp => new TrustedAppGenerator('seed').generate(getCommonItemDataOverrides()); -export const getExceptionProvider = () => { +export const getExceptionProviderMock = (): ExceptionListItemSchema => { + // Grab the properties from the generated Trusted App that should be the same across both types + // eslint-disable-next-line @typescript-eslint/naming-convention + const { name, description, created_at, updated_at, updated_by, created_by, id } = + getTrustedAppProviderMock(); + // cloneDeep needed because exception mock generator uses state across instances return cloneDeep( getExceptionListItemSchemaMock({ - ...getCommonItemDataOverrides(), + name, + description, + created_at, + updated_at, + updated_by, + created_by, + id, os_types: ['windows'], - updated_at: new Date().toISOString(), - created_by: 'Justa', - updated_by: 'Mara', entries: [ { field: 'process.hash.*', diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx index fd087f267a9b5..b955d9fe71db7 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_item_nav_by_router.tsx @@ -9,6 +9,7 @@ import React, { memo } from 'react'; import { EuiContextMenuItem, EuiContextMenuItemProps } from '@elastic/eui'; import { NavigateToAppOptions } from 'kibana/public'; import { useNavigateToAppEventHandler } from '../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; +import { useTestIdGenerator } from '../hooks/use_test_id_generator'; export interface ContextMenuItemNavByRouterProps extends EuiContextMenuItemProps { /** The Kibana (plugin) app id */ @@ -34,6 +35,7 @@ export const ContextMenuItemNavByRouter = memo( ...navigateOptions, onClick, }); + const getTestId = useTestIdGenerator(otherMenuItemProps['data-test-subj']); return ( ( {textTruncate ? (
{ + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: ( + props?: Partial + ) => ReturnType; + let items: ContextMenuWithRouterSupportProps['items']; + + const clickMenuTriggerButton = () => { + act(() => { + fireEvent.click(renderResult.getByTestId('testMenu-triggerButton')); + }); + }; + + const getContextMenuPanel = () => renderResult.queryByTestId('testMenu-popoverPanel'); + + beforeEach(() => { + appTestContext = createAppRootMockRenderer(); + + items = [ + { + children: 'click me 1', + 'data-test-subj': 'menu-item-one', + textTruncate: false, + }, + { + children: 'click me 2', + navigateAppId: APP_ID, + navigateOptions: { + path: '/one/two/three', + }, + href: 'http://some-url.elastic/one/two/three', + }, + { + children: 'click me 3 with some very long text here that needs to be truncated', + textTruncate: true, + }, + ]; + + render = (overrideProps = {}) => { + const props: ContextMenuWithRouterSupportProps = { + items, + 'data-test-subj': 'testMenu', + button: {'Menu'}, + ...overrideProps, + }; + + renderResult = appTestContext.render(); + + return renderResult; + }; + }); + + it('should toggle the context menu when button is clicked', () => { + render(); + + expect(getContextMenuPanel()).toBeNull(); + + clickMenuTriggerButton(); + + expect(getContextMenuPanel()).not.toBeNull(); + }); + + it('should auto include test subjects on items if one is not defined by the menu item props', () => { + render(); + clickMenuTriggerButton(); + + // this test id should be unchanged from what the Props for the item + expect(renderResult.getByTestId('menu-item-one')).not.toBeNull(); + + // these should have been auto-inserted + expect(renderResult.getByTestId('testMenu-item-1')).not.toBeNull(); + expect(renderResult.getByTestId('testMenu-item-2')).not.toBeNull(); + }); + + it('should truncate text of menu item when `textTruncate` prop is `true`', () => { + render({ maxWidth: undefined }); + clickMenuTriggerButton(); + + expect(renderResult.getByTestId('testMenu-item-2-truncateWrapper')).not.toBeNull(); + }); + + it('should close menu when a menu item is clicked and call menu item onclick callback', async () => { + render(); + clickMenuTriggerButton(); + await act(async () => { + const menuPanelRemoval = waitForElementToBeRemoved(getContextMenuPanel()); + fireEvent.click(renderResult.getByTestId('menu-item-one')); + await menuPanelRemoval; + }); + + expect(getContextMenuPanel()).toBeNull(); + }); + + it('should truncate menu and menu item content when `maxWidth` is used', () => { + render(); + clickMenuTriggerButton(); + + expect(renderResult.getByTestId('menu-item-one-truncateWrapper')).not.toBeNull(); + expect(renderResult.getByTestId('testMenu-item-1-truncateWrapper')).not.toBeNull(); + expect(renderResult.getByTestId('testMenu-item-2-truncateWrapper')).not.toBeNull(); + }); + + it('should navigate using the router when item is clicked', () => { + render(); + clickMenuTriggerButton(); + act(() => { + fireEvent.click(renderResult.getByTestId('testMenu-item-1')); + }); + + expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith( + APP_ID, + expect.objectContaining({ path: '/one/two/three' }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx index 3f21f3995ac5b..41abb0309a7d1 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/context_menu_with_router_support.tsx @@ -13,6 +13,7 @@ import { EuiPopover, EuiPopoverProps, } from '@elastic/eui'; +import uuid from 'uuid'; import { ContextMenuItemNavByRouter, ContextMenuItemNavByRouterProps, @@ -49,10 +50,12 @@ export const ContextMenuWithRouterSupport = memo { - return items.map((itemProps) => { + return items.map((itemProps, index) => { return ( { handleCloseMenu(); @@ -63,7 +66,7 @@ export const ContextMenuWithRouterSupport = memo ); }); - }, [handleCloseMenu, items, maxWidth]); + }, [getTestId, handleCloseMenu, items, maxWidth]); type AdditionalPanelProps = Partial>; const additionalContextMenuPanelProps = useMemo(() => { @@ -86,7 +89,11 @@ export const ContextMenuWithRouterSupport = memo +
{button}
} diff --git a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/index.ts b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/index.ts index 56c6009ccf1b2..527183ef40697 100644 --- a/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/index.ts +++ b/x-pack/plugins/security_solution/public/management/components/context_menu_with_router_support/index.ts @@ -6,3 +6,4 @@ */ export * from './context_menu_with_router_support'; +export * from './context_menu_item_nav_by_router'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx index 30397fe1d32f2..70f5bca339c82 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/index.tsx @@ -24,3 +24,5 @@ export const EndpointsContainer = memo(() => { }); EndpointsContainer.displayName = 'EndpointsContainer'; +export { endpointListFleetApisHttpMock } from './mocks'; +export { EndpointListFleetApisHttpMockInterface } from './mocks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 010fe48f29418..e0b5837c2f78a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -26,19 +26,21 @@ import { HOST_METADATA_LIST_ROUTE, } from '../../../../common/endpoint/constants'; import { - AGENT_POLICY_API_ROUTES, - appRoutesService, - CheckPermissionsResponse, - EPM_API_ROUTES, - GetAgentPoliciesResponse, - GetPackagesResponse, -} from '../../../../../fleet/common'; -import { - PendingActionsHttpMockInterface, pendingActionsHttpMock, + PendingActionsHttpMockInterface, } from '../../../common/lib/endpoint_pending_actions/mocks'; import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants'; import { TransformStatsResponse } from './types'; +import { + fleetGetAgentPolicyListHttpMock, + FleetGetAgentPolicyListHttpMockInterface, + FleetGetAgentStatusHttpMockInterface, + fleetGetCheckPermissionsHttpMock, + FleetGetCheckPermissionsInterface, + FleetGetEndpointPackagePolicyHttpMockInterface, + fleetGetPackageListHttpMock, + FleetGetPackageListHttpMockInterface, +} from '../mocks'; type EndpointMetadataHttpMocksInterface = ResponseProvidersInterface<{ metadataList: () => HostResultList; @@ -149,88 +151,6 @@ export const endpointActivityLogHttpMock = }, ]); -export type FleetGetPackageListHttpMockInterface = ResponseProvidersInterface<{ - packageList: () => GetPackagesResponse; -}>; -export const fleetGetPackageListHttpMock = - httpHandlerMockFactory([ - { - id: 'packageList', - method: 'get', - path: EPM_API_ROUTES.LIST_PATTERN, - handler() { - const generator = new EndpointDocGenerator('seed'); - - return { - response: [generator.generateEpmPackage()], - }; - }, - }, - ]); - -export type FleetGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{ - agentPolicy: () => GetAgentPoliciesResponse; -}>; -export const fleetGetAgentPolicyListHttpMock = - httpHandlerMockFactory([ - { - id: 'agentPolicy', - path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, - method: 'get', - handler: () => { - const generator = new EndpointDocGenerator('seed'); - const endpointMetadata = generator.generateHostMetadata(); - const agentPolicy = generator.generateAgentPolicy(); - - // Make sure that the Agent policy returned from the API has the Integration Policy ID that - // the endpoint metadata is using. This is needed especially when testing the Endpoint Details - // flyout where certain actions might be disabled if we know the endpoint integration policy no - // longer exists. - (agentPolicy.package_policies as string[]).push( - endpointMetadata.Endpoint.policy.applied.id - ); - - return { - items: [agentPolicy], - perPage: 10, - total: 1, - page: 1, - }; - }, - }, - ]); - -export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{ - checkPermissions: () => CheckPermissionsResponse; -}>; - -export const fleetGetCheckPermissionsHttpMock = - httpHandlerMockFactory([ - { - id: 'checkPermissions', - path: appRoutesService.getCheckPermissionsPath(), - method: 'get', - handler: () => { - return { - error: undefined, - success: true, - }; - }, - }, - ]); - -type FleetApisHttpMockInterface = FleetGetPackageListHttpMockInterface & - FleetGetAgentPolicyListHttpMockInterface & - FleetGetCheckPermissionsInterface; -/** - * Mocks all Fleet apis needed to render the Endpoint List/Details pages - */ -export const fleetApisHttpMock = composeHttpHandlerMocks([ - fleetGetPackageListHttpMock, - fleetGetAgentPolicyListHttpMock, - fleetGetCheckPermissionsHttpMock, -]); - type TransformHttpMocksInterface = ResponseProvidersInterface<{ metadataTransformStats: () => TransformStatsResponse; }>; @@ -251,10 +171,24 @@ export const transformsHttpMocks = httpHandlerMockFactory([ + fleetGetPackageListHttpMock, + fleetGetAgentPolicyListHttpMock, + fleetGetCheckPermissionsHttpMock, + ]); type EndpointPageHttpMockInterface = EndpointMetadataHttpMocksInterface & EndpointPolicyResponseHttpMockInterface & EndpointActivityLogHttpMockInterface & - FleetApisHttpMockInterface & + EndpointListFleetApisHttpMockInterface & PendingActionsHttpMockInterface & TransformHttpMocksInterface; /** @@ -264,7 +198,7 @@ export const endpointPageHttpMock = composeHttpHandlerMocks GetPackagesResponse; +}>; +export const fleetGetPackageListHttpMock = + httpHandlerMockFactory([ + { + id: 'packageList', + method: 'get', + path: EPM_API_ROUTES.LIST_PATTERN, + handler() { + const generator = new EndpointDocGenerator('seed'); + + return { + response: [generator.generateEpmPackage()], + }; + }, + }, + ]); + +export type FleetGetEndpointPackagePolicyHttpMockInterface = ResponseProvidersInterface<{ + endpointPackagePolicy: () => GetPolicyResponse; +}>; +export const fleetGetEndpointPackagePolicyHttpMock = + httpHandlerMockFactory([ + { + id: 'endpointPackagePolicy', + path: PACKAGE_POLICY_API_ROUTES.INFO_PATTERN, + method: 'get', + handler: () => { + return { + items: new EndpointDocGenerator('seed').generatePolicyPackagePolicy(), + }; + }, + }, + ]); + +export type FleetGetEndpointPackagePolicyListHttpMockInterface = ResponseProvidersInterface<{ + endpointPackagePolicyList: () => GetPolicyListResponse; +}>; +export const fleetGetEndpointPackagePolicyListHttpMock = + httpHandlerMockFactory([ + { + id: 'endpointPackagePolicyList', + path: PACKAGE_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: () => { + const generator = new EndpointDocGenerator('seed'); + + const items = Array.from({ length: 5 }, (_, index) => { + const policy = generator.generatePolicyPackagePolicy(); + policy.name += ` ${index}`; + return policy; + }); + + return { + items, + total: 1, + page: 1, + perPage: 10, + }; + }, + }, + ]); + +export type FleetGetAgentPolicyListHttpMockInterface = ResponseProvidersInterface<{ + agentPolicy: () => GetAgentPoliciesResponse; +}>; +export const fleetGetAgentPolicyListHttpMock = + httpHandlerMockFactory([ + { + id: 'agentPolicy', + path: AGENT_POLICY_API_ROUTES.LIST_PATTERN, + method: 'get', + handler: () => { + const generator = new EndpointDocGenerator('seed'); + const endpointMetadata = generator.generateHostMetadata(); + const agentPolicy = generator.generateAgentPolicy(); + + // Make sure that the Agent policy returned from the API has the Integration Policy ID that + // the endpoint metadata is using. This is needed especially when testing the Endpoint Details + // flyout where certain actions might be disabled if we know the endpoint integration policy no + // longer exists. + (agentPolicy.package_policies as string[]).push( + endpointMetadata.Endpoint.policy.applied.id + ); + + return { + items: [agentPolicy], + perPage: 10, + total: 1, + page: 1, + }; + }, + }, + ]); + +export type FleetGetCheckPermissionsInterface = ResponseProvidersInterface<{ + checkPermissions: () => CheckPermissionsResponse; +}>; +export const fleetGetCheckPermissionsHttpMock = + httpHandlerMockFactory([ + { + id: 'checkPermissions', + path: appRoutesService.getCheckPermissionsPath(), + method: 'get', + handler: () => { + return { + error: undefined, + success: true, + }; + }, + }, + ]); + +export type FleetGetAgentStatusHttpMockInterface = ResponseProvidersInterface<{ + agentStatus: () => GetAgentStatusResponse; +}>; +export const fleetGetAgentStatusHttpMock = + httpHandlerMockFactory([ + { + id: 'agentStatus', + path: AGENT_API_ROUTES.STATUS_PATTERN, + method: 'get', + handler: () => { + return { + results: { + total: 50, + inactive: 5, + online: 40, + error: 0, + offline: 5, + updating: 0, + other: 0, + events: 0, + }, + }; + }, + }, + ]); diff --git a/x-pack/plugins/security_solution/public/management/pages/mocks/index.ts b/x-pack/plugins/security_solution/public/management/pages/mocks/index.ts new file mode 100644 index 0000000000000..c7388cad5696f --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/mocks/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './fleet_mocks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts index 6839edb965332..0fbd674b265b0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { PolicyDetailsState } from '../../../types'; +import { PolicyArtifactsState, PolicyDetailsState } from '../../../types'; import { initialPolicyDetailsState } from '../reducer'; import { getAssignableArtifactsList, @@ -16,6 +16,12 @@ import { getAssignableArtifactsListExist, getAssignableArtifactsListExistIsLoading, getUpdateArtifacts, + doesPolicyTrustedAppsListNeedUpdate, + isPolicyTrustedAppListLoading, + getPolicyTrustedAppList, + getPolicyTrustedAppsListPagination, + getTrustedAppsListOfAllPolicies, + getTrustedAppsAllPoliciesById, } from './trusted_apps_selectors'; import { getCurrentArtifactsLocation, isOnPolicyTrustedAppsView } from './policy_common_selectors'; @@ -27,15 +33,190 @@ import { createFailedResourceState, } from '../../../../../state'; import { MANAGEMENT_ROUTING_POLICY_DETAILS_TRUSTED_APPS_PATH } from '../../../../../common/constants'; -import { getMockListResponse, getAPIError, getMockCreateResponse } from '../../../test_utils'; +import { + getMockListResponse, + getAPIError, + getMockCreateResponse, + getMockPolicyDetailsArtifactListUrlParams, + getMockPolicyDetailsArtifactsPageLocationUrlParams, +} from '../../../test_utils'; +import { getGeneratedPolicyResponse } from '../../../../trusted_apps/store/mocks'; describe('policy trusted apps selectors', () => { let initialState: ImmutableObject; + const createArtifactsState = ( + artifacts: Partial = {} + ): ImmutableObject => { + return { + ...initialState, + artifacts: { + ...initialState.artifacts, + ...artifacts, + }, + }; + }; + beforeEach(() => { initialState = initialPolicyDetailsState(); }); + describe('doesPolicyTrustedAppsListNeedUpdate()', () => { + it('should return true if state is not loaded', () => { + expect(doesPolicyTrustedAppsListNeedUpdate(initialState)).toBe(true); + }); + + it('should return true if it is loaded, but URL params were changed', () => { + expect( + doesPolicyTrustedAppsListNeedUpdate( + createArtifactsState({ + location: getMockPolicyDetailsArtifactsPageLocationUrlParams({ page_index: 4 }), + assignedList: createLoadedResourceState({ + location: getMockPolicyDetailsArtifactListUrlParams(), + artifacts: getMockListResponse(), + }), + }) + ) + ).toBe(true); + }); + + it('should return false if state is loaded adn URL params are the same', () => { + expect( + doesPolicyTrustedAppsListNeedUpdate( + createArtifactsState({ + location: getMockPolicyDetailsArtifactsPageLocationUrlParams(), + assignedList: createLoadedResourceState({ + location: getMockPolicyDetailsArtifactListUrlParams(), + artifacts: getMockListResponse(), + }), + }) + ) + ).toBe(false); + }); + }); + + describe('isPolicyTrustedAppListLoading()', () => { + it('should return true when loading data', () => { + expect( + isPolicyTrustedAppListLoading( + createArtifactsState({ + assignedList: createLoadingResourceState(createUninitialisedResourceState()), + }) + ) + ).toBe(true); + }); + + it.each([ + ['uninitialized', createUninitialisedResourceState() as PolicyArtifactsState['assignedList']], + ['loaded', createLoadedResourceState({}) as PolicyArtifactsState['assignedList']], + ['failed', createFailedResourceState({}) as PolicyArtifactsState['assignedList']], + ])('should return false when state is %s', (__, assignedListState) => { + expect( + isPolicyTrustedAppListLoading(createArtifactsState({ assignedList: assignedListState })) + ).toBe(false); + }); + }); + + describe('getPolicyTrustedAppList()', () => { + it('should return the list of trusted apps', () => { + const listResponse = getMockListResponse(); + + expect( + getPolicyTrustedAppList( + createArtifactsState({ + location: getMockPolicyDetailsArtifactsPageLocationUrlParams(), + assignedList: createLoadedResourceState({ + location: getMockPolicyDetailsArtifactListUrlParams(), + artifacts: listResponse, + }), + }) + ) + ).toEqual(listResponse.data); + }); + + it('should return empty array if no data is loaded', () => { + expect(getPolicyTrustedAppList(initialState)).toEqual([]); + }); + }); + + describe('getPolicyTrustedAppsListPagination()', () => { + it('should return default pagination data even if no api data is available', () => { + expect(getPolicyTrustedAppsListPagination(initialState)).toEqual({ + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [10, 20, 50], + totalItemCount: 0, + }); + }); + + it('should return pagination data based on api response data', () => { + const listResponse = getMockListResponse(); + + listResponse.page = 6; + listResponse.per_page = 100; + listResponse.total = 1000; + + expect( + getPolicyTrustedAppsListPagination( + createArtifactsState({ + location: getMockPolicyDetailsArtifactsPageLocationUrlParams({ + page_index: 5, + page_size: 100, + }), + assignedList: createLoadedResourceState({ + location: getMockPolicyDetailsArtifactListUrlParams({ + page_index: 5, + page_size: 100, + }), + artifacts: listResponse, + }), + }) + ) + ).toEqual({ + pageIndex: 5, + pageSize: 100, + pageSizeOptions: [10, 20, 50], + totalItemCount: 1000, + }); + }); + }); + + describe('getTrustedAppsListOfAllPolicies()', () => { + it('should return the loaded list of policies', () => { + const policiesApiResponse = getGeneratedPolicyResponse(); + + expect( + getTrustedAppsListOfAllPolicies( + createArtifactsState({ + policies: createLoadedResourceState(policiesApiResponse), + }) + ) + ).toEqual(policiesApiResponse.items); + }); + + it('should return an empty array of no policy data was loaded yet', () => { + expect(getTrustedAppsListOfAllPolicies(initialState)).toEqual([]); + }); + }); + + describe('getTrustedAppsAllPoliciesById()', () => { + it('should return an empty object if no polices', () => { + expect(getTrustedAppsAllPoliciesById(initialState)).toEqual({}); + }); + + it('should return an object with policy id and policy data', () => { + const policiesApiResponse = getGeneratedPolicyResponse(); + + expect( + getTrustedAppsAllPoliciesById( + createArtifactsState({ + policies: createLoadedResourceState(policiesApiResponse), + }) + ) + ).toEqual({ [policiesApiResponse.items[0].id]: policiesApiResponse.items[0] }); + }); + }); + describe('isOnPolicyTrustedAppsPage()', () => { it('when location is on policy trusted apps page', () => { const isOnPage = isOnPolicyTrustedAppsView({ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts index 84f0f4a2c63b8..3177f13ae77e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/trusted_apps_selectors.ts @@ -31,6 +31,7 @@ import { LoadedResourceState, } from '../../../../../state'; import { getCurrentArtifactsLocation } from './policy_common_selectors'; +import { ServerApiError } from '../../../../../../common/types'; export const doesPolicyHaveTrustedApps = ( state: PolicyDetailsState @@ -212,3 +213,11 @@ export const getDoesAnyTrustedAppExistsIsLoading: PolicyDetailsSelector return isLoadingResourceState(doesAnyTrustedAppExists); } ); + +export const getPolicyTrustedAppListError: PolicyDetailsSelector< + Immutable | undefined +> = createSelector(getCurrentPolicyAssignedTrustedAppsState, (currentAssignedTrustedAppsState) => { + if (isFailedResourceState(currentAssignedTrustedAppsState)) { + return currentAssignedTrustedAppsState.error; + } +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts index d92c41f5a1cc6..599c6328855e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/index.ts @@ -5,25 +5,4 @@ * 2.0. */ -import { - GetTrustedAppsListResponse, - PostTrustedAppCreateResponse, -} from '../../../../../common/endpoint/types'; - -import { createSampleTrustedApps, createSampleTrustedApp } from '../../trusted_apps/test_utils'; - -export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({ - data: createSampleTrustedApps({}), - per_page: 100, - page: 1, - total: 100, -}); - -export const getMockCreateResponse: () => PostTrustedAppCreateResponse = () => - createSampleTrustedApp(1) as unknown as unknown as PostTrustedAppCreateResponse; - -export const getAPIError = () => ({ - statusCode: 500, - error: 'Internal Server Error', - message: 'Something is not right', -}); +export * from './mocks'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts new file mode 100644 index 0000000000000..be38e591dd9da --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/test_utils/mocks.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + composeHttpHandlerMocks, + httpHandlerMockFactory, + ResponseProvidersInterface, +} from '../../../../common/mock/endpoint/http_handler_mock_factory'; +import { + GetTrustedAppsListRequest, + GetTrustedAppsListResponse, + PostTrustedAppCreateResponse, +} from '../../../../../common/endpoint/types'; +import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants'; +import { TrustedAppGenerator } from '../../../../../common/endpoint/data_generators/trusted_app_generator'; +import { createSampleTrustedApps, createSampleTrustedApp } from '../../trusted_apps/test_utils'; +import { + PolicyDetailsArtifactsPageListLocationParams, + PolicyDetailsArtifactsPageLocation, +} from '../types'; +import { + fleetGetAgentStatusHttpMock, + FleetGetAgentStatusHttpMockInterface, + fleetGetEndpointPackagePolicyHttpMock, + FleetGetEndpointPackagePolicyHttpMockInterface, + fleetGetEndpointPackagePolicyListHttpMock, + FleetGetEndpointPackagePolicyListHttpMockInterface, +} from '../../mocks'; + +export const getMockListResponse: () => GetTrustedAppsListResponse = () => ({ + data: createSampleTrustedApps({}), + per_page: 100, + page: 1, + total: 100, +}); + +export const getMockPolicyDetailsArtifactsPageLocationUrlParams = ( + overrides: Partial = {} +): PolicyDetailsArtifactsPageLocation => { + return { + page_index: 0, + page_size: 10, + filter: '', + show: undefined, + ...overrides, + }; +}; + +export const getMockPolicyDetailsArtifactListUrlParams = ( + overrides: Partial = {} +): PolicyDetailsArtifactsPageListLocationParams => { + return { + page_index: 0, + page_size: 10, + filter: '', + ...overrides, + }; +}; + +export const getMockCreateResponse: () => PostTrustedAppCreateResponse = () => + createSampleTrustedApp(1) as unknown as unknown as PostTrustedAppCreateResponse; + +export const getAPIError = () => ({ + statusCode: 500, + error: 'Internal Server Error', + message: 'Something is not right', +}); + +type PolicyDetailsTrustedAppsHttpMocksInterface = ResponseProvidersInterface<{ + policyTrustedAppsList: () => GetTrustedAppsListResponse; +}>; + +/** + * HTTP mocks that support the Trusted Apps tab of the Policy Details page + */ +export const policyDetailsTrustedAppsHttpMocks = + httpHandlerMockFactory([ + { + id: 'policyTrustedAppsList', + path: TRUSTED_APPS_LIST_API, + method: 'get', + handler: ({ query }): GetTrustedAppsListResponse => { + const apiQueryParams = query as GetTrustedAppsListRequest; + const generator = new TrustedAppGenerator('seed'); + const perPage = apiQueryParams.per_page ?? 10; + const data = Array.from({ length: Math.min(perPage, 50) }, () => generator.generate()); + + // Change the 3rd entry (index 2) to be policy specific + data[2].effectScope = { + type: 'policy', + policies: [ + // IDs below are those generated by the `fleetGetEndpointPackagePolicyListHttpMock()` mock + 'ddf6570b-9175-4a6d-b288-61a09771c647', + 'b8e616ae-44fc-4be7-846c-ce8fa5c082dd', + ], + }; + + return { + page: apiQueryParams.page ?? 1, + per_page: perPage, + total: 20, + data, + }; + }, + }, + ]); + +export type PolicyDetailsPageAllApiHttpMocksInterface = + FleetGetEndpointPackagePolicyHttpMockInterface & + FleetGetAgentStatusHttpMockInterface & + FleetGetEndpointPackagePolicyListHttpMockInterface & + PolicyDetailsTrustedAppsHttpMocksInterface; +export const policyDetailsPageAllApiHttpMocks = + composeHttpHandlerMocks([ + fleetGetEndpointPackagePolicyHttpMock, + fleetGetAgentStatusHttpMock, + fleetGetEndpointPackagePolicyListHttpMock, + policyDetailsTrustedAppsHttpMocks, + ]); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx new file mode 100644 index 0000000000000..07b62d13e8edc --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -0,0 +1,253 @@ +/* + * 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 { + AppContextTestRender, + createAppRootMockRenderer, +} from '../../../../../../common/mock/endpoint'; +import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; +import { PolicyTrustedAppsList } from './policy_trusted_apps_list'; +import React from 'react'; +import { policyDetailsPageAllApiHttpMocks } from '../../../test_utils'; +import { isFailedResourceState, isLoadedResourceState } from '../../../../../state'; +import { fireEvent, within, act, waitFor } from '@testing-library/react'; +import { APP_ID } from '../../../../../../../common/constants'; + +describe('when rendering the PolicyTrustedAppsList', () => { + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: (waitForLoadedState?: boolean) => Promise>; + let mockedApis: ReturnType; + let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + + const getCardByIndexPosition = (cardIndex: number = 0) => { + const card = renderResult.getAllByTestId('policyTrustedAppsGrid-card')[cardIndex]; + + if (!card) { + throw new Error(`Card at index [${cardIndex}] not found`); + } + + return card; + }; + + const toggleCardExpandCollapse = (cardIndex: number = 0) => { + act(() => { + fireEvent.click( + within(getCardByIndexPosition(cardIndex)).getByTestId( + 'policyTrustedAppsGrid-card-header-expandCollapse' + ) + ); + }); + }; + + const toggleCardActionMenu = async (cardIndex: number = 0) => { + act(() => { + fireEvent.click( + within(getCardByIndexPosition(cardIndex)).getByTestId( + 'policyTrustedAppsGrid-card-header-actions-button' + ) + ); + }); + + await waitFor(() => + expect(renderResult.getByTestId('policyTrustedAppsGrid-card-header-actions-contextMenuPanel')) + ); + }; + + beforeEach(() => { + appTestContext = createAppRootMockRenderer(); + + mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); + appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true }); + waitForAction = appTestContext.middlewareSpy.waitForAction; + + render = async (waitForLoadedState: boolean = true) => { + appTestContext.history.push( + getPolicyDetailsArtifactsListPath('ddf6570b-9175-4a6d-b288-61a09771c647') + ); + const trustedAppDataReceived = waitForLoadedState + ? waitForAction('assignedTrustedAppsListStateChanged', { + validate({ payload }) { + return isLoadedResourceState(payload); + }, + }) + : Promise.resolve(); + + renderResult = appTestContext.render(); + await trustedAppDataReceived; + + return renderResult; + }; + }); + + // FIXME: implement this test once PR #113802 is merged + it.todo('should show loading spinner if checking to see if trusted apps exist'); + + it('should show total number of of items being displayed', async () => { + await render(); + + expect(renderResult.getByTestId('policyDetailsTrustedAppsCount').textContent).toBe( + 'Showing 20 trusted applications' + ); + }); + + it('should show card grid', async () => { + await render(); + + expect(renderResult.getByTestId('policyTrustedAppsGrid')).toBeTruthy(); + await expect(renderResult.findAllByTestId('policyTrustedAppsGrid-card')).resolves.toHaveLength( + 10 + ); + }); + + it('should expand cards', async () => { + await render(); + // expand + toggleCardExpandCollapse(); + toggleCardExpandCollapse(4); + + await waitFor(() => + expect( + renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') + ).toHaveLength(2) + ); + }); + + it('should collapse cards', async () => { + await render(); + + // expand + toggleCardExpandCollapse(); + toggleCardExpandCollapse(4); + + await waitFor(() => + expect( + renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') + ).toHaveLength(2) + ); + + // collapse + toggleCardExpandCollapse(); + toggleCardExpandCollapse(4); + + await waitFor(() => + expect( + renderResult.queryAllByTestId('policyTrustedAppsGrid-card-criteriaConditions') + ).toHaveLength(0) + ); + }); + + it('should show action menu on card', async () => { + await render(); + expect( + renderResult.getAllByTestId('policyTrustedAppsGrid-card-header-actions-button') + ).toHaveLength(10); + }); + + it('should navigate to trusted apps page when view full details action is clicked', async () => { + await render(); + await toggleCardActionMenu(); + act(() => { + fireEvent.click(renderResult.getByTestId('policyTrustedAppsGrid-viewFullDetailsAction')); + }); + + expect(appTestContext.coreStart.application.navigateToApp).toHaveBeenCalledWith( + APP_ID, + expect.objectContaining({ + path: '/administration/trusted_apps?show=edit&id=89f72d8a-05b5-4350-8cad-0dc3661d6e67', + }) + ); + }); + + it('should display policy names on assignment context menu', async () => { + const retrieveAllPolicies = waitForAction('policyDetailsListOfAllPoliciesStateChanged', { + validate({ payload }) { + return isLoadedResourceState(payload); + }, + }); + await render(); + await retrieveAllPolicies; + act(() => { + fireEvent.click( + within(getCardByIndexPosition(2)).getByTestId( + 'policyTrustedAppsGrid-card-header-effectScope-popupMenu-button' + ) + ); + }); + await waitFor(() => + expect( + renderResult.getByTestId( + 'policyTrustedAppsGrid-card-header-effectScope-popupMenu-popoverPanel' + ) + ) + ); + + expect( + renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-0') + .textContent + ).toEqual('Endpoint Policy 0'); + expect( + renderResult.getByTestId('policyTrustedAppsGrid-card-header-effectScope-popupMenu-item-1') + .textContent + ).toEqual('Endpoint Policy 1'); + }); + + it.todo('should navigate to policy details when clicking policy on assignment context menu'); + + it('should handle pagination changes', async () => { + await render(); + + expect(appTestContext.history.location.search).not.toBeTruthy(); + + act(() => { + fireEvent.click(renderResult.getByTestId('pagination-button-next')); + }); + + expect(appTestContext.history.location.search).toMatch('?page_index=1'); + }); + + it('should reset `pageIndex` when a new pageSize is selected', async () => { + await render(); + // page ahead + act(() => { + fireEvent.click(renderResult.getByTestId('pagination-button-next')); + }); + await waitFor(() => { + expect(appTestContext.history.location.search).toBeTruthy(); + }); + + // now change the page size + await act(async () => { + fireEvent.click(renderResult.getByTestId('tablePaginationPopoverButton')); + await waitFor(() => expect(renderResult.getByTestId('tablePagination-50-rows'))); + }); + act(() => { + fireEvent.click(renderResult.getByTestId('tablePagination-50-rows')); + }); + + expect(appTestContext.history.location.search).toMatch('?page_size=50'); + }); + + it('should show toast message if trusted app list api call fails', async () => { + const error = new Error('oh no'); + // @ts-expect-error + mockedApis.responseProvider.policyTrustedAppsList.mockRejectedValue(error); + await render(false); + await act(async () => { + await waitForAction('assignedTrustedAppsListStateChanged', { + validate: ({ payload }) => isFailedResourceState(payload), + }); + }); + + expect(appTestContext.startServices.notifications.toasts.addError).toHaveBeenCalledWith( + error, + expect.objectContaining({ + title: expect.any(String), + }) + ); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index 333820b5d81b4..6793bee9c3c01 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -19,6 +19,7 @@ import { doesPolicyHaveTrustedApps, getCurrentArtifactsLocation, getPolicyTrustedAppList, + getPolicyTrustedAppListError, getPolicyTrustedAppsListPagination, getTrustedAppsAllPoliciesById, isPolicyTrustedAppListLoading, @@ -31,12 +32,17 @@ import { getTrustedAppsListPath, } from '../../../../../common/routing'; import { Immutable, TrustedApp } from '../../../../../../../common/endpoint/types'; -import { useAppUrl } from '../../../../../../common/lib/kibana'; +import { useAppUrl, useToasts } from '../../../../../../common/lib/kibana'; import { APP_ID } from '../../../../../../../common/constants'; import { ContextMenuItemNavByRouterProps } from '../../../../../components/context_menu_with_router_support/context_menu_item_nav_by_router'; import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; +import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; + +const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; export const PolicyTrustedAppsList = memo(() => { + const getTestId = useTestIdGenerator(DATA_TEST_SUBJ); + const toasts = useToasts(); const history = useHistory(); const { getAppUrl } = useAppUrl(); const policyId = usePolicyDetailsSelector(policyIdFromParams); @@ -47,11 +53,10 @@ export const PolicyTrustedAppsList = memo(() => { const pagination = usePolicyDetailsSelector(getPolicyTrustedAppsListPagination); const urlParams = usePolicyDetailsSelector(getCurrentArtifactsLocation); const allPoliciesById = usePolicyDetailsSelector(getTrustedAppsAllPoliciesById); + const trustedAppsApiError = usePolicyDetailsSelector(getPolicyTrustedAppListError); const [isCardExpanded, setCardExpanded] = useState>({}); - // TODO:PT show load errors if any - const handlePageChange = useCallback( ({ pageIndex, pageSize }) => { history.push( @@ -135,6 +140,7 @@ export const PolicyTrustedAppsList = memo(() => { href: getAppUrl({ appId: APP_ID, path: viewUrlPath }), navigateAppId: APP_ID, navigateOptions: { path: viewUrlPath }, + 'data-test-subj': getTestId('viewFullDetailsAction'), }, ], policies: assignedPoliciesMenuItems, @@ -144,7 +150,7 @@ export const PolicyTrustedAppsList = memo(() => { } return newCardProps; - }, [allPoliciesById, getAppUrl, isCardExpanded, trustedAppItems]); + }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems]); const provideCardProps = useCallback['cardComponentProps']>( (item) => { @@ -153,6 +159,17 @@ export const PolicyTrustedAppsList = memo(() => { [cardProps] ); + // if an error occurred while loading the data, show toast + useEffect(() => { + if (trustedAppsApiError) { + toasts.addError(trustedAppsApiError as unknown as Error, { + title: i18n.translate('xpack.securitySolution.endpoint.policy.trustedApps.list.apiError', { + defaultMessage: 'Error while retrieving list of trusted applications', + }), + }); + } + }, [toasts, trustedAppsApiError]); + // Anytime a new set of data (trusted apps) is retrieved, reset the card expand state useEffect(() => { setCardExpanded({}); @@ -161,7 +178,11 @@ export const PolicyTrustedAppsList = memo(() => { if (hasTrustedApps.loading || isTrustedAppExistsCheckLoading) { return ( - + ); } @@ -180,8 +201,9 @@ export const PolicyTrustedAppsList = memo(() => { onExpandCollapse={handleExpandCollapse} cardComponentProps={provideCardProps} loading={isLoading} + error={trustedAppsApiError?.message} pagination={pagination as Pagination} - data-test-subj="policyTrustedAppsGrid" + data-test-subj={DATA_TEST_SUBJ} /> ); From c9e3e0e9b5c8703f8dd2b8e815cbd5ea5f0f2c4c Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Tue, 12 Oct 2021 08:26:05 -0500 Subject: [PATCH 058/287] Fix GC time calculation (#113992) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Was in µs. Corrected to be in ms. Also use correct duration formatting on the GC time chart and time spent by dependency chart. --- .../shared/charts/breakdown_chart/index.tsx | 12 +- .../shared/charts/metrics_chart/index.tsx | 16 +- .../gc/fetch_and_transform_gc_metrics.test.ts | 133 ++++++++ .../java/gc/fetch_and_transform_gc_metrics.ts | 9 +- .../tests/metrics_charts/metrics_charts.ts | 316 +++++++++--------- 5 files changed, 317 insertions(+), 169 deletions(-) create mode 100644 x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts diff --git a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx index 9dc2fbd4cc961..213bac40c2248 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx @@ -27,8 +27,8 @@ import { Annotation } from '../../../../../common/annotations'; import { useChartTheme } from '../../../../../../observability/public'; import { asAbsoluteDateTime, - asDuration, asPercent, + getDurationFormatter, } from '../../../../../common/utils/formatters'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { useChartPointerEventContext } from '../../../../context/chart_pointer_event/use_chart_pointer_event_context'; @@ -39,6 +39,10 @@ import { ChartContainer } from '../../charts/chart_container'; import { isTimeseriesEmpty, onBrushEnd } from '../../charts/helper/helper'; import { useApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; +import { + getMaxY, + getResponseTimeTickFormatter, +} from '../../../shared/charts/transaction_charts/helper'; interface Props { fetchStatus: FETCH_STATUS; @@ -50,7 +54,6 @@ interface Props { } const asPercentBound = (y: number | null) => asPercent(y, 1); -const asDurationBound = (y: number | null) => asDuration(y); export function BreakdownChart({ fetchStatus, @@ -82,8 +85,11 @@ export function BreakdownChart({ const isEmpty = isTimeseriesEmpty(timeseries); + const maxY = getMaxY(timeseries); const yTickFormat: TickFormatter = - yAxisType === 'duration' ? asDurationBound : asPercentBound; + yAxisType === 'duration' + ? getResponseTimeTickFormatter(getDurationFormatter(maxY)) + : asPercentBound; return ( diff --git a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx index 9ee77cd95ee0d..9f437a95e7dd9 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/metrics_chart/index.tsx @@ -9,9 +9,9 @@ import { EuiTitle } from '@elastic/eui'; import React from 'react'; import { asDecimal, - asDuration, asInteger, asPercent, + getDurationFormatter, getFixedByteFormatter, } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -19,22 +19,24 @@ import { GenericMetricsChart } from '../../../../../server/lib/metrics/transform import { Maybe } from '../../../../../typings/common'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; import { TimeseriesChart } from '../timeseries_chart'; +import { + getMaxY, + getResponseTimeTickFormatter, +} from '../transaction_charts/helper'; function getYTickFormatter(chart: GenericMetricsChart) { + const max = getMaxY(chart.series); + switch (chart.yUnit) { case 'bytes': { - const max = Math.max( - ...chart.series.map(({ data }) => - Math.max(...data.map(({ y }) => y || 0)) - ) - ); return getFixedByteFormatter(max); } case 'percent': { return (y: Maybe) => asPercent(y || 0, 1); } case 'time': { - return asDuration; + const durationFormatter = getDurationFormatter(max); + return getResponseTimeTickFormatter(durationFormatter); } case 'integer': { return asInteger; diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts new file mode 100644 index 0000000000000..c22c326473e2c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.test.ts @@ -0,0 +1,133 @@ +/* + * 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 { + METRIC_JAVA_GC_COUNT, + METRIC_JAVA_GC_TIME, +} from '../../../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../../../helpers/setup_request'; +import { ChartBase } from '../../../types'; + +import { fetchAndTransformGcMetrics } from './fetch_and_transform_gc_metrics'; + +describe('fetchAndTransformGcMetrics', () => { + describe('given "jvm.gc.time"', () => { + it('converts the value to milliseconds', async () => { + const chartBase = {} as unknown as ChartBase; + const response = { + hits: { total: { value: 1 } }, + aggregations: { + per_pool: { + buckets: [ + { + key: 'Copy', + doc_count: 30, + timeseries: { + buckets: [ + { + key_as_string: '2021-10-05T16:03:30.000Z', + key: 1633449810000, + doc_count: 1, + max: { + value: 23750, + }, + derivative: { + value: 11, + }, + value: { + value: 11, + }, + }, + ], + }, + }, + ], + }, + }, + }; + const setup = { + apmEventClient: { search: () => Promise.resolve(response) }, + config: { 'xpack.gc.metricsInterval': 0 }, + } as unknown as Setup; + const fieldName = METRIC_JAVA_GC_TIME; + + const { series } = await fetchAndTransformGcMetrics({ + chartBase, + environment: 'test environment', + fieldName, + kuery: '', + operationName: 'test operation name', + setup, + serviceName: 'test service name', + start: 1633456140000, + end: 1633457078105, + }); + + expect(series[0].data[0].y).toEqual(22000); + }); + }); + + describe('given "jvm.gc.rate"', () => { + it('does not convert the value to milliseconds', async () => { + const chartBase = {} as unknown as ChartBase; + const response = { + hits: { + total: { + value: 62, + }, + }, + aggregations: { + per_pool: { + buckets: [ + { + key: 'Copy', + doc_count: 31, + timeseries: { + buckets: [ + { + key_as_string: '2021-10-05T18:01:30.000Z', + key: 1633456890000, + doc_count: 1, + max: { + value: 815, + }, + derivative: { + value: 4, + }, + value: { + value: 4, + }, + }, + ], + }, + }, + ], + }, + }, + }; + const setup = { + apmEventClient: { search: () => Promise.resolve(response) }, + config: { 'xpack.gc.metricsInterval': 0 }, + } as unknown as Setup; + const fieldName = METRIC_JAVA_GC_COUNT; + + const { series } = await fetchAndTransformGcMetrics({ + chartBase, + environment: 'test environment', + fieldName, + kuery: '', + operationName: 'test operation name', + setup, + serviceName: 'test service name', + start: 1633456140000, + end: 1633457078105, + }); + + expect(series[0].data[0].y).toEqual(8); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 8231e4d3c6faa..ba35836452122 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -135,10 +135,17 @@ export async function fetchAndTransformGcMetrics({ const data = timeseriesData.buckets.map((bucket) => { // derivative/value will be undefined for the first hit and if the `max` value is null const bucketValue = bucket.value?.value; - const y = isFiniteNumber(bucketValue) + + const unconvertedY = isFiniteNumber(bucketValue) ? round(bucketValue * (60 / bucketSize), 1) : null; + // convert to milliseconds if we're calculating time, but not for rate + const y = + unconvertedY !== null && fieldName === METRIC_JAVA_GC_TIME + ? unconvertedY * 1000 + : unconvertedY; + return { y, x: bucket.key, diff --git a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts index 8d3a18a44f02e..7b621de111ef8 100644 --- a/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts +++ b/x-pack/test/apm_api_integration/tests/metrics_charts/metrics_charts.ts @@ -39,11 +39,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('contains CPU usage and System memory usage chart data', async () => { expect(chartsResponse.status).to.be(200); expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - ] - `); + Array [ + "CPU usage", + "System memory usage", + ] + `); }); describe('CPU usage', () => { @@ -57,25 +57,25 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.714, - 0.3877, - 0.75, - 0.2543, - ] - `); + Array [ + 0.714, + 0.3877, + 0.75, + 0.2543, + ] + `); }); }); @@ -91,21 +91,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(systemMemoryUsageChart).to.not.empty(); expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); + Array [ + "Max", + "Average", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.722093920925555, - 0.718173546796348, - ] - `); + Array [ + 0.722093920925555, + 0.718173546796348, + ] + `); }); }); }); @@ -128,16 +128,16 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct chart data', async () => { expect(chartsResponse.status).to.be(200); expectSnapshot(chartsResponse.body.charts.map((chart) => chart.title)).toMatchInline(` - Array [ - "CPU usage", - "System memory usage", - "Heap Memory", - "Non-Heap Memory", - "Thread Count", - "Garbage collection per minute", - "Garbage collection time spent per minute", - ] - `); + Array [ + "CPU usage", + "System memory usage", + "Heap Memory", + "Non-Heap Memory", + "Thread Count", + "Garbage collection per minute", + "Garbage collection time spent per minute", + ] + `); }); describe('CPU usage', () => { @@ -151,37 +151,37 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "System max", - "System average", - "Process max", - "Process average", - ] - `); + Array [ + "System max", + "System average", + "Process max", + "Process average", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.203, - 0.178777777777778, - 0.01, - 0.009, - ] - `); + Array [ + 0.203, + 0.178777777777778, + 0.01, + 0.009, + ] + `); }); it('has the correct rate', async () => { const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 0.193, - 0.193, - 0.009, - 0.009, - ] - `); + Array [ + 0.193, + 0.193, + 0.009, + 0.009, + ] + `); }); }); @@ -197,31 +197,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(systemMemoryUsageChart).to.not.empty(); expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)) .toMatchInline(` - Array [ - "Max", - "Average", - ] - `); + Array [ + "Max", + "Average", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.707924703557837, - 0.705395980841182, - ] - `); + Array [ + 0.707924703557837, + 0.705395980841182, + ] + `); }); it('has the correct rate', async () => { const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 0.707924703557837, - 0.707924703557837, - ] - `); + Array [ + 0.707924703557837, + 0.707924703557837, + ] + `); }); }); @@ -236,34 +236,34 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - "Avg. limit", - ] - `); + Array [ + "Avg. used", + "Avg. committed", + "Avg. limit", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 222501617.777778, - 374341632, - 1560281088, - ] - `); + Array [ + 222501617.777778, + 374341632, + 1560281088, + ] + `); }); it('has the correct rate', async () => { const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 211472896, - 374341632, - 1560281088, - ] - `); + Array [ + 211472896, + 374341632, + 1560281088, + ] + `); }); }); @@ -278,31 +278,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. used", - "Avg. committed", - ] - `); + Array [ + "Avg. used", + "Avg. committed", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 138573397.333333, - 147677639.111111, - ] - `); + Array [ + 138573397.333333, + 147677639.111111, + ] + `); }); it('has the correct rate', async () => { const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 138162752, - 147386368, - ] - `); + Array [ + 138162752, + 147386368, + ] + `); }); }); @@ -317,31 +317,31 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Avg. count", - "Max count", - ] - `); + Array [ + "Avg. count", + "Max count", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 44.4444444444444, - 45, - ] - `); + Array [ + 44.4444444444444, + 45, + ] + `); }); it('has the correct rate', async () => { const yValues = cpuUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 44, - 44, - ] - `); + Array [ + 44, + 44, + ] + `); }); }); @@ -356,21 +356,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0, - 3, - ] - `); + Array [ + 0, + 3, + ] + `); }); }); @@ -385,21 +385,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('has correct series', () => { expect(cpuUsageChart).to.not.empty(); expectSnapshot(cpuUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "G1 Old Generation", - "G1 Young Generation", - ] - `); + Array [ + "G1 Old Generation", + "G1 Young Generation", + ] + `); }); it('has correct series overall values', () => { expectSnapshot(cpuUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0, - 37.5, - ] - `); + Array [ + 0, + 37500, + ] + `); }); }); }); @@ -419,26 +419,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(systemMemoryUsageChart).to.not.empty(); expectSnapshot(systemMemoryUsageChart?.series.map(({ title }) => title)).toMatchInline(` - Array [ - "Max", - "Average", - ] - `); + Array [ + "Max", + "Average", + ] + `); expectSnapshot(systemMemoryUsageChart?.series.map(({ overallValue }) => overallValue)) .toMatchInline(` - Array [ - 0.114523896426499, - 0.114002376090415, - ] - `); + Array [ + 0.114523896426499, + 0.114002376090415, + ] + `); const yValues = systemMemoryUsageChart?.series.map((serie) => first(serie.data)?.y); expectSnapshot(yValues).toMatchInline(` - Array [ - 0.11383724014064, - 0.11383724014064, - ] - `); + Array [ + 0.11383724014064, + 0.11383724014064, + ] + `); }); }); } From a054749c7a49c211e5c09dd64b35f76ead569aa7 Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 12 Oct 2021 09:28:37 -0400 Subject: [PATCH 059/287] [Stack Monitoring] ES Overview fix completed recoveries section (#114179) * fix completed recoveries section * fix type * fix type Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/elasticsearch/overview.tsx | 5 +- .../public/components/elasticsearch/index.ts | 1 + .../elasticsearch/overview/index.ts | 2 + .../elasticsearch/overview/overview_react.js | 66 ++++++++ .../elasticsearch/shard_activity/index.js | 1 + .../shard_activity/parse_props.js | 14 +- .../shard_activity/shard_activity_react.js | 156 ++++++++++++++++++ 7 files changed, 240 insertions(+), 5 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/overview/overview_react.js create mode 100644 x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx index c58aaa5dffb04..d1500d5d9587b 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/overview.tsx @@ -10,7 +10,8 @@ import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; -import { ElasticsearchOverview } from '../../../components/elasticsearch'; +// @ts-ignore +import { ElasticsearchOverviewReact } from '../../../components/elasticsearch'; import { ComponentProps } from '../../route_init'; import { useCharts } from '../../hooks/use_charts'; import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; @@ -78,7 +79,7 @@ export const ElasticsearchOverviewPage: React.FC = ({ clusters } const shardActivityData = shardActivity && filterShardActivityData(shardActivity); // no filter on data = null return ( - + + + + + + + + {metricsToShow.map((metric, index) => ( + + + + + ))} + + + + + + + + + + + + + ); +} diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js index bcdbbe715f86e..8c0b8b4c9c82d 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/index.js @@ -6,3 +6,4 @@ */ export { ShardActivity } from './shard_activity'; +export { ShardActivityReact } from './shard_activity_react'; diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js index 76f22583af0c8..1f0ed47adf387 100644 --- a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/parse_props.js @@ -37,18 +37,26 @@ export const parseProps = (props) => { target, translog, type, + timezone, } = props; const { files, size } = index; - const injector = Legacy.shims.getAngularInjector(); - const timezone = injector.get('config').get('dateFormat:tz'); + + let thisTimezone; + // react version passes timezone while Angular uses injector + if (!timezone) { + const injector = Legacy.shims.getAngularInjector(); + thisTimezone = injector.get('config').get('dateFormat:tz'); + } else { + thisTimezone = timezone; + } return { name: indexName || index.name, shard: `${id} / ${isPrimary ? 'Primary' : 'Replica'}`, relocationType: type === 'PRIMARY_RELOCATION' ? 'Primary Relocation' : normalizeString(type), stage: normalizeString(stage), - startTime: formatDateTimeLocal(startTimeInMillis, timezone), + startTime: formatDateTimeLocal(startTimeInMillis, thisTimezone), totalTime: formatMetric(Math.floor(totalTimeInMillis / 1000), '00:00:00'), isCopiedFromPrimary: !isPrimary || type === 'PRIMARY_RELOCATION', sourceName: source.name === undefined ? 'n/a' : source.name, diff --git a/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js new file mode 100644 index 0000000000000..cc219ff0fff32 --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/elasticsearch/shard_activity/shard_activity_react.js @@ -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 React, { Fragment } from 'react'; +import { EuiText, EuiTitle, EuiLink, EuiSpacer, EuiSwitch } from '@elastic/eui'; +import { EuiMonitoringTable } from '../../table'; +import { RecoveryIndex } from './recovery_index'; +import { TotalTime } from './total_time'; +import { SourceDestination } from './source_destination'; +import { FilesProgress, BytesProgress, TranslogProgress } from './progress'; +import { parseProps } from './parse_props'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; + +const columns = [ + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.indexTitle', { + defaultMessage: 'Index', + }), + field: 'name', + render: (_name, shard) => , + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.stageTitle', { + defaultMessage: 'Stage', + }), + field: 'stage', + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.totalTimeTitle', { + defaultMessage: 'Total Time', + }), + field: null, + render: (shard) => , + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.sourceDestinationTitle', { + defaultMessage: 'Source / Destination', + }), + field: null, + render: (shard) => , + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.filesTitle', { + defaultMessage: 'Files', + }), + field: null, + render: (shard) => , + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.bytesTitle', { + defaultMessage: 'Bytes', + }), + field: null, + render: (shard) => , + }, + { + name: i18n.translate('xpack.monitoring.kibana.shardActivity.translogTitle', { + defaultMessage: 'Translog', + }), + field: null, + render: (shard) => , + }, +]; + +export const ShardActivityReact = (props) => { + const { + data: rawData, + sorting, + pagination, + onTableChange, + toggleShardActivityHistory, + showShardActivityHistory, + } = props; + const { services } = useKibana(); + const timezone = services.uiSettings?.get('dateFormat:tz'); + const getNoDataMessage = () => { + if (showShardActivityHistory) { + return i18n.translate('xpack.monitoring.elasticsearch.shardActivity.noDataMessage', { + defaultMessage: + 'There are no historical shard activity records for the selected time range.', + }); + } + return ( + + +
+ + + + ), + }} + /> +
+ ); + }; + + const rows = rawData.map((data) => parseProps({ ...data, timezone })); + + return ( + + + +

+ +

+
+
+ + + } + onChange={toggleShardActivityHistory} + checked={showShardActivityHistory} + /> + + +
+ ); +}; From 9e42b32015cd0bdca0aecbfc93c3bfc1b1404e1a Mon Sep 17 00:00:00 2001 From: Sandra G Date: Tue, 12 Oct 2021 09:32:52 -0400 Subject: [PATCH 060/287] [Stack Monitoring] Add breadcrumbs to ES pages after migrating from Angular (#114555) * add breadcrumbs * fix bad merge * fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../pages/elasticsearch/ccr_page.tsx | 13 +++++++++-- .../pages/elasticsearch/ccr_shard_page.tsx | 22 +++++++++++++++--- .../elasticsearch/index_advanced_page.tsx | 21 +++++++++++++++-- .../pages/elasticsearch/index_page.tsx | 20 ++++++++++++++-- .../pages/elasticsearch/indices_page.tsx | 14 +++++++++-- .../pages/elasticsearch/ml_jobs_page.tsx | 14 +++++++++-- .../elasticsearch/node_advanced_page.tsx | 23 ++++++++++++++++--- .../pages/elasticsearch/node_page.tsx | 22 +++++++++++++++--- 8 files changed, 130 insertions(+), 19 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx index 8a9a736286c3f..cb37705c959aa 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_page.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; @@ -18,7 +18,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; - +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; interface SetupModeProps { setupMode: any; flyoutComponent: any; @@ -27,6 +27,7 @@ interface SetupModeProps { export const ElasticsearchCcrPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { services } = useKibana<{ data: any }>(); const clusterUuid = globalState.cluster_uuid; @@ -37,6 +38,14 @@ export const ElasticsearchCcrPage: React.FC = ({ clusters }) => const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + }); + } + }, [cluster, generateBreadcrumbs]); + const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.title', { defaultMessage: 'Elasticsearch - Ccr', }); diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx index 21f9fd10f0806..29cf9ade8d997 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ccr_shard_page.tsx @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import { find } from 'lodash'; import { get } from 'lodash'; import { i18n } from '@kbn/i18n'; import { PageTemplate } from '../page_template'; @@ -19,6 +20,7 @@ import { SetupModeContext } from '../../../components/setup_mode/setup_mode_cont import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { ELASTICSEARCH_SYSTEM_ID, RULE_CCR_READ_EXCEPTIONS } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; interface SetupModeProps { setupMode: any; @@ -26,14 +28,28 @@ interface SetupModeProps { bottomBarComponent: any; } -export const ElasticsearchCcrShardPage: React.FC = () => { +export const ElasticsearchCcrShardPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); const { services } = useKibana<{ data: any }>(); + const [data, setData] = useState({} as any); const { index, shardId }: { index: string; shardId: string } = useParams(); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const clusterUuid = globalState.cluster_uuid; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + name: 'ccr', + instance: `Index: ${index} Shard: ${shardId}`, + }); + } + }, [cluster, generateBreadcrumbs, index, shardId]); const ccs = globalState.ccs; - const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); const title = i18n.translate('xpack.monitoring.elasticsearch.ccr.shard.title', { diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx index 86dba4e2f921c..f2f2ec36b7cd9 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_advanced_page.tsx @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; +import { find } from 'lodash'; import { useParams } from 'react-router-dom'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; @@ -19,9 +20,11 @@ import { AdvancedIndex } from '../../../components/elasticsearch/index/advanced' import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { ELASTICSEARCH_SYSTEM_ID, RULE_LARGE_SHARD_SIZE } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -export const ElasticsearchIndexAdvancedPage: React.FC = () => { +export const ElasticsearchIndexAdvancedPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); const { zoomInfo, onBrush } = useCharts(); @@ -29,6 +32,20 @@ export const ElasticsearchIndexAdvancedPage: React.FC = () => { const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + name: 'indices', + instance: index, + }); + } + }, [cluster, generateBreadcrumbs, index]); + const title = i18n.translate('xpack.monitoring.elasticsearch.index.advanced.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Advanced', values: { diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx index b73fee0c963cc..8e70a99e67914 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/index_page.tsx @@ -4,9 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; +import { find } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { GlobalStateContext } from '../../contexts/global_state_context'; // @ts-ignore @@ -23,9 +24,11 @@ import { labels } from '../../../components/elasticsearch/shard_allocation/lib/l import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { ELASTICSEARCH_SYSTEM_ID, RULE_LARGE_SHARD_SIZE } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -export const ElasticsearchIndexPage: React.FC = () => { +export const ElasticsearchIndexPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { services } = useKibana<{ data: any }>(); const { index }: { index: string } = useParams(); const { zoomInfo, onBrush } = useCharts(); @@ -34,6 +37,19 @@ export const ElasticsearchIndexPage: React.FC = () => { const [indexLabel, setIndexLabel] = useState(labels.index as any); const [nodesByIndicesData, setNodesByIndicesData] = useState([]); const [alerts, setAlerts] = useState({}); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + name: 'indices', + instance: index, + }); + } + }, [cluster, generateBreadcrumbs, index]); const title = i18n.translate('xpack.monitoring.elasticsearch.index.overview.title', { defaultMessage: 'Elasticsearch - Indices - {indexName} - Overview', diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx index 44e01cbf66ff3..277bde2ac35cb 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/indices_page.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; @@ -19,16 +19,18 @@ import { useLocalStorage } from '../../hooks/use_local_storage'; import { AlertsByName } from '../../../alerts/types'; import { fetchAlerts } from '../../../lib/fetch_alerts'; import { ELASTICSEARCH_SYSTEM_ID, RULE_LARGE_SHARD_SIZE } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; export const ElasticsearchIndicesPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { services } = useKibana<{ data: any }>(); const { getPaginationTableProps } = useTable('elasticsearch.indices'); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; const [data, setData] = useState({} as any); const [showSystemIndices, setShowSystemIndices] = useLocalStorage( 'showSystemIndices', @@ -36,6 +38,14 @@ export const ElasticsearchIndicesPage: React.FC = ({ clusters }) ); const [alerts, setAlerts] = useState({}); + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + }); + } + }, [cluster, generateBreadcrumbs]); + const title = i18n.translate('xpack.monitoring.elasticsearch.indices.routeTitle', { defaultMessage: 'Elasticsearch - Indices', }); diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx index 7edf15886cc20..b97007f1c1462 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/ml_jobs_page.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { find } from 'lodash'; import { ElasticsearchTemplate } from './elasticsearch_template'; @@ -16,6 +16,7 @@ import { SetupModeRenderer } from '../../setup_mode/setup_mode_renderer'; import { SetupModeContext } from '../../../components/setup_mode/setup_mode_context'; import { useTable } from '../../hooks/use_table'; import type { MLJobs } from '../../../types'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; import { ELASTICSEARCH_SYSTEM_ID } from '../../../../common/constants'; interface SetupModeProps { @@ -26,13 +27,22 @@ interface SetupModeProps { export const ElasticsearchMLJobsPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { services } = useKibana<{ data: any }>(); const { getPaginationTableProps } = useTable('elasticsearch.mlJobs'); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; const cluster = find(clusters, { cluster_uuid: clusterUuid, - }); + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + }); + } + }, [cluster, generateBreadcrumbs]); const [data, setData] = useState({} as any); const title = i18n.translate('xpack.monitoring.elasticsearch.mlJobs.routeTitle', { diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx index 9c0f5f4627b01..820eb2fb20cd8 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_advanced_page.tsx @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ItemTemplate } from './item_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -24,19 +25,35 @@ import { RULE_DISK_USAGE, RULE_MEMORY_USAGE, } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -export const ElasticsearchNodeAdvancedPage: React.FC = () => { +export const ElasticsearchNodeAdvancedPage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { zoomInfo, onBrush } = useCharts(); + const [data, setData] = useState({} as any); const { node }: { node: string } = useParams(); const { services } = useKibana<{ data: any }>(); const clusterUuid = globalState.cluster_uuid; const ccs = globalState.ccs; - const [data, setData] = useState({} as any); const [alerts, setAlerts] = useState({}); + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + name: 'nodes', + instance: data?.nodeSummary?.name, + }); + } + }, [cluster, generateBreadcrumbs, data?.nodeSummary?.name]); + const title = i18n.translate('xpack.monitoring.elasticsearch.node.advanced.title', { defaultMessage: 'Elasticsearch - Nodes - {nodeName} - Advanced', values: { diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx index f5ae4dc29b28e..b2d6fb94183ec 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/node_page.tsx @@ -4,8 +4,9 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { useContext, useState, useCallback } from 'react'; +import React, { useContext, useState, useCallback, useEffect } from 'react'; import { useParams } from 'react-router-dom'; +import { find } from 'lodash'; import { i18n } from '@kbn/i18n'; import { ItemTemplate } from './item_template'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; @@ -30,9 +31,11 @@ import { RULE_DISK_USAGE, RULE_MEMORY_USAGE, } from '../../../../common/constants'; +import { BreadcrumbContainer } from '../../hooks/use_breadcrumbs'; -export const ElasticsearchNodePage: React.FC = () => { +export const ElasticsearchNodePage: React.FC = ({ clusters }) => { const globalState = useContext(GlobalStateContext); + const { generate: generateBreadcrumbs } = useContext(BreadcrumbContainer.Context); const { zoomInfo, onBrush } = useCharts(); const [showSystemIndices, setShowSystemIndices] = useLocalStorage( 'showSystemIndices', @@ -42,10 +45,23 @@ export const ElasticsearchNodePage: React.FC = () => { const { node }: { node: string } = useParams(); const { services } = useKibana<{ data: any }>(); + const [data, setData] = useState({} as any); const clusterUuid = globalState.cluster_uuid; + const cluster = find(clusters, { + cluster_uuid: clusterUuid, + }) as any; + + useEffect(() => { + if (cluster) { + generateBreadcrumbs(cluster.cluster_name, { + inElasticsearch: true, + name: 'nodes', + instance: data?.nodeSummary?.name, + }); + } + }, [cluster, generateBreadcrumbs, data?.nodeSummary?.name]); const ccs = globalState.ccs; - const [data, setData] = useState({} as any); const [nodesByIndicesData, setNodesByIndicesData] = useState([]); const title = i18n.translate('xpack.monitoring.elasticsearch.node.overview.title', { From d37cf3045bed23a05c491a4eebe9928f5b054be5 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 12 Oct 2021 06:35:55 -0700 Subject: [PATCH 061/287] [Reporting] Remove unused settings for 8.0 (#114216) * [Reporting] Remove unused settings for 8.0 * add helpful version comments Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/reporting/server/config/index.ts | 10 +++------- x-pack/plugins/reporting/server/config/schema.ts | 4 ++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index c7afdb22f8bdb..f8fa47bc00bb0 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -17,13 +17,9 @@ export const config: PluginConfigDescriptor = { exposeToBrowser: { poll: true, roles: true }, schema: ConfigSchema, deprecations: ({ unused }) => [ - unused('capture.browser.chromium.maxScreenshotDimension'), - unused('capture.concurrency'), - unused('capture.settleTime'), - unused('capture.timeout'), - unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), - unused('poll.jobsRefresh.intervalErrorMultiplier'), - unused('kibanaApp'), + unused('capture.browser.chromium.maxScreenshotDimension'), // unused since 7.8 + unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), // unused since 7.10 + unused('poll.jobsRefresh.intervalErrorMultiplier'), // unused since 7.10 (settings, fromPath, addDeprecation) => { const reporting = get(settings, fromPath); if (reporting?.index) { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index affd8b7bee7ff..832cf6c28e1fa 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -160,11 +160,11 @@ const RolesSchema = schema.object({ const PollSchema = schema.object({ jobCompletionNotifier: schema.object({ interval: schema.number({ defaultValue: 10000 }), - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // deprecated as unused since 7.10 }), jobsRefresh: schema.object({ interval: schema.number({ defaultValue: 5000 }), - intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // unused + intervalErrorMultiplier: schema.number({ defaultValue: 5 }), // deprecated as unused since 7.10 }), }); From 4f893931248fbdf6bbfad3459b0fd33404f65697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Louv-Jansen?= Date: Tue, 12 Oct 2021 15:37:50 +0200 Subject: [PATCH 062/287] [APM] Add Table of contents to data model docs (#114608) --- x-pack/plugins/apm/dev_docs/apm_queries.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/x-pack/plugins/apm/dev_docs/apm_queries.md b/x-pack/plugins/apm/dev_docs/apm_queries.md index 8508e5a173c85..0fbcd4fc1c8a8 100644 --- a/x-pack/plugins/apm/dev_docs/apm_queries.md +++ b/x-pack/plugins/apm/dev_docs/apm_queries.md @@ -1,7 +1,17 @@ -# Data model +### Table of Contents + - [Transactions](#transactions) + - [System metrics](#system-metrics) + - [Transaction breakdown metrics](#transaction-breakdown-metrics) + - [Span breakdown metrics](#span-breakdown-metrics) + - [Service destination metrics](#service-destination-metrics) + - [Common filters](#common-filters) + +--- + +### Data model Elastic APM agents capture different types of information from within their instrumented applications. These are known as events, and can be spans, transactions, errors, or metrics. You can find more information [here](https://www.elastic.co/guide/en/apm/get-started/current/apm-data-model.html). -# Running examples +### Running examples You can run the example queries on the [edge cluster](https://edge-oblt.elastic.dev/) or any another cluster that contains APM data. # Transactions @@ -307,7 +317,7 @@ The above example is overly simplified. In reality [we do a bit more](https://gi -# Transaction breakdown metrics (`transaction_breakdown`) +# Transaction breakdown metrics A pre-aggregations of transaction documents where `transaction.breakdown.count` is the number of original transactions. @@ -327,7 +337,7 @@ Noteworthy fields: `transaction.name`, `transaction.type` } ``` -# Span breakdown metrics (`span_breakdown`) +# Span breakdown metrics A pre-aggregations of span documents where `span.self_time.count` is the number of original spans. Measures the "self-time" for a span type, and optional subtype, within a transaction group. @@ -482,7 +492,7 @@ GET apm-*-metric-*,metrics-apm*/_search?terminate_after=1000 } ``` -## Common filters +# Common filters Most Elasticsearch queries will need to have one or more filters. There are a couple of reasons for adding filters: From afe81bb1a2a8da526a1f42f19e442c32e2ab0fd0 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Tue, 12 Oct 2021 15:49:17 +0200 Subject: [PATCH 063/287] [Reporting] Fix missing force now behaviour for v2 reports (#114516) * fix missing force now behaviour for v2 reports * added jest test * updated jest test snapshot to match removal of forceNow injection from locator params Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/export_types/common/index.ts | 1 - .../export_types/common/set_force_now.ts | 24 ---------- .../v2/get_full_redirect_app_url.test.ts | 45 +++++++++++++++++++ .../common/v2/get_full_redirect_app_url.ts | 7 ++- .../export_types/png_v2/execute_job.test.ts | 6 +-- .../server/export_types/png_v2/execute_job.ts | 5 +-- .../printable_pdf_v2/execute_job.ts | 3 +- .../printable_pdf_v2/lib/generate_pdf.ts | 5 ++- 8 files changed, 60 insertions(+), 36 deletions(-) delete mode 100644 x-pack/plugins/reporting/server/export_types/common/set_force_now.ts create mode 100644 x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index 09d3236fa7b54..c35dcb5344e21 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -12,7 +12,6 @@ export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; export { generatePngObservableFactory } from './generate_png'; export { getCustomLogo } from './get_custom_logo'; -export { setForceNow } from './set_force_now'; export interface TimeRangeParams { min?: Date | string | number | null; diff --git a/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts b/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts deleted file mode 100644 index b4f4b1b0ace05..0000000000000 --- a/x-pack/plugins/reporting/server/export_types/common/set_force_now.ts +++ /dev/null @@ -1,24 +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 type { LocatorParams } from '../../../common/types'; - -/** - * Add `forceNow` to {@link LocatorParams['params']} to enable clients to set the time appropriately when - * reporting navigates to the page in Chromium. - */ -export const setForceNow = - (forceNow: string) => - (locator: LocatorParams): LocatorParams => { - return { - ...locator, - params: { - ...locator.params, - forceNow, - }, - }; - }; diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts new file mode 100644 index 0000000000000..7a2ec5b83e7f4 --- /dev/null +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.test.ts @@ -0,0 +1,45 @@ +/* + * 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 { get } from 'lodash'; +import { ReportingConfig } from '../../../config/config'; +import { getFullRedirectAppUrl } from './get_full_redirect_app_url'; + +describe('getFullRedirectAppUrl', () => { + let config: ReportingConfig; + + beforeEach(() => { + const values = { + server: { + basePath: 'test', + }, + kibanaServer: { + protocol: 'http', + hostname: 'localhost', + port: '1234', + }, + }; + config = { + get: jest.fn((...args: string[]) => get(values, args)), + kbnConfig: { + get: jest.fn((...args: string[]) => get(values, args)), + }, + }; + }); + + test('smoke test', () => { + expect(getFullRedirectAppUrl(config, 'test', undefined)).toBe( + 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r' + ); + }); + + test('adding forceNow', () => { + expect(getFullRedirectAppUrl(config, 'test', 'TEST with a space')).toBe( + 'http://localhost:1234/test/s/test/app/management/insightsAndAlerting/reporting/r?forceNow=TEST%20with%20a%20space' + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts index bb640eff667e9..9c329db64fa1a 100644 --- a/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts +++ b/x-pack/plugins/reporting/server/export_types/common/v2/get_full_redirect_app_url.ts @@ -10,7 +10,11 @@ import { ReportingConfig } from '../../..'; import { getRedirectAppPath } from '../../../../common/constants'; import { buildKibanaPath } from '../../../../common/build_kibana_path'; -export function getFullRedirectAppUrl(config: ReportingConfig, spaceId?: string) { +export function getFullRedirectAppUrl( + config: ReportingConfig, + spaceId?: string, + forceNow?: string +) { const [basePath, protocol, hostname, port] = [ config.kbnConfig.get('server', 'basePath'), config.get('kibanaServer', 'protocol'), @@ -29,5 +33,6 @@ export function getFullRedirectAppUrl(config: ReportingConfig, spaceId?: string) hostname, port, pathname: path, + query: forceNow ? { forceNow } : undefined, }); } diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index e57eab382468c..3cf3c057e7b9c 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -102,12 +102,10 @@ test(`passes browserTimezone to generatePng`, async () => { "warning": [Function], }, Array [ - "localhost:80undefined/app/management/insightsAndAlerting/reporting/r", + "localhost:80undefined/app/management/insightsAndAlerting/reporting/r?forceNow=test", Object { "id": "test", - "params": Object { - "forceNow": "test", - }, + "params": Object {}, "version": "test", }, ], diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 5c2cc66d3d3aa..a7478de1cc96e 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -16,7 +16,6 @@ import { getConditionalHeaders, omitBlockedHeaders, generatePngObservableFactory, - setForceNow, } from '../common'; import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url'; import { TaskPayloadPNGV2 } from './types'; @@ -38,8 +37,8 @@ export const runTaskFnFactory: RunTaskFnFactory> = map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), mergeMap((conditionalHeaders) => { - const url = getFullRedirectAppUrl(config, job.spaceId); - const [locatorParams] = job.locatorParams.map(setForceNow(job.forceNow)); + const url = getFullRedirectAppUrl(config, job.spaceId, job.forceNow); + const [locatorParams] = job.locatorParams; apmGetAssets?.end(); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index e44f5e98fa4fe..2c553295aa840 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -16,7 +16,6 @@ import { getConditionalHeaders, omitBlockedHeaders, getCustomLogo, - setForceNow, } from '../common'; import { generatePdfObservableFactory } from './lib/generate_pdf'; import { TaskPayloadPDFV2 } from './types'; @@ -50,7 +49,7 @@ export const runTaskFnFactory: RunTaskFnFactory> = jobLogger, job, title, - locatorParams.map(setForceNow(job.forceNow)), + locatorParams, browserTimezone, conditionalHeaders, layout, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index 424a347876a1d..9fb31a1104279 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -56,7 +56,10 @@ export async function generatePdfObservableFactory(reporting: ReportingCore) { /** * For each locator we get the relative URL to the redirect app */ - const urls = locatorParams.map(() => getFullRedirectAppUrl(reporting.getConfig(), job.spaceId)); + const urls = locatorParams.map(() => + getFullRedirectAppUrl(reporting.getConfig(), job.spaceId, job.forceNow) + ); + const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { logger, urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[], From bc96e408c9a852b3aa78fbea283a6e6ff595c1e9 Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Tue, 12 Oct 2021 07:02:03 -0700 Subject: [PATCH 064/287] Changes `rewriteBasePath` core config deprecation level to warning (#114566) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../server/config/deprecation/core_deprecations.test.ts | 7 ++++++- src/core/server/config/deprecation/core_deprecations.ts | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index e08f2216f5cbe..99fe6c7cd1dc4 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -54,7 +54,7 @@ describe('core deprecations', () => { describe('rewriteBasePath', () => { it('logs a warning is server.basePath is set and server.rewriteBasePath is not', () => { - const { messages } = applyCoreDeprecations({ + const { messages, levels } = applyCoreDeprecations({ server: { basePath: 'foo', }, @@ -64,6 +64,11 @@ describe('core deprecations', () => { "You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana will expect that all requests start with server.basePath rather than expecting you to rewrite the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the current behavior and silence this warning.", ] `); + expect(levels).toMatchInlineSnapshot(` + Array [ + "warning", + ] + `); }); it('does not log a warning if both server.basePath and server.rewriteBasePath are unset', () => { diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 5adbb338b42e4..79fb2aac60da4 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -16,6 +16,7 @@ const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDe 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + 'the requests in your reverse proxy. Set server.rewriteBasePath to false to preserve the ' + 'current behavior and silence this warning.', + level: 'warning', correctiveActions: { manualSteps: [ `Set 'server.rewriteBasePath' in the config file, CLI flag, or environment variable (in Docker only).`, From d5d364724bb6da744c5708a797a3358040cfc318 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 12 Oct 2021 16:32:40 +0200 Subject: [PATCH 065/287] [Exploratory view] Fix auto apply on date change (#114251) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/date_range_picker.tsx | 10 +- .../configurations/lens_attributes.ts | 6 +- .../exploratory_view/exploratory_view.tsx | 6 +- .../exploratory_view/header/header.test.tsx | 5 +- .../shared/exploratory_view/header/header.tsx | 11 +- .../hooks/use_lens_attributes.ts | 2 +- .../hooks/use_series_storage.tsx | 2 +- .../hooks/use_time_range.test.tsx | 108 ++++++++++++++++++ .../exploratory_view/hooks/use_time_range.ts | 66 +++++++++++ .../exploratory_view/lens_embeddable.tsx | 42 +------ 10 files changed, 194 insertions(+), 64 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.test.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.ts diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx index 643f01d570ead..5529f28927028 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/date_range_picker.tsx @@ -15,7 +15,7 @@ import { useUiSetting } from '../../../../../../../../src/plugins/kibana_react/p import { SeriesUrl } from '../types'; import { ReportTypes } from '../configurations/constants'; -export const parseAbsoluteDate = (date: string, options = {}) => { +export const parseRelativeDate = (date: string, options = {}) => { return DateMath.parse(date, options)!; }; export function DateRangePicker({ seriesId, series }: { seriesId: number; series: SeriesUrl }) { @@ -27,12 +27,12 @@ export function DateRangePicker({ seriesId, series }: { seriesId: number; series const { from: mainFrom, to: mainTo } = firstSeries!.time; - const startDate = parseAbsoluteDate(seriesFrom ?? mainFrom)!; - const endDate = parseAbsoluteDate(seriesTo ?? mainTo, { roundUp: true })!; + const startDate = parseRelativeDate(seriesFrom ?? mainFrom)!; + const endDate = parseRelativeDate(seriesTo ?? mainTo, { roundUp: true })!; const getTotalDuration = () => { - const mainStartDate = parseAbsoluteDate(mainFrom)!; - const mainEndDate = parseAbsoluteDate(mainTo, { roundUp: true })!; + const mainStartDate = parseRelativeDate(mainFrom)!; + const mainEndDate = parseRelativeDate(mainTo, { roundUp: true })!; return mainEndDate.diff(mainStartDate, 'millisecond'); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 428ef54277fe2..fa5a8beb0087d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -41,7 +41,7 @@ import { } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; import { PersistableFilter } from '../../../../../../lens/common'; -import { parseAbsoluteDate } from '../components/date_range_picker'; +import { parseRelativeDate } from '../components/date_range_picker'; import { getDistributionInPercentageColumn } from './lens_columns/overall_column'; function getLayerReferenceName(layerId: string) { @@ -544,11 +544,11 @@ export class LensAttributes { time: { from }, } = layerConfig; - const inDays = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days')); + const inDays = Math.abs(parseRelativeDate(mainFrom).diff(parseRelativeDate(from), 'days')); if (inDays > 1) { return inDays + 'd'; } - const inHours = Math.abs(parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours')); + const inHours = Math.abs(parseRelativeDate(mainFrom).diff(parseRelativeDate(from), 'hours')); if (inHours === 0) { return null; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index faf064868dec5..9870d88d1220a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -96,11 +96,7 @@ export function ExploratoryView({ {lens ? ( <> - + + ); getByText('Refresh'); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 59b146ae9af1a..181c8342b87af 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -11,21 +11,18 @@ import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@el import { TypedLensByValueInput } from '../../../../../../lens/public'; import { useSeriesStorage } from '../hooks/use_series_storage'; import { LastUpdated } from './last_updated'; -import { combineTimeRanges } from '../lens_embeddable'; import { ExpViewActionMenu } from '../components/action_menu'; +import { useExpViewTimeRange } from '../hooks/use_time_range'; interface Props { - seriesId?: number; lastUpdated?: number; lensAttributes: TypedLensByValueInput['attributes'] | null; } -export function ExploratoryViewHeader({ seriesId, lensAttributes, lastUpdated }: Props) { - const { getSeries, allSeries, setLastRefresh, reportType } = useSeriesStorage(); +export function ExploratoryViewHeader({ lensAttributes, lastUpdated }: Props) { + const { setLastRefresh } = useSeriesStorage(); - const series = seriesId ? getSeries(seriesId) : undefined; - - const timeRange = combineTimeRanges(reportType, allSeries, series); + const timeRange = useExpViewTimeRange(); return ( <> diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 23eee140b68cf..dbf36777b536f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -94,7 +94,7 @@ export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null if (isEmpty(indexPatterns) || isEmpty(allSeries) || !reportType) { return null; } - + // we only use the data from url to apply, since that get's updated to apply changes const allSeriesT: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); const layerConfigs = getLayerConfigs(allSeriesT, reportType, theme, indexPatterns); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index 83042876db2ae..e71c66ba1f11b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -45,7 +45,7 @@ export function convertAllShortSeries(allShortSeries: AllShortSeries) { } export const allSeriesKey = 'sr'; -const reportTypeKey = 'reportType'; +export const reportTypeKey = 'reportType'; export function UrlStorageContextProvider({ children, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.test.tsx new file mode 100644 index 0000000000000..38534b1c79e3e --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.test.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { allSeriesKey, reportTypeKey, UrlStorageContextProvider } from './use_series_storage'; +import { renderHook } from '@testing-library/react-hooks'; +import { useExpViewTimeRange } from './use_time_range'; +import { ReportTypes } from '../configurations/constants'; +import { createKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public'; +import { TRANSACTION_DURATION } from '../configurations/constants/elasticsearch_fieldnames'; + +const mockSingleSeries = [ + { + name: 'performance-distribution', + dataType: 'ux', + breakdown: 'user_agent.name', + time: { from: 'now-15m', to: 'now' }, + selectedMetricField: TRANSACTION_DURATION, + reportDefinitions: { 'service.name': ['elastic-co'] }, + }, +]; + +const mockMultipleSeries = [ + ...mockSingleSeries, + { + name: 'kpi-over-time', + dataType: 'synthetics', + breakdown: 'user_agent.name', + time: { from: 'now-30m', to: 'now' }, + selectedMetricField: TRANSACTION_DURATION, + reportDefinitions: { 'service.name': ['elastic-co'] }, + }, +]; + +describe('useExpViewTimeRange', function () { + const storage = createKbnUrlStateStorage({ useHash: false }); + + function Wrapper({ children }: { children: JSX.Element }) { + return {children}; + } + it('should return expected result when there is one series', async function () { + await storage.set(allSeriesKey, mockSingleSeries); + await storage.set(reportTypeKey, ReportTypes.KPI); + + const { result } = renderHook(() => useExpViewTimeRange(), { + wrapper: Wrapper, + }); + + expect(result.current).toEqual({ + from: 'now-15m', + to: 'now', + }); + }); + + it('should return expected result when there are multiple KPI series', async function () { + await storage.set(allSeriesKey, mockMultipleSeries); + await storage.set(reportTypeKey, ReportTypes.KPI); + + const { result } = renderHook(() => useExpViewTimeRange(), { + wrapper: Wrapper, + }); + + expect(result.current).toEqual({ + from: 'now-15m', + to: 'now', + }); + }); + + it('should return expected result when there are multiple distribution series with relative dates', async function () { + await storage.set(allSeriesKey, mockMultipleSeries); + await storage.set(reportTypeKey, ReportTypes.DISTRIBUTION); + + const { result } = renderHook(() => useExpViewTimeRange(), { + wrapper: Wrapper, + }); + + expect(result.current).toEqual({ + from: 'now-30m', + to: 'now', + }); + }); + + it('should return expected result when there are multiple distribution series with absolute dates', async function () { + // from:'2021-10-11T09:55:39.551Z',to:'2021-10-11T10:55:41.516Z'))) + mockMultipleSeries[0].time.from = '2021-10-11T09:55:39.551Z'; + mockMultipleSeries[0].time.to = '2021-10-11T11:55:41.516Z'; + + mockMultipleSeries[1].time.from = '2021-01-11T09:55:39.551Z'; + mockMultipleSeries[1].time.to = '2021-10-11T10:55:41.516Z'; + + await storage.set(allSeriesKey, mockMultipleSeries); + await storage.set(reportTypeKey, ReportTypes.DISTRIBUTION); + + const { result } = renderHook(() => useExpViewTimeRange(), { + wrapper: Wrapper, + }); + + expect(result.current).toEqual({ + from: '2021-01-11T09:55:39.551Z', + to: '2021-10-11T11:55:41.516Z', + }); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.ts new file mode 100644 index 0000000000000..60087cfd0330c --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_time_range.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty } from 'lodash'; +import { useMemo } from 'react'; +import { + AllSeries, + allSeriesKey, + convertAllShortSeries, + useSeriesStorage, +} from './use_series_storage'; + +import { ReportViewType, SeriesUrl } from '../types'; +import { ReportTypes } from '../configurations/constants'; +import { parseRelativeDate } from '../components/date_range_picker'; + +export const combineTimeRanges = ( + reportType: ReportViewType, + allSeries: SeriesUrl[], + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + + if (reportType === ReportTypes.KPI) { + return firstSeries?.time; + } + + allSeries.forEach((series) => { + if ( + series.dataType && + series.selectedMetricField && + !isEmpty(series.reportDefinitions) && + series.time + ) { + const seriesFrom = parseRelativeDate(series.time.from)!; + const seriesTo = parseRelativeDate(series.time.to, { roundUp: true })!; + + if (!to || seriesTo > parseRelativeDate(to, { roundUp: true })) { + to = series.time.to; + } + if (!from || seriesFrom < parseRelativeDate(from)) { + from = series.time.from; + } + } + }); + + return { to, from }; +}; +export const useExpViewTimeRange = () => { + const { storage, reportType, lastRefresh, firstSeries } = useSeriesStorage(); + + return useMemo(() => { + // we only use the data from url to apply, since that get updated to apply changes + const allSeriesFromUrl: AllSeries = convertAllShortSeries(storage.get(allSeriesKey) ?? []); + const firstSeriesT = allSeriesFromUrl?.[0]; + + return firstSeriesT ? combineTimeRanges(reportType, allSeriesFromUrl, firstSeriesT) : undefined; + // we want to keep last refresh in dependencies to force refresh + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [reportType, storage, lastRefresh, firstSeries]); +}; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx index 9e4d9486dc155..235790e72862c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/lens_embeddable.tsx @@ -8,50 +8,16 @@ import { i18n } from '@kbn/i18n'; import React, { Dispatch, SetStateAction, useCallback } from 'react'; import styled from 'styled-components'; -import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useSeriesStorage } from './hooks/use_series_storage'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; -import { ReportViewType, SeriesUrl } from './types'; -import { ReportTypes } from './configurations/constants'; +import { useExpViewTimeRange } from './hooks/use_time_range'; interface Props { lensAttributes: TypedLensByValueInput['attributes']; setLastUpdated: Dispatch>; } -export const combineTimeRanges = ( - reportType: ReportViewType, - allSeries: SeriesUrl[], - firstSeries?: SeriesUrl -) => { - let to: string = ''; - let from: string = ''; - - if (reportType === ReportTypes.KPI) { - return firstSeries?.time; - } - - allSeries.forEach((series) => { - if ( - series.dataType && - series.selectedMetricField && - !isEmpty(series.reportDefinitions) && - series.time - ) { - const seriesTo = new Date(series.time.to); - const seriesFrom = new Date(series.time.from); - if (!to || seriesTo > new Date(to)) { - to = series.time.to; - } - if (!from || seriesFrom < new Date(from)) { - from = series.time.from; - } - } - }); - - return { to, from }; -}; export function LensEmbeddable(props: Props) { const { lensAttributes, setLastUpdated } = props; @@ -62,11 +28,11 @@ export function LensEmbeddable(props: Props) { const LensComponent = lens?.EmbeddableComponent; - const { firstSeries, setSeries, allSeries, reportType } = useSeriesStorage(); + const { firstSeries, setSeries, reportType } = useSeriesStorage(); const firstSeriesId = 0; - const timeRange = firstSeries ? combineTimeRanges(reportType, allSeries, firstSeries) : null; + const timeRange = useExpViewTimeRange(); const onLensLoad = useCallback(() => { setLastUpdated(Date.now()); @@ -93,7 +59,7 @@ export function LensEmbeddable(props: Props) { [reportType, setSeries, firstSeries, notifications?.toasts] ); - if (timeRange === null || !firstSeries) { + if (!timeRange || !firstSeries) { return null; } From ff1b014c7bdcd2ab5aee896bca0cc077736aec58 Mon Sep 17 00:00:00 2001 From: Marco Vettorello Date: Tue, 12 Oct 2021 16:36:18 +0200 Subject: [PATCH 066/287] Update dependency @elastic/charts to v37 (master) (#113968) --- api_docs/charts.json | 4 +-- package.json | 2 +- .../static/utils/transform_click_event.ts | 4 +-- .../apps/main/components/chart/histogram.tsx | 7 +++-- .../vis_types/pie/public/utils/get_layers.ts | 1 - .../components/timelion_vis_component.tsx | 2 ++ .../timelion/public/helpers/panel_utils.ts | 6 ++-- .../xy/public/components/xy_settings.tsx | 1 - .../vis_types/xy/public/config/get_axis.ts | 16 ++--------- .../vis_types/xy/public/utils/domain.ts | 12 ++++---- .../utils/render_all_series.test.mocks.ts | 15 ++++++++-- .../vis_types/xy/public/vis_component.tsx | 6 +++- .../apps/dashboard/dashboard_state.ts | 14 ++++------ .../page_objects/visualize_chart_page.ts | 12 ++++---- .../services/visualizations/pie_chart.ts | 20 +++++++++++-- .../alerting/chart_preview/index.tsx | 2 +- .../RumDashboard/Charts/PageLoadDistChart.tsx | 5 ++-- .../distribution/index.tsx | 12 ++++---- .../transaction_details_tabs.tsx | 4 +-- .../app/transaction_details/types.ts | 4 +-- .../shared/charts/breakdown_chart/index.tsx | 5 +++- .../components/shared/charts/helper/helper.ts | 4 +-- .../shared/charts/timeseries_chart.tsx | 5 +++- .../document_count_chart.tsx | 9 ++++-- .../heatmap_visualization/chart_component.tsx | 3 +- .../pie_visualization/render_function.tsx | 1 - .../__snapshots__/expression.test.tsx.snap | 28 +++++++++---------- .../xy_visualization/expression.test.tsx | 16 +++++++---- .../public/xy_visualization/expression.tsx | 25 +++++++++-------- .../lens/public/xy_visualization/x_domain.tsx | 6 ++-- .../create_calendar.tsx | 8 +++--- .../explorer/swimlane_container.tsx | 23 ++++++++------- .../pages/components/charts/common/utils.ts | 2 +- .../event_rate_chart/event_rate_chart.tsx | 6 +--- .../components/app/section/apm/index.tsx | 12 ++++++-- .../public/components/app/section/helper.ts | 4 +-- .../components/app/section/logs/index.tsx | 12 ++++++-- .../components/app/section/uptime/index.tsx | 3 +- .../alert_types/threshold/visualization.tsx | 7 ++++- .../common/charts/duration_chart.tsx | 2 +- .../watch_visualization.tsx | 7 ++++- yarn.lock | 11 ++++---- 42 files changed, 205 insertions(+), 143 deletions(-) diff --git a/api_docs/charts.json b/api_docs/charts.json index 5d4f047a247e2..83a2a93df42a1 100644 --- a/api_docs/charts.json +++ b/api_docs/charts.json @@ -229,7 +229,7 @@ ", xAccessor: string | number | ", "AccessorFn", ") => ({ x: selectedRange }: ", - "XYBrushArea", + "XYBrushEvent", ") => ", { "pluginId": "charts", @@ -4266,4 +4266,4 @@ } ] } -} \ No newline at end of file +} diff --git a/package.json b/package.json index 705d902d6afef..16422e3fda27e 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@elastic/apm-generator": "link:bazel-bin/packages/elastic-apm-generator", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", - "@elastic/charts": "34.2.1", + "@elastic/charts": "37.0.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.15.0", diff --git a/src/plugins/charts/public/static/utils/transform_click_event.ts b/src/plugins/charts/public/static/utils/transform_click_event.ts index 7fdd59f47988d..d175046b20ebb 100644 --- a/src/plugins/charts/public/static/utils/transform_click_event.ts +++ b/src/plugins/charts/public/static/utils/transform_click_event.ts @@ -9,7 +9,7 @@ import { XYChartSeriesIdentifier, GeometryValue, - XYBrushArea, + XYBrushEvent, Accessor, AccessorFn, Datum, @@ -261,7 +261,7 @@ export const getFilterFromSeriesFn = */ export const getBrushFromChartBrushEventFn = (table: Datatable, xAccessor: Accessor | AccessorFn) => - ({ x: selectedRange }: XYBrushArea): BrushTriggerEvent => { + ({ x: selectedRange }: XYBrushEvent): BrushTriggerEvent => { const [start, end] = selectedRange ?? [0, 0]; const range: [number, number] = [start, end]; const column = table.columns.findIndex(({ id }) => validateAccessorId(id, xAccessor)); diff --git a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx index 333050e1ca5e6..350c46591c8b4 100644 --- a/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx +++ b/src/plugins/discover/public/application/apps/main/components/chart/histogram.tsx @@ -14,6 +14,7 @@ import dateMath from '@elastic/datemath'; import { Axis, BrushEndListener, + XYBrushEvent, Chart, ElementClickListener, HistogramBarSeries, @@ -65,8 +66,8 @@ export function DiscoverHistogram({ const timeZone = getTimezone(uiSettings); const { chartData, fetchStatus } = dataState; - const onBrushEnd: BrushEndListener = useCallback( - ({ x }) => { + const onBrushEnd = useCallback( + ({ x }: XYBrushEvent) => { if (!x) { return; } @@ -184,7 +185,7 @@ export function DiscoverHistogram({ { const fillLabel: Partial = { - textInvertible: true, valueFont: { fontWeight: 700, }, diff --git a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx index d7b7bb14723d7..e6d2638bedf48 100644 --- a/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx +++ b/src/plugins/vis_types/timelion/public/components/timelion_vis_component.tsx @@ -64,6 +64,8 @@ const DefaultYAxis = () => ( id="left" domain={withStaticPadding({ fit: false, + min: NaN, + max: NaN, })} position={Position.Left} groupId={`${MAIN_GROUP_ID}`} diff --git a/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts b/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts index 3c76b95bd05ca..98be5efc55a26 100644 --- a/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts +++ b/src/plugins/vis_types/timelion/public/helpers/panel_utils.ts @@ -88,8 +88,8 @@ const adaptYaxisParams = (yaxis: IAxis) => { tickFormat: y.tickFormatter, domain: withStaticPadding({ fit: y.min === undefined && y.max === undefined, - min: y.min, - max: y.max, + min: y.min ?? NaN, + max: y.max ?? NaN, }), }; }; @@ -118,6 +118,8 @@ export const extractAllYAxis = (series: Series[]) => { groupId, domain: withStaticPadding({ fit: false, + min: NaN, + max: NaN, }), id: (yaxis?.position || Position.Left) + index, position: Position.Left, diff --git a/src/plugins/vis_types/xy/public/components/xy_settings.tsx b/src/plugins/vis_types/xy/public/components/xy_settings.tsx index 5e02b65822d6c..74aff7535c2d8 100644 --- a/src/plugins/vis_types/xy/public/components/xy_settings.tsx +++ b/src/plugins/vis_types/xy/public/components/xy_settings.tsx @@ -71,7 +71,6 @@ function getValueLabelsStyling() { return { displayValue: { fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, - fill: { textInverted: false, textContrast: true }, alignment: { horizontal: HorizontalAlignment.Center, vertical: VerticalAlignment.Middle }, }, }; diff --git a/src/plugins/vis_types/xy/public/config/get_axis.ts b/src/plugins/vis_types/xy/public/config/get_axis.ts index b5cc96830e46a..09495725296cd 100644 --- a/src/plugins/vis_types/xy/public/config/get_axis.ts +++ b/src/plugins/vis_types/xy/public/config/get_axis.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { identity, isNil } from 'lodash'; +import { identity } from 'lodash'; import { AxisSpec, TickFormatter, YDomainRange, ScaleType as ECScaleType } from '@elastic/charts'; @@ -171,17 +171,5 @@ function getAxisDomain( const fit = defaultYExtents; const padding = boundsMargin || undefined; - if (!isNil(min) && !isNil(max)) { - return { fit, padding, min, max }; - } - - if (!isNil(min)) { - return { fit, padding, min }; - } - - if (!isNil(max)) { - return { fit, padding, max }; - } - - return { fit, padding }; + return { fit, padding, min: min ?? NaN, max: max ?? NaN }; } diff --git a/src/plugins/vis_types/xy/public/utils/domain.ts b/src/plugins/vis_types/xy/public/utils/domain.ts index fa8dd74e3942a..5b1310863979a 100644 --- a/src/plugins/vis_types/xy/public/utils/domain.ts +++ b/src/plugins/vis_types/xy/public/utils/domain.ts @@ -33,6 +33,8 @@ export const getXDomain = (params: Aspect['params']): DomainRange => { return { minInterval, + min: NaN, + max: NaN, }; }; @@ -74,9 +76,9 @@ export const getAdjustedDomain = ( }; } - return 'interval' in params - ? { - minInterval: params.interval, - } - : {}; + return { + minInterval: 'interval' in params ? params.interval : undefined, + min: NaN, + max: NaN, + }; }; diff --git a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts index 5fe1b03dd8b93..c14e313b1e7a4 100644 --- a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts @@ -112,7 +112,10 @@ export const getVisConfig = (): VisConfig => { mode: AxisMode.Normal, type: 'linear', }, - domain: {}, + domain: { + min: NaN, + max: NaN, + }, integersOnly: false, }, ], @@ -246,7 +249,10 @@ export const getVisConfigMutipleYaxis = (): VisConfig => { mode: AxisMode.Normal, type: 'linear', }, - domain: {}, + domain: { + min: NaN, + max: NaN, + }, integersOnly: false, }, ], @@ -435,7 +441,10 @@ export const getVisConfigPercentiles = (): VisConfig => { mode: AxisMode.Normal, type: 'linear', }, - domain: {}, + domain: { + min: NaN, + max: NaN, + }, integersOnly: false, }, ], diff --git a/src/plugins/vis_types/xy/public/vis_component.tsx b/src/plugins/vis_types/xy/public/vis_component.tsx index f4d566f49602e..515ad3e7eaf6f 100644 --- a/src/plugins/vis_types/xy/public/vis_component.tsx +++ b/src/plugins/vis_types/xy/public/vis_component.tsx @@ -19,6 +19,7 @@ import { ScaleType, AccessorFn, Accessor, + XYBrushEvent, } from '@elastic/charts'; import { compact } from 'lodash'; @@ -131,7 +132,10 @@ const VisComponent = (props: VisComponentProps) => { ): BrushEndListener | undefined => { if (xAccessor !== null && isInterval) { return (brushArea) => { - const event = getBrushFromChartBrushEventFn(visData, xAccessor)(brushArea); + const event = getBrushFromChartBrushEventFn( + visData, + xAccessor + )(brushArea as XYBrushEvent); props.fireEvent(event); }; } diff --git a/test/functional/apps/dashboard/dashboard_state.ts b/test/functional/apps/dashboard/dashboard_state.ts index 45ba62749dd77..0cc0fa4806482 100644 --- a/test/functional/apps/dashboard/dashboard_state.ts +++ b/test/functional/apps/dashboard/dashboard_state.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import chroma from 'chroma-js'; import { PIE_CHART_VIS_NAME, AREA_CHART_VIS_NAME } from '../../page_objects/dashboard_page'; import { DEFAULT_PANEL_WIDTH } from '../../../../src/plugins/dashboard/public/application/embeddable/dashboard_constants'; @@ -264,14 +265,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.header.waitUntilLoadingHasFinished(); await retry.try(async () => { - const allPieSlicesColor = await pieChart.getAllPieSliceStyles('80,000'); - let whitePieSliceCounts = 0; - allPieSlicesColor.forEach((style) => { - if (style.indexOf('rgb(255, 255, 255)') > -1) { - whitePieSliceCounts++; - } - }); - + const allPieSlicesColor = await pieChart.getAllPieSliceColor('80,000'); + const whitePieSliceCounts = allPieSlicesColor.reduce((count, color) => { + // converting the color to a common format, testing the color, not the string format + return chroma(color).hex().toUpperCase() === '#FFFFFF' ? count + 1 : count; + }, 0); expect(whitePieSliceCounts).to.be(1); }); }); diff --git a/test/functional/page_objects/visualize_chart_page.ts b/test/functional/page_objects/visualize_chart_page.ts index d2e4091f93577..b0e9e21d07b0b 100644 --- a/test/functional/page_objects/visualize_chart_page.ts +++ b/test/functional/page_objects/visualize_chart_page.ts @@ -7,7 +7,7 @@ */ import { Position } from '@elastic/charts'; -import Color from 'color'; +import chroma from 'chroma-js'; import { FtrService } from '../ftr_provider_context'; @@ -181,17 +181,17 @@ export class VisualizeChartPageObject extends FtrService { return items.some(({ color: c }) => c === color); } - public async doesSelectedLegendColorExistForPie(color: string) { + public async doesSelectedLegendColorExistForPie(matchingColor: string) { if (await this.isNewLibraryChart(pieChartSelector)) { + const hexMatchingColor = chroma(matchingColor).hex().toUpperCase(); const slices = (await this.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? []; - return slices.some(({ color: c }) => { - const rgbColor = new Color(color).rgb().toString(); - return c === rgbColor; + return slices.some(({ color }) => { + return hexMatchingColor === chroma(color).hex().toUpperCase(); }); } - return await this.testSubjects.exists(`legendSelectedColor-${color}`); + return await this.testSubjects.exists(`legendSelectedColor-${matchingColor}`); } public async expectError() { diff --git a/test/functional/services/visualizations/pie_chart.ts b/test/functional/services/visualizations/pie_chart.ts index 7c925318f0211..ff0c24e2830cf 100644 --- a/test/functional/services/visualizations/pie_chart.ts +++ b/test/functional/services/visualizations/pie_chart.ts @@ -7,6 +7,7 @@ */ import expect from '@kbn/expect'; +import { isNil } from 'lodash'; import { FtrService } from '../../ftr_provider_context'; const pieChartSelector = 'visTypePieChart'; @@ -100,8 +101,8 @@ export class PieChartService extends FtrService { return await pieSlice.getAttribute('style'); } - async getAllPieSliceStyles(name: string) { - this.log.debug(`VisualizePage.getAllPieSliceStyles(${name})`); + async getAllPieSliceColor(name: string) { + this.log.debug(`VisualizePage.getAllPieSliceColor(${name})`); if (await this.visChart.isNewLibraryChart(pieChartSelector)) { const slices = (await this.visChart.getEsChartDebugState(pieChartSelector))?.partition?.[0]?.partitions ?? @@ -112,9 +113,22 @@ export class PieChartService extends FtrService { return selectedSlice.map((slice) => slice.color); } const pieSlices = await this.getAllPieSlices(name); - return await Promise.all( + const slicesStyles = await Promise.all( pieSlices.map(async (pieSlice) => await pieSlice.getAttribute('style')) ); + return slicesStyles + .map( + (styles) => + styles.split(';').reduce>((styleAsObj, style) => { + const stylePair = style.split(':'); + if (stylePair.length !== 2) { + return styleAsObj; + } + styleAsObj[stylePair[0].trim()] = stylePair[1].trim(); + return styleAsObj; + }, {}).fill // in vislib the color is available on the `fill` style prop + ) + .filter((d) => !isNil(d)); } async getPieChartData() { diff --git a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx index 8a54c76df0f69..ee6a58b0dbb76 100644 --- a/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/chart_preview/index.tsx @@ -96,7 +96,7 @@ export function ChartPreview({ position={Position.Left} tickFormat={yTickFormat} ticks={5} - domain={{ max: yMax }} + domain={{ max: yMax, min: NaN }} /> { + const onBrushEnd = ({ x }: XYBrushEvent) => { if (!x) { return; } @@ -99,7 +100,7 @@ export function PageLoadDistChart({ diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 97f38a8123a4e..efd01c32b2462 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -6,7 +6,7 @@ */ import React, { useEffect } from 'react'; -import { BrushEndListener, XYBrushArea } from '@elastic/charts'; +import { BrushEndListener, XYBrushEvent } from '@elastic/charts'; import { EuiBadge, EuiFlexGroup, @@ -61,7 +61,7 @@ export function getFormattedSelection(selection: Selection): string { } interface TransactionDistributionProps { - onChartSelection: BrushEndListener; + onChartSelection: (event: XYBrushEvent) => void; onClearSelection: () => void; selection?: Selection; traceSamples: TabContentProps['traceSamples']; @@ -126,10 +126,8 @@ export function TransactionDistribution({ const trackApmEvent = useUiTracker({ app: 'apm' }); - const onTrackedChartSelection: BrushEndListener = ( - brushArea: XYBrushArea - ) => { - onChartSelection(brushArea); + const onTrackedChartSelection = (brushEvent: XYBrushEvent) => { + onChartSelection(brushEvent); trackApmEvent({ metric: 'transaction_distribution_chart_selection' }); }; @@ -216,7 +214,7 @@ export function TransactionDistribution({ markerCurrentTransaction={markerCurrentTransaction} markerPercentile={DEFAULT_PERCENTILE_THRESHOLD} markerValue={response.percentileThresholdValue ?? 0} - onChartSelection={onTrackedChartSelection} + onChartSelection={onTrackedChartSelection as BrushEndListener} hasData={hasData} selection={selection} status={status} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx index b249161980586..9ccca9886e679 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/transaction_details_tabs.tsx @@ -10,7 +10,7 @@ import React, { useCallback, useEffect, useState } from 'react'; import { omit } from 'lodash'; import { useHistory } from 'react-router-dom'; -import { XYBrushArea } from '@elastic/charts'; +import { XYBrushEvent } from '@elastic/charts'; import { EuiPanel, EuiSpacer, EuiTabs, EuiTab } from '@elastic/eui'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; @@ -48,7 +48,7 @@ export function TransactionDetailsTabs() { environment, }); - const selectSampleFromChartSelection = (selection: XYBrushArea) => { + const selectSampleFromChartSelection = (selection: XYBrushEvent) => { if (selection !== undefined) { const { x } = selection; if (Array.isArray(x)) { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts index 1ccb3d01a9b28..c3d2b9648e82a 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/types.ts +++ b/x-pack/plugins/apm/public/components/app/transaction_details/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { XYBrushArea } from '@elastic/charts'; +import { XYBrushEvent } from '@elastic/charts'; import type { TraceSample } from '../../../hooks/use_transaction_trace_samples_fetcher'; @@ -14,6 +14,6 @@ export interface TabContentProps { onFilter: () => void; sampleRangeFrom?: number; sampleRangeTo?: number; - selectSampleFromChartSelection: (selection: XYBrushArea) => void; + selectSampleFromChartSelection: (selection: XYBrushEvent) => void; traceSamples: TraceSample[]; } diff --git a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx index 213bac40c2248..16157071affcd 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/breakdown_chart/index.tsx @@ -17,6 +17,7 @@ import { ScaleType, Settings, TickFormatter, + XYBrushEvent, } from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -96,7 +97,9 @@ export function BreakdownChart({ onBrushEnd({ x, history })} + onBrushEnd={(event) => + onBrushEnd({ x: (event as XYBrushEvent).x, history }) + } showLegend showLegendExtra legendPosition={Position.Bottom} diff --git a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts index d94f2ce8f5c5d..9dccddd509387 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/charts/helper/helper.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { XYBrushArea } from '@elastic/charts'; +import { XYBrushEvent } from '@elastic/charts'; import { History } from 'history'; import { Coordinate, TimeSeries } from '../../../../../typings/timeseries'; import { fromQuery, toQuery } from '../../Links/url_helpers'; @@ -14,7 +14,7 @@ export const onBrushEnd = ({ x, history, }: { - x: XYBrushArea['x']; + x: XYBrushEvent['x']; history: History; }) => { if (x) { diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx index 65ecdec0f36a5..08e8908d50e7a 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart.tsx @@ -20,6 +20,7 @@ import { ScaleType, Settings, YDomainRange, + XYBrushEvent, } from '@elastic/charts'; import { EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -115,7 +116,9 @@ export function TimeseriesChart({ onBrushEnd({ x, history })} + onBrushEnd={(event) => + onBrushEnd({ x: (event as XYBrushEvent).x, history }) + } theme={{ ...chartTheme, areaSeriesStyle: { diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx index b8df4defa18a2..6459fc4006cea 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -20,6 +20,7 @@ import { ScaleType, Settings, XYChartElementEvent, + XYBrushEvent, } from '@elastic/charts'; import moment from 'moment'; import { useDataVisualizerKibana } from '../../../../kibana_context'; @@ -91,7 +92,7 @@ export const DocumentCountChart: FC = ({ [data] ); - const onBrushEnd: BrushEndListener = ({ x }) => { + const onBrushEnd = ({ x }: XYBrushEvent) => { if (!x) { return; } @@ -117,7 +118,11 @@ export const DocumentCountChart: FC = ({ height: 120, }} > - + = ({ }; const config: HeatmapSpec['config'] = { - onBrushEnd, grid: { stroke: { width: @@ -338,6 +338,7 @@ export const HeatmapComponent: FC = ({ labelOptions: { maxLines: args.legend.shouldTruncate ? args.legend?.maxLines ?? 1 : 0 }, }, }} + onBrushEnd={onBrushEnd as BrushEndListener} /> = { - textInvertible: true, valueFont: { fontWeight: 700, }, diff --git a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap index fe3137c905ffb..0fad522624975 100644 --- a/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap +++ b/x-pack/plugins/lens/public/xy_visualization/__snapshots__/expression.test.tsx.snap @@ -73,8 +73,8 @@ exports[`xy_expression XYChart component it renders area 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -302,8 +302,8 @@ exports[`xy_expression XYChart component it renders bar 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -545,8 +545,8 @@ exports[`xy_expression XYChart component it renders horizontal bar 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -788,8 +788,8 @@ exports[`xy_expression XYChart component it renders line 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -1017,8 +1017,8 @@ exports[`xy_expression XYChart component it renders stacked area 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -1254,8 +1254,8 @@ exports[`xy_expression XYChart component it renders stacked bar 1`] = ` domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ @@ -1505,8 +1505,8 @@ exports[`xy_expression XYChart component it renders stacked horizontal bar 1`] = domain={ Object { "fit": false, - "max": undefined, - "min": undefined, + "max": NaN, + "min": NaN, } } gridLine={ diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index 4056aa730c2ab..af2995fb65b71 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -809,8 +809,8 @@ describe('xy_expression', () => { ); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ fit: true, - min: undefined, - max: undefined, + min: NaN, + max: NaN, }); }); @@ -838,6 +838,8 @@ describe('xy_expression', () => { ); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ fit: false, + min: NaN, + max: NaN, }); }); @@ -867,8 +869,8 @@ describe('xy_expression', () => { ); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ fit: false, - min: undefined, - max: undefined, + min: NaN, + max: NaN, }); }); @@ -959,7 +961,11 @@ describe('xy_expression', () => { }} /> ); - expect(component.find(Settings).prop('xDomain')).toEqual({ minInterval: 101 }); + expect(component.find(Settings).prop('xDomain')).toEqual({ + minInterval: 101, + min: NaN, + max: NaN, + }); }); test('disabled legend extra by default', () => { diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 5dfad58f50018..87462e71f3cf6 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -25,9 +25,12 @@ import { LayoutDirection, ElementClickListener, BrushEndListener, + XYBrushEvent, CurveType, LegendPositionConfig, LabelOverflowConstraint, + DisplayValueStyle, + RecursivePartial, } from '@elastic/charts'; import { I18nProvider } from '@kbn/i18n/react'; import type { @@ -169,7 +172,9 @@ export const getXyChartRenderer = (dependencies: { }, }); -function getValueLabelsStyling(isHorizontal: boolean) { +function getValueLabelsStyling(isHorizontal: boolean): { + displayValue: RecursivePartial; +} { const VALUE_LABELS_MAX_FONTSIZE = 12; const VALUE_LABELS_MIN_FONTSIZE = 10; const VALUE_LABELS_VERTICAL_OFFSET = -10; @@ -178,11 +183,9 @@ function getValueLabelsStyling(isHorizontal: boolean) { return { displayValue: { fontSize: { min: VALUE_LABELS_MIN_FONTSIZE, max: VALUE_LABELS_MAX_FONTSIZE }, - fill: { textContrast: true, textInverted: false, textBorder: 0 }, + fill: { textBorder: 0 }, alignment: isHorizontal - ? { - vertical: VerticalAlignment.Middle, - } + ? { vertical: VerticalAlignment.Middle } : { horizontal: HorizontalAlignment.Center }, offsetX: isHorizontal ? VALUE_LABELS_HORIZONTAL_OFFSET : 0, offsetY: isHorizontal ? 0 : VALUE_LABELS_VERTICAL_OFFSET, @@ -388,14 +391,14 @@ export function XYChart({ }) ); const fit = !hasBarOrArea && extent.mode === 'dataBounds'; - let min: undefined | number; - let max: undefined | number; + let min: number = NaN; + let max: number = NaN; if (extent.mode === 'custom') { const { inclusiveZeroError, boundaryError } = validateExtent(hasBarOrArea, extent); if (!inclusiveZeroError && !boundaryError) { - min = extent.lowerBound; - max = extent.upperBound; + min = extent.lowerBound ?? NaN; + max = extent.upperBound ?? NaN; } } else { const axisHasThreshold = thresholdLayers.some(({ yConfig }) => @@ -517,7 +520,7 @@ export function XYChart({ onClickValue(context); }; - const brushHandler: BrushEndListener = ({ x }) => { + const brushHandler = ({ x }: XYBrushEvent) => { if (!x) { return; } @@ -592,7 +595,7 @@ export function XYChart({ allowBrushingLastHistogramBucket={Boolean(isTimeViz)} rotation={shouldRotate ? 90 : 0} xDomain={xDomain} - onBrushEnd={interactive ? brushHandler : undefined} + onBrushEnd={interactive ? (brushHandler as BrushEndListener) : undefined} onElementClick={interactive ? clickHandler : undefined} legendAction={getLegendAction( filteredLayers, diff --git a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx index ccb047d54e369..d5eb8ac1e92ba 100644 --- a/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/x_domain.tsx @@ -26,12 +26,12 @@ export const getXDomain = ( ) => { const baseDomain = isTimeViz ? { - min: data.dateRange?.fromDate.getTime(), - max: data.dateRange?.toDate.getTime(), + min: data.dateRange?.fromDate.getTime() ?? NaN, + max: data.dateRange?.toDate.getTime() ?? NaN, minInterval, } : isHistogram - ? { minInterval } + ? { minInterval, min: NaN, max: NaN } : undefined; if (isHistogram && isFullyQualified(baseDomain)) { diff --git a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx index b4015d0c0eb92..1fae57c7922ce 100644 --- a/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx +++ b/x-pack/plugins/ml/public/application/components/model_snapshots/revert_model_snapshot_flyout/create_calendar.tsx @@ -9,7 +9,7 @@ import React, { FC, Fragment, useCallback, memo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import moment from 'moment'; -import { XYBrushArea } from '@elastic/charts'; +import { XYBrushEvent, BrushEndListener } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, @@ -57,7 +57,7 @@ export const CreateCalendar: FC = ({ const { euiTheme } = useCurrentEuiTheme(); const onBrushEnd = useCallback( - ({ x }: XYBrushArea) => { + ({ x }: XYBrushEvent) => { if (x && x.length === 2) { const end = x[1] < minSelectableTimeStamp ? null : x[1]; if (end !== null) { @@ -252,7 +252,7 @@ interface ChartProps { eventRateData: LineChartPoint[]; anomalies: Anomaly[]; loading: boolean; - onBrushEnd(area: XYBrushArea): void; + onBrushEnd(area: XYBrushEvent): void; overlayRanges: Array<{ start: number; end: number }>; overlayColor: string; } @@ -272,7 +272,7 @@ const Chart: FC = memo( color: overlayColor, showMarker: false, }))} - onBrushEnd={onBrushEnd} + onBrushEnd={onBrushEnd as BrushEndListener} /> ), (prev: ChartProps, next: ChartProps) => { diff --git a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx index 49bd00d888cf8..ef8e80381293e 100644 --- a/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx +++ b/x-pack/plugins/ml/public/application/explorer/swimlane_container.tsx @@ -18,6 +18,7 @@ import { import { throttle } from 'lodash'; import { Chart, + BrushEndListener, Settings, Heatmap, HeatmapElementEvent, @@ -286,16 +287,6 @@ export const SwimlaneContainer: FC = ({ if (!showSwimlane) return {}; const config: HeatmapSpec['config'] = { - onBrushEnd: (e: HeatmapBrushEvent) => { - if (!e.cells.length) return; - - onCellsSelection({ - lanes: e.y as string[], - times: e.x.map((v) => (v as number) / 1000) as [number, number], - type: swimlaneType, - viewByFieldName: swimlaneData.fieldName, - }); - }, grid: { cellHeight: { min: CELL_HEIGHT, @@ -396,6 +387,17 @@ export const SwimlaneContainer: FC = ({ [swimlaneData] ); + const onBrushEnd = (e: HeatmapBrushEvent) => { + if (!e.cells.length) return; + + onCellsSelection({ + lanes: e.y as string[], + times: e.x!.map((v) => (v as number) / 1000) as [number, number], + type: swimlaneType, + viewByFieldName: swimlaneData.fieldName, + }); + }; + // A resize observer is required to compute the bucket span based on the chart width to fetch the data accordingly return ( @@ -427,6 +429,7 @@ export const SwimlaneContainer: FC = ({ xDomain={xDomain} tooltip={tooltipOptions} debugState={window._echDebugStateFlag ?? false} + onBrushEnd={onBrushEnd as BrushEndListener} /> = ({ {showAxis === true && } - {onBrushEnd === undefined ? ( - - ) : ( - - )} + {overlayRanges && overlayRanges.map((range, i) => ( diff --git a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx index 7a42e96c3823d..8df14129623f6 100644 --- a/x-pack/plugins/observability/public/components/app/section/apm/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/apm/index.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { + Axis, + BarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + XYBrushEvent, +} from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; @@ -117,7 +125,7 @@ export function APMSection({ bucketSize }: Props) { onBrushEnd({ x, history })} + onBrushEnd={(event) => onBrushEnd({ x: (event as XYBrushEvent).x, history })} theme={chartTheme} showLegend={false} xDomain={{ min, max }} diff --git a/x-pack/plugins/observability/public/components/app/section/helper.ts b/x-pack/plugins/observability/public/components/app/section/helper.ts index f1b2992386063..077bd67a8590c 100644 --- a/x-pack/plugins/observability/public/components/app/section/helper.ts +++ b/x-pack/plugins/observability/public/components/app/section/helper.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { XYBrushArea } from '@elastic/charts'; +import { XYBrushEvent } from '@elastic/charts'; import { History } from 'history'; import { fromQuery, toQuery } from '../../../utils/url'; -export const onBrushEnd = ({ x, history }: { x: XYBrushArea['x']; history: History }) => { +export const onBrushEnd = ({ x, history }: { x: XYBrushEvent['x']; history: History }) => { if (x) { const start = x[0]; const end = x[1]; diff --git a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx index da5a8f25045a5..dcd51a531a73d 100644 --- a/x-pack/plugins/observability/public/components/app/section/logs/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/logs/index.tsx @@ -5,7 +5,15 @@ * 2.0. */ -import { Axis, BarSeries, niceTimeFormatter, Position, ScaleType, Settings } from '@elastic/charts'; +import { + Axis, + BarSeries, + niceTimeFormatter, + Position, + ScaleType, + Settings, + XYBrushEvent, +} from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem, euiPaletteColorBlind, EuiSpacer, EuiTitle } from '@elastic/eui'; import numeral from '@elastic/numeral'; import { i18n } from '@kbn/i18n'; @@ -124,7 +132,7 @@ export function LogsSection({ bucketSize }: Props) { onBrushEnd({ x, history })} + onBrushEnd={(event) => onBrushEnd({ x: (event as XYBrushEvent).x, history })} theme={chartTheme} showLegend legendPosition={Position.Right} diff --git a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx index 28cbd12663c1b..8c0f1f8db7c1a 100644 --- a/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx +++ b/x-pack/plugins/observability/public/components/app/section/uptime/index.tsx @@ -13,6 +13,7 @@ import { ScaleType, Settings, TickFormatter, + XYBrushEvent, } from '@elastic/charts'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import numeral from '@elastic/numeral'; @@ -123,7 +124,7 @@ export function UptimeSection({ bucketSize }: Props) { {/* Chart section */} onBrushEnd({ x, history })} + onBrushEnd={(event) => onBrushEnd({ x: (event as XYBrushEvent).x, history })} theme={chartTheme} showLegend={false} legendPosition={Position.Right} diff --git a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx index 1db09d0492e68..68141945d73fd 100644 --- a/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx +++ b/x-pack/plugins/stack_alerts/public/alert_types/threshold/visualization.tsx @@ -277,7 +277,12 @@ export const ThresholdVisualization: React.FunctionComponent = ({ showOverlappingTicks={true} tickFormat={dateFormatter} /> - + {alertVisualizationDataKeys.map((key: string) => { return ( getTickFormat(d)} diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index 468e9dfa68e1b..c995e4a449867 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -227,7 +227,12 @@ export const WatchVisualization = () => { showOverlappingTicks={true} tickFormat={dateFormatter} /> - + {watchVisualizationDataKeys.map((key: string) => { return ( Date: Tue, 12 Oct 2021 16:36:38 +0200 Subject: [PATCH 067/287] [Fleet] Fix agent count in update modal (#114622) --- .../screens/detail/settings/update_button.tsx | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx index 8cdb3ece30621..48569d782a70b 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/update_button.tsx @@ -18,12 +18,12 @@ import { EuiConfirmModal, EuiSpacer, } from '@elastic/eui'; -import { sumBy } from 'lodash'; import type { GetAgentPoliciesResponse, PackageInfo, UpgradePackagePolicyDryRunResponse, + PackagePolicy, } from '../../../../../types'; import { InstallStatus } from '../../../../../types'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../constants'; @@ -109,10 +109,24 @@ export const UpdateButton: React.FunctionComponent = ({ }, [packagePolicyIds]); const packagePolicyCount = useMemo(() => packagePolicyIds.length, [packagePolicyIds]); + + function isStringArray(arr: unknown | string[]): arr is string[] { + return Array.isArray(arr) && arr.every((p) => typeof p === 'string'); + } + const agentCount = useMemo( - () => sumBy(agentPolicyData?.items, ({ agents }) => agents ?? 0), - [agentPolicyData] + () => + agentPolicyData?.items.reduce((acc, item) => { + const existingPolicies = isStringArray(item?.package_policies) + ? (item?.package_policies as string[]).filter((p) => packagePolicyIds.includes(p)) + : (item?.package_policies as PackagePolicy[]).filter((p) => + packagePolicyIds.includes(p.id) + ); + return (acc += existingPolicies.length > 0 && item?.agents ? item?.agents : 0); + }, 0), + [agentPolicyData, packagePolicyIds] ); + const conflictCount = useMemo( () => dryRunData?.filter((item) => item.hasErrors).length, [dryRunData] From 0b46bb1b93e01137e0c9cd957fa197ae024d7ea0 Mon Sep 17 00:00:00 2001 From: Davis Plumlee <56367316+dplumlee@users.noreply.github.com> Date: Tue, 12 Oct 2021 09:40:54 -0500 Subject: [PATCH 068/287] [Security Solution][Detection Alerts] Fixes follow-up alert refresh bugs (#112169) --- .../detection_alerts/acknowledged.spec.ts | 11 +- .../detection_alerts/closing.spec.ts | 69 +- .../detection_alerts/opening.spec.ts | 15 +- .../cypress/screens/alerts.ts | 6 + .../timeline_actions/alert_context_menu.tsx | 7 +- .../components/take_action_dropdown/index.tsx | 43 +- .../__snapshots__/index.test.tsx.snap | 788 +++++++++--------- .../side_panel/event_details/footer.tsx | 46 +- .../public/container/use_update_alerts.ts | 8 +- .../hooks/use_status_bulk_action_items.tsx | 2 +- 10 files changed, 549 insertions(+), 446 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index 5d72105178b69..2dad11ac7e937 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -6,7 +6,11 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, +} from '../../screens/alerts'; import { selectNumberOfAlerts, @@ -50,11 +54,16 @@ describe('Marking alerts as acknowledged', () => { markAcknowledgedFirstAlert(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedAcknowledged; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${expectedNumberOfAlerts}`); goToAcknowledgedAlerts(); waitForAlerts(); cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeMarkedAcknowledged} alert`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfAlertsToBeMarkedAcknowledged}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts index 602619b056244..860a4e6089a27 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/closing.spec.ts @@ -6,7 +6,13 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, SELECTED_ALERTS, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + SELECTED_ALERTS, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, + ALERTS_TREND_SIGNAL_RULE_NAME_PANEL, +} from '../../screens/alerts'; import { closeFirstAlert, @@ -46,6 +52,7 @@ describe('Closing alerts', () => { .then((alertNumberString) => { const numberOfAlerts = alertNumberString.split(' ')[0]; cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${numberOfAlerts}`); selectNumberOfAlerts(numberOfAlertsToBeClosed); @@ -56,6 +63,10 @@ describe('Closing alerts', () => { const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlertsAfterClosing} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlertsAfterClosing}` + ); goToClosedAlerts(); waitForAlerts(); @@ -75,6 +86,10 @@ describe('Closing alerts', () => { 'have.text', `${expectedNumberOfClosedAlertsAfterOpened} alerts` ); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfClosedAlertsAfterOpened}` + ); goToOpenedAlerts(); waitForAlerts(); @@ -83,6 +98,10 @@ describe('Closing alerts', () => { +numberOfAlerts - expectedNumberOfClosedAlertsAfterOpened; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfOpenedAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfOpenedAlerts}` + ); }); }); @@ -103,11 +122,59 @@ describe('Closing alerts', () => { const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeClosed; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${expectedNumberOfAlerts}`); + + goToClosedAlerts(); + waitForAlerts(); + + cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeClosed} alert`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfAlertsToBeClosed}` + ); + }); + }); + + it('Updates trend histogram whenever alert status is updated in table', () => { + const numberOfAlertsToBeClosed = 1; + cy.get(ALERTS_COUNT) + .invoke('text') + .then((alertNumberString) => { + const numberOfAlerts = alertNumberString.split(' ')[0]; + cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('have.text', `${numberOfAlerts}`); + + selectNumberOfAlerts(numberOfAlertsToBeClosed); + + cy.get(SELECTED_ALERTS).should('have.text', `Selected ${numberOfAlertsToBeClosed} alert`); + + closeAlerts(); + waitForAlerts(); + + const expectedNumberOfAlertsAfterClosing = +numberOfAlerts - numberOfAlertsToBeClosed; + cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlertsAfterClosing} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlertsAfterClosing}` + ); goToClosedAlerts(); waitForAlerts(); cy.get(ALERTS_COUNT).should('have.text', `${numberOfAlertsToBeClosed} alert`); + + const numberOfAlertsToBeOpened = 1; + selectNumberOfAlerts(numberOfAlertsToBeOpened); + + cy.get(SELECTED_ALERTS).should('have.text', `Selected ${numberOfAlertsToBeOpened} alert`); + cy.get(ALERTS_TREND_SIGNAL_RULE_NAME_PANEL).should('exist'); + + openAlerts(); + waitForAlerts(); + + cy.get(ALERTS_COUNT).should('not.exist'); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should('not.exist'); + cy.get(ALERTS_TREND_SIGNAL_RULE_NAME_PANEL).should('not.exist'); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts index 645abfed8ac0e..87cef27b5b346 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/opening.spec.ts @@ -6,7 +6,12 @@ */ import { getNewRule } from '../../objects/rule'; -import { ALERTS_COUNT, SELECTED_ALERTS, TAKE_ACTION_POPOVER_BTN } from '../../screens/alerts'; +import { + ALERTS_COUNT, + SELECTED_ALERTS, + TAKE_ACTION_POPOVER_BTN, + ALERT_COUNT_TABLE_FIRST_ROW_COUNT, +} from '../../screens/alerts'; import { closeAlerts, @@ -74,6 +79,10 @@ describe('Opening alerts', () => { const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeOpened; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${expectedNumberOfAlerts}` + ); goToOpenedAlerts(); waitForAlerts(); @@ -82,6 +91,10 @@ describe('Opening alerts', () => { 'have.text', `${numberOfOpenedAlerts + numberOfAlertsToBeOpened} alerts`.toString() ); + cy.get(ALERT_COUNT_TABLE_FIRST_ROW_COUNT).should( + 'have.text', + `${numberOfOpenedAlerts + numberOfAlertsToBeOpened}` + ); }); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 675a25641a2bd..d18a8e1ba10ab 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -70,3 +70,9 @@ export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActions export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]'; + +export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT = + '[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text'; + +export const ALERTS_TREND_SIGNAL_RULE_NAME_PANEL = + '[data-test-subj="render-content-signal.rule.name"]'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index fc8dd4b024fd9..06d61b3f0b284 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -12,6 +12,7 @@ import { indexOf } from 'lodash'; import { connect, ConnectedProps } from 'react-redux'; import { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { get } from 'lodash/fp'; +import { useRouteSpy } from '../../../../common/utils/route/use_route_spy'; import { buildGetAlertByIdQuery } from '../../../../common/components/exceptions/helpers'; import { EventsTdContent } from '../../../../timelines/components/timeline/styles'; import { DEFAULT_ICON_BUTTON_WIDTH } from '../../../../timelines/components/timeline/helpers'; @@ -63,6 +64,7 @@ const AlertContextMenuComponent: React.FC { const [isPopoverOpen, setPopover] = useState(false); + const [routeProps] = useRouteSpy(); const afterItemSelection = useCallback(() => { setPopover(false); @@ -112,10 +114,13 @@ const AlertContextMenuComponent: React.FC { if (timelineId === TimelineId.active) { refetchQuery([timelineQuery]); + if (routeProps.pageName === 'alerts') { + refetchQuery(globalQuery); + } } else { refetchQuery(globalQuery); } - }, [timelineId, globalQuery, timelineQuery]); + }, [timelineId, globalQuery, timelineQuery, routeProps]); const { exceptionModalType, diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 200b21bbecc4b..f7d65d1a3f3f4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -9,8 +9,7 @@ import React, { useState, useCallback, useMemo } from 'react'; import { EuiContextMenuPanel, EuiButton, EuiPopover } from '@elastic/eui'; import type { ExceptionListType } from '@kbn/securitysolution-io-ts-list-types'; import { isEmpty } from 'lodash/fp'; -import { connect, ConnectedProps } from 'react-redux'; -import { TimelineEventsDetailsItem, TimelineId } from '../../../../common'; +import { TimelineEventsDetailsItem } from '../../../../common'; import { TAKE_ACTION } from '../alerts_table/alerts_utility_bar/translations'; import { useExceptionActions } from '../alerts_table/timeline_actions/use_add_exception_actions'; import { useAlertsActions } from '../alerts_table/timeline_actions/use_alerts_actions'; @@ -24,8 +23,6 @@ import { Status } from '../../../../common/detection_engine/schemas/common/schem import { isAlertFromEndpointAlert } from '../../../common/utils/endpoint_alert_check'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; import { useAddToCaseActions } from '../alerts_table/timeline_actions/use_add_to_case_actions'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; - interface ActionsData { alertStatus: Status; eventId: string; @@ -48,7 +45,7 @@ export interface TakeActionDropdownProps { timelineId: string; } -export const TakeActionDropdownComponent = React.memo( +export const TakeActionDropdown = React.memo( ({ detailsData, ecsData, @@ -61,9 +58,7 @@ export const TakeActionDropdownComponent = React.memo( onAddIsolationStatusClick, refetch, timelineId, - globalQuery, - timelineQuery, - }: TakeActionDropdownProps & PropsFromRedux) => { + }: TakeActionDropdownProps) => { const tGridEnabled = useIsExperimentalFeatureEnabled('tGridEnabled'); const [isPopoverOpen, setIsPopoverOpen] = useState(false); @@ -146,24 +141,12 @@ export const TakeActionDropdownComponent = React.memo( closePopoverHandler(); }, [closePopoverHandler]); - const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { - newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); - }; - - const refetchAll = useCallback(() => { - if (timelineId === TimelineId.active) { - refetchQuery([timelineQuery]); - } else { - refetchQuery(globalQuery); - } - }, [timelineId, globalQuery, timelineQuery]); - const { actionItems: statusActionItems } = useAlertsActions({ alertStatus: actionsData.alertStatus, closePopover: closePopoverAndFlyout, eventId: actionsData.eventId, indexName, - refetch: refetchAll, + refetch, timelineId, }); @@ -233,21 +216,3 @@ export const TakeActionDropdownComponent = React.memo( ) : null; } ); - -const makeMapStateToProps = () => { - const getGlobalQueries = inputsSelectors.globalQuery(); - const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); - const mapStateToProps = (state: State, { timelineId }: TakeActionDropdownProps) => { - return { - globalQuery: getGlobalQueries(state), - timelineQuery: getTimelineQuery(state, timelineId), - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const TakeActionDropdown = connector(React.memo(TakeActionDropdownComponent)); diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap index 137d8d78bcdaa..2bb9da12e44ea 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/__snapshots__/index.test.tsx.snap @@ -1056,7 +1056,7 @@ Array [
- - -
- + +
-
- -
- - -
-
-
- -
-
- +
+
+
+ +
+ + + , @@ -2093,7 +2095,7 @@ Array [ - - -
- -
- -
- + +
+ +
+ +
- -
-
-
-
-
-
- +
+
+
+
+
+
+
+ , ] diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx index 32c3f5a885346..4ddcd710e0406 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/footer.tsx @@ -5,11 +5,12 @@ * 2.0. */ -import React, { useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { EuiFlyoutFooter, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { find, get, isEmpty } from 'lodash/fp'; +import { connect, ConnectedProps } from 'react-redux'; import { TakeActionDropdown } from '../../../../detections/components/take_action_dropdown'; -import type { TimelineEventsDetailsItem } from '../../../../../common'; +import { TimelineEventsDetailsItem, TimelineId } from '../../../../../common'; import { useExceptionModal } from '../../../../detections/components/alerts_table/timeline_actions/use_add_exception_modal'; import { AddExceptionModalWrapper } from '../../../../detections/components/alerts_table/timeline_actions/alert_context_menu'; import { EventFiltersModal } from '../../../../management/pages/event_filters/view/components/modal'; @@ -18,6 +19,7 @@ import { getFieldValue } from '../../../../detections/components/host_isolation/ import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { Ecs } from '../../../../../common/ecs'; import { useFetchEcsAlertsData } from '../../../../detections/containers/detection_engine/alerts/use_fetch_ecs_alerts_data'; +import { inputsModel, inputsSelectors, State } from '../../../../common/store'; interface EventDetailsFooterProps { detailsData: TimelineEventsDetailsItem[] | null; @@ -41,7 +43,7 @@ interface AddExceptionModalWrapperData { ruleName: string; } -export const EventDetailsFooter = React.memo( +export const EventDetailsFooterComponent = React.memo( ({ detailsData, expandedEvent, @@ -50,7 +52,9 @@ export const EventDetailsFooter = React.memo( loadingEventDetails, onAddIsolationStatusClick, timelineId, - }: EventDetailsFooterProps) => { + globalQuery, + timelineQuery, + }: EventDetailsFooterProps & PropsFromRedux) => { const ruleIndex = useMemo( () => find({ category: 'signal', field: 'signal.rule.index' }, detailsData)?.values, [detailsData] @@ -78,6 +82,18 @@ export const EventDetailsFooter = React.memo( [expandedEvent?.eventId] ); + const refetchQuery = (newQueries: inputsModel.GlobalQuery[]) => { + newQueries.forEach((q) => q.refetch && (q.refetch as inputsModel.Refetch)()); + }; + + const refetchAll = useCallback(() => { + if (timelineId === TimelineId.active) { + refetchQuery([timelineQuery]); + } else { + refetchQuery(globalQuery); + } + }, [timelineId, globalQuery, timelineQuery]); + const { exceptionModalType, onAddExceptionTypeClick, @@ -86,7 +102,7 @@ export const EventDetailsFooter = React.memo( ruleIndices, } = useExceptionModal({ ruleIndex, - refetch: expandedEvent?.refetch, + refetch: refetchAll, timelineId, }); const { closeAddEventFilterModal, isAddEventFilterModalOpen, onAddEventFilterClick } = @@ -113,7 +129,7 @@ export const EventDetailsFooter = React.memo( onAddEventFilterClick={onAddEventFilterClick} onAddExceptionTypeClick={onAddExceptionTypeClick} onAddIsolationStatusClick={onAddIsolationStatusClick} - refetch={expandedEvent?.refetch} + refetch={refetchAll} indexName={expandedEvent.indexName} timelineId={timelineId} /> @@ -142,3 +158,21 @@ export const EventDetailsFooter = React.memo( ); } ); + +const makeMapStateToProps = () => { + const getGlobalQueries = inputsSelectors.globalQuery(); + const getTimelineQuery = inputsSelectors.timelineQueryByIdSelector(); + const mapStateToProps = (state: State, { timelineId }: EventDetailsFooterProps) => { + return { + globalQuery: getGlobalQueries(state), + timelineQuery: getTimelineQuery(state, timelineId), + }; + }; + return mapStateToProps; +}; + +const connector = connect(makeMapStateToProps); + +type PropsFromRedux = ConnectedProps; + +export const EventDetailsFooter = connector(React.memo(EventDetailsFooterComponent)); diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7f42ddc6e8211..b38c3b9a71fef 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -17,6 +17,8 @@ import { /** * Update alert status by query + * + * @param useDetectionEngine logic flag for using the regular Detection Engine URL or the RAC URL * * @param status to update to('open' / 'closed' / 'acknowledged') * @param index index to be updated @@ -26,7 +28,7 @@ import { * @throws An error if response is not OK */ export const useUpdateAlertsStatus = ( - timelineId: string + useDetectionEngine: boolean = false ): { updateAlertStatus: (params: { status: AlertStatus; @@ -37,7 +39,7 @@ export const useUpdateAlertsStatus = ( const { http } = useKibana().services; return { updateAlertStatus: async ({ status, index, query }) => { - if (['detections-page', 'detections-rules-details-page'].includes(timelineId)) { + if (useDetectionEngine) { return http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { method: 'POST', body: JSON.stringify({ status, query }), @@ -51,5 +53,3 @@ export const useUpdateAlertsStatus = ( }, }; }; - -// diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index c9269436646ea..c6e0e13c4dcb4 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -28,7 +28,7 @@ export const useStatusBulkActionItems = ({ onUpdateFailure, timelineId, }: StatusBulkActionsProps) => { - const { updateAlertStatus } = useUpdateAlertsStatus(timelineId ?? ''); + const { updateAlertStatus } = useUpdateAlertsStatus(timelineId != null); const { addSuccess, addError, addWarning } = useAppToasts(); const onAlertStatusUpdateSuccess = useCallback( From df971b7dc90b844c83d561faaa57730d8e9810d5 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 12 Oct 2021 16:46:23 +0200 Subject: [PATCH 069/287] [Exploratory view] Render content only on expand (#114237) --- .../shared/exploratory_view/series_editor/series.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx index 11f96afe7ceab..d320b84c6a684 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/series.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { EuiFlexItem, EuiFlexGroup, EuiPanel, EuiAccordion, EuiSpacer } from '@elastic/eui'; @@ -47,6 +47,14 @@ export function Series({ item, isExpanded, toggleExpanded }: Props) { seriesId: id, }; + const [isExapndedOnce, setIsExapndedOnce] = useState(false); + + useEffect(() => { + if (isExpanded) { + setIsExapndedOnce(true); + } + }, [isExpanded]); + return ( - + {isExapndedOnce && } From ba8abc41516d8778f11cfdd8e12580adc9d7a79f Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 12 Oct 2021 16:58:37 +0200 Subject: [PATCH 070/287] [Vega] Improve error message in case of invalid $schema URL (#114459) * :bug: Catch the schema parser and provide a better error message * :globe_with_meridians: Add i18n --- .../public/data_model/vega_parser.test.js | 14 +++++++ .../vega/public/data_model/vega_parser.ts | 40 ++++++++++++------- 2 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js index cfeed174307ac..13c17b8f4c38f 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.test.js @@ -81,6 +81,20 @@ describe(`VegaParser.parseAsync`, () => { }) ) ); + + test(`should return a specific error in case of $schema URL not valid`, async () => { + const vp = new VegaParser({ + $schema: 'https://vega.github.io/schema/vega-lite/v4.jsonanythingtobreakthis', + mark: 'circle', + encoding: { row: { field: 'a' } }, + }); + + await vp.parseAsync(); + + expect(vp.error).toBe( + 'The URL for the JSON "$schema" is incorrect. Correct the URL, then click Update.' + ); + }); }); describe(`VegaParser._setDefaultValue`, () => { diff --git a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts index 9000fed7f6116..bf2a6be25c71a 100644 --- a/src/plugins/vis_types/vega/public/data_model/vega_parser.ts +++ b/src/plugins/vis_types/vega/public/data_model/vega_parser.ts @@ -553,25 +553,37 @@ The URL is an identifier only. Kibana and your browser will never access this UR * @private */ private parseSchema(spec: VegaSpec) { - const schema = schemaParser(spec.$schema); - const isVegaLite = schema.library === 'vega-lite'; - const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; + try { + const schema = schemaParser(spec.$schema); + const isVegaLite = schema.library === 'vega-lite'; + const libVersion = isVegaLite ? vegaLiteVersion : vegaVersion; - if (versionCompare(schema.version, libVersion) > 0) { - this._onWarning( - i18n.translate('visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage', { + if (versionCompare(schema.version, libVersion) > 0) { + this._onWarning( + i18n.translate( + 'visTypeVega.vegaParser.notValidLibraryVersionForInputSpecWarningMessage', + { + defaultMessage: + 'The input spec uses {schemaLibrary} {schemaVersion}, but current version of {schemaLibrary} is {libraryVersion}.', + values: { + schemaLibrary: schema.library, + schemaVersion: schema.version, + libraryVersion: libVersion, + }, + } + ) + ); + } + + return { isVegaLite, libVersion }; + } catch (e) { + throw Error( + i18n.translate('visTypeVega.vegaParser.notValidSchemaForInputSpec', { defaultMessage: - 'The input spec uses {schemaLibrary} {schemaVersion}, but current version of {schemaLibrary} is {libraryVersion}.', - values: { - schemaLibrary: schema.library, - schemaVersion: schema.version, - libraryVersion: libVersion, - }, + 'The URL for the JSON "$schema" is incorrect. Correct the URL, then click Update.', }) ); } - - return { isVegaLite, libVersion }; } /** From f4ef2b116be5f92dc673c385e5968ae4b1fda0f6 Mon Sep 17 00:00:00 2001 From: Brian Seeders Date: Tue, 12 Oct 2021 11:18:13 -0400 Subject: [PATCH 071/287] [buildkite] buildkite dependencies need to install before print_agent_links (#114573) --- .buildkite/scripts/lifecycle/pre_command.sh | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.buildkite/scripts/lifecycle/pre_command.sh b/.buildkite/scripts/lifecycle/pre_command.sh index be31bb74ef668..cae6a07708d46 100755 --- a/.buildkite/scripts/lifecycle/pre_command.sh +++ b/.buildkite/scripts/lifecycle/pre_command.sh @@ -4,16 +4,17 @@ set -euo pipefail source .buildkite/scripts/common/util.sh -node .buildkite/scripts/lifecycle/print_agent_links.js || true - -echo '--- Job Environment Setup' +BUILDKITE_TOKEN="$(retry 5 5 vault read -field=buildkite_token_all_jobs secret/kibana-issues/dev/buildkite-ci)" +export BUILDKITE_TOKEN +echo '--- Install buildkite dependencies' cd '.buildkite' retry 5 15 yarn install cd - -BUILDKITE_TOKEN="$(retry 5 5 vault read -field=buildkite_token_all_jobs secret/kibana-issues/dev/buildkite-ci)" -export BUILDKITE_TOKEN +node .buildkite/scripts/lifecycle/print_agent_links.js || true + +echo '--- Job Environment Setup' # Set up a custom ES Snapshot Manifest if one has been specified for this build { From 435404e9614edb067778a7cadf5a58a460e86adb Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 12 Oct 2021 10:23:01 -0500 Subject: [PATCH 072/287] [Stack Monitoring] Fix blank page between loading page and overview (#114550) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../public/application/pages/loading_page.tsx | 10 +++++++++- .../monitoring/public/application/route_init.tsx | 5 ++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx index d5c1bcf80c23e..ebc43dd5c627e 100644 --- a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx @@ -15,12 +15,20 @@ import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants'; const CODE_PATHS = [CODE_PATH_ELASTICSEARCH]; -export const LoadingPage = () => { +export const LoadingPage = ({ staticLoadingState }: { staticLoadingState?: boolean }) => { const { clusters, loaded } = useClusters(null, undefined, CODE_PATHS); const title = i18n.translate('xpack.monitoring.loading.pageTitle', { defaultMessage: 'Loading', }); + if (staticLoadingState) { + return ( + + ; + + ); + } + return ( {loaded === false ? : renderRedirections(clusters)} diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index 8a11df3de50ae..092b3f54036c9 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -9,6 +9,7 @@ import { Route, Redirect, useLocation } from 'react-router-dom'; import { useClusters } from './hooks/use_clusters'; import { GlobalStateContext } from './contexts/global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; +import { LoadingPage } from './pages/loading_page'; export interface ComponentProps { clusters: []; @@ -66,7 +67,9 @@ export const RouteInit: React.FC = ({ - ) : null; + ) : ( + + ); }; const isExpired = (license: any): boolean => { From fa69602b343bde7d5375ed2d2c732bf628b9ab28 Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Tue, 12 Oct 2021 17:32:32 +0200 Subject: [PATCH 073/287] [Lens] Fix Metric visualization scale (#113956) * :bug: Fix metric rescale * :camera_flash: Restored old snapshots * :bug: Extend the fix to all scenarios * :camera_flash: Refresh snapshots for new fix Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../metric_visualization/expression.test.tsx | 20 ++++++++++++++----- .../metric_visualization/expression.tsx | 2 +- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx index a3ac5b5837772..db70a7c8508e5 100644 --- a/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/expression.test.tsx @@ -90,7 +90,9 @@ describe('metric_expression', () => { reportDescription="Fancy chart description" reportTitle="My fanci metric chart" > - +
{ reportDescription="Fancy chart description" reportTitle="My fanci metric chart" > - +
{ reportDescription="" reportTitle="" > - +
{ reportDescription="" reportTitle="" > - +
{ reportDescription="" reportTitle="" > - +
- +
{value}
From 917807e7a342de15f704c7674574f2494bc82dce Mon Sep 17 00:00:00 2001 From: Pete Hampton Date: Tue, 12 Oct 2021 16:32:44 +0100 Subject: [PATCH 074/287] Telemetry: update security filterlist. (#114495) --- .../server/lib/telemetry/filters.ts | 5 +++++ .../server/lib/telemetry/sender.test.ts | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts index a29f195ed5ecc..ee162fb76f95b 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/filters.ts @@ -14,6 +14,7 @@ export interface AllowlistFields { // Allow list process fields within events. This includes "process" and "Target.process".' const allowlistProcessFields: AllowlistFields = { args: true, + entity_id: true, name: true, executable: true, code_signature: true, @@ -30,6 +31,9 @@ const allowlistProcessFields: AllowlistFields = { dll: true, malware_signature: true, memory_region: true, + real: { + entity_id: true, + }, token: { integrity_level_name: true, }, @@ -49,6 +53,7 @@ const allowlistBaseEventFields: AllowlistFields = { original_file_name: true, }, }, + dns: true, event: true, file: { extension: true, diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts index 21e6b2cf6d9c4..46ed0b1f0bfb6 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/sender.test.ts @@ -36,6 +36,11 @@ describe('TelemetryEventsSender', () => { event: { kind: 'alert', }, + dns: { + question: { + name: 'test-dns', + }, + }, agent: { name: 'test', }, @@ -79,6 +84,7 @@ describe('TelemetryEventsSender', () => { nope: 'nope', executable: null, // null fields are never allowlisted working_directory: '/some/usr/dir', + entity_id: 'some_entity_id', }, Responses: '{ "result": 0 }', // >= 7.15 Target: { @@ -102,6 +108,11 @@ describe('TelemetryEventsSender', () => { event: { kind: 'alert', }, + dns: { + question: { + name: 'test-dns', + }, + }, agent: { name: 'test', }, @@ -139,6 +150,7 @@ describe('TelemetryEventsSender', () => { process: { name: 'foo.exe', working_directory: '/some/usr/dir', + entity_id: 'some_entity_id', }, Responses: '{ "result": 0 }', Target: { From 2c2b8e388ade50362464924e1e9b19c62b6c06b4 Mon Sep 17 00:00:00 2001 From: Yara Tercero Date: Tue, 12 Oct 2021 08:49:11 -0700 Subject: [PATCH 075/287] [Security Solution][Platform] - Bug fix when loading a saved query in detections (#114347) Bug in checking === for null when possibility of undefined, added tests. --- .../components/url_state/helpers.test.ts | 57 ++++++++++++++++++- .../common/components/url_state/helpers.ts | 7 ++- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts index 8a678be0616b9..ba806da195461 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts @@ -6,7 +6,8 @@ */ import { navTabs } from '../../../app/home/home_navigations'; -import { getTitle } from './helpers'; +import { getTitle, isQueryStateEmpty } from './helpers'; +import { CONSTANTS } from './constants'; describe('Helpers Url_State', () => { describe('getTitle', () => { @@ -31,4 +32,58 @@ describe('Helpers Url_State', () => { expect(result).toEqual(''); }); }); + + describe('isQueryStateEmpty', () => { + test('returns true if queryState is undefined', () => { + const result = isQueryStateEmpty(undefined, CONSTANTS.savedQuery); + expect(result).toBeTruthy(); + }); + + test('returns true if queryState is null', () => { + const result = isQueryStateEmpty(null, CONSTANTS.savedQuery); + expect(result).toBeTruthy(); + }); + + test('returns true if url key is "query" and queryState is empty string', () => { + const result = isQueryStateEmpty({}, CONSTANTS.appQuery); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "query" and queryState is not empty', () => { + const result = isQueryStateEmpty( + { query: { query: '*:*' }, language: 'kuery' }, + CONSTANTS.appQuery + ); + expect(result).toBeFalsy(); + }); + + test('returns true if url key is "filters" and queryState is empty', () => { + const result = isQueryStateEmpty([], CONSTANTS.filters); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "filters" and queryState is not empty', () => { + const result = isQueryStateEmpty( + [{ query: { query: '*:*' }, meta: { key: '123' } }], + CONSTANTS.filters + ); + expect(result).toBeFalsy(); + }); + + // TODO: Is this a bug, or intended? + test('returns false if url key is "timeline" and queryState is empty', () => { + const result = isQueryStateEmpty({}, CONSTANTS.timeline); + expect(result).toBeFalsy(); + }); + + test('returns true if url key is "timeline" and queryState id is empty string', () => { + const result = isQueryStateEmpty({ id: '', isOpen: true }, CONSTANTS.timeline); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "timeline" and queryState is not empty', () => { + const result = isQueryStateEmpty({ id: '123', isOpen: true }, CONSTANTS.timeline); + expect(result).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index ba09ed914dc68..5b6bb0400ccdf 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -199,8 +199,11 @@ export const updateTimerangeUrl = ( return timeRange; }; -export const isQueryStateEmpty = (queryState: ValueUrlState | null, urlKey: KeyUrlState) => - queryState === null || +export const isQueryStateEmpty = ( + queryState: ValueUrlState | undefined | null, + urlKey: KeyUrlState +): boolean => + queryState == null || (urlKey === CONSTANTS.appQuery && isEmpty((queryState as Query).query)) || (urlKey === CONSTANTS.filters && isEmpty(queryState)) || (urlKey === CONSTANTS.timeline && (queryState as TimelineUrl).id === ''); From 86af44854c40b5c343ca82c638b3ac13b6dd140c Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 12 Oct 2021 11:02:41 -0500 Subject: [PATCH 076/287] [DOCS] Reformats the Logs settings tables into definition lists (#114140) --- .../general-infra-logs-ui-settings.asciidoc | 41 +++++++++---------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/docs/settings/general-infra-logs-ui-settings.asciidoc b/docs/settings/general-infra-logs-ui-settings.asciidoc index 282239dcf166c..1e6dcf012206b 100644 --- a/docs/settings/general-infra-logs-ui-settings.asciidoc +++ b/docs/settings/general-infra-logs-ui-settings.asciidoc @@ -1,31 +1,28 @@ -[cols="2*<"] -|=== -| `xpack.infra.enabled` - | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] - Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. -| `xpack.infra.sources.default.logAlias` - | Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +`xpack.infra.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set to `false` to disable the Logs and Metrics app plugin {kib}. Defaults to `true`. -| `xpack.infra.sources.default.metricAlias` - | Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. +`xpack.infra.sources.default.logAlias`:: +Index pattern for matching indices that contain log data. Defaults to `filebeat-*,kibana_sample_data_logs*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -| `xpack.infra.sources.default.fields.timestamp` - | Timestamp used to sort log entries. Defaults to `@timestamp`. +`xpack.infra.sources.default.metricAlias`:: +Index pattern for matching indices that contain Metricbeat data. Defaults to `metricbeat-*`. To match multiple wildcard patterns, use a comma to separate the names, with no space after the comma. For example, `logstash-app1-*,default-logs-*`. -| `xpack.infra.sources.default.fields.message` - | Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. +`xpack.infra.sources.default.fields.timestamp`:: +Timestamp used to sort log entries. Defaults to `@timestamp`. -| `xpack.infra.sources.default.fields.tiebreaker` - | Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. +`xpack.infra.sources.default.fields.message`:: +Fields used to display messages in the Logs app. Defaults to `['message', '@message']`. -| `xpack.infra.sources.default.fields.host` - | Field used to identify hosts. Defaults to `host.name`. +`xpack.infra.sources.default.fields.tiebreaker`:: +Field used to break ties between two entries with the same timestamp. Defaults to `_doc`. -| `xpack.infra.sources.default.fields.container` - | Field used to identify Docker containers. Defaults to `container.id`. +`xpack.infra.sources.default.fields.host`:: +Field used to identify hosts. Defaults to `host.name`. -| `xpack.infra.sources.default.fields.pod` - | Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. +`xpack.infra.sources.default.fields.container`:: +Field used to identify Docker containers. Defaults to `container.id`. -|=== +`xpack.infra.sources.default.fields.pod`:: +Field used to identify Kubernetes pods. Defaults to `kubernetes.pod.uid`. \ No newline at end of file From be0a1e6c00ad87dd341e63bdb52ea033dbd5fbc1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 12 Oct 2021 11:07:38 -0500 Subject: [PATCH 077/287] [DOCS] Reformats the Machine learning settings tables into definition lists (#114143) --- docs/settings/ml-settings.asciidoc | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/docs/settings/ml-settings.asciidoc b/docs/settings/ml-settings.asciidoc index 59fa236e08275..e67876c76df0d 100644 --- a/docs/settings/ml-settings.asciidoc +++ b/docs/settings/ml-settings.asciidoc @@ -11,18 +11,14 @@ enabled by default. [[general-ml-settings-kb]] ==== General {ml} settings -[cols="2*<"] -|=== -| `xpack.ml.enabled` {ess-icon} - | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] - Set to `true` (default) to enable {kib} {ml-features}. + - + - If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} - instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, - you can still use the {ml} APIs. To disable {ml} entirely, see the - {ref}/ml-settings.html[{es} {ml} settings]. - -|=== +`xpack.ml.enabled` {ess-icon}:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set to `true` (default) to enable {kib} {ml-features}. + ++ +If set to `false` in `kibana.yml`, the {ml} icon is hidden in this {kib} +instance. If `xpack.ml.enabled` is set to `true` in `elasticsearch.yml`, however, +you can still use the {ml} APIs. To disable {ml} entirely, refer to +{ref}/ml-settings.html[{es} {ml} settings]. [[advanced-ml-settings-kb]] ==== Advanced {ml} settings From 511c0859449637039d151f3de78a23065e46d908 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 12 Oct 2021 11:08:02 -0500 Subject: [PATCH 078/287] [DOCS] Reformats the Search sessions settings tables into definition lists (#114145) --- .../search-sessions-settings.asciidoc | 39 +++++++------------ 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/docs/settings/search-sessions-settings.asciidoc b/docs/settings/search-sessions-settings.asciidoc index abd6a8f12b568..7b03cd23a9023 100644 --- a/docs/settings/search-sessions-settings.asciidoc +++ b/docs/settings/search-sessions-settings.asciidoc @@ -7,37 +7,26 @@ Configure the search session settings in your `kibana.yml` configuration file. +`xpack.data_enhanced.search.sessions.enabled` {ess-icon}:: +Set to `true` (default) to enable search sessions. -[cols="2*<"] -|=== -a| `xpack.data_enhanced.` -`search.sessions.enabled` {ess-icon} -| Set to `true` (default) to enable search sessions. +`xpack.data_enhanced.search.sessions.trackingInterval` {ess-icon}:: +The frequency for updating the state of a search session. The default is `10s`. -a| `xpack.data_enhanced.` -`search.sessions.trackingInterval` {ess-icon} -| The frequency for updating the state of a search session. The default is `10s`. - -a| `xpack.data_enhanced.` -`search.sessions.pageSize` {ess-icon} -| How many search sessions {kib} processes at once while monitoring +`xpack.data_enhanced.search.sessions.pageSize` {ess-icon}:: +How many search sessions {kib} processes at once while monitoring session progress. The default is `100`. -a| `xpack.data_enhanced.` -`search.sessions.notTouchedTimeout` {ess-icon} -| How long {kib} stores search results from unsaved sessions, +`xpack.data_enhanced.search.sessions.notTouchedTimeout` {ess-icon}:: +How long {kib} stores search results from unsaved sessions, after the last search in the session completes. The default is `5m`. -a| `xpack.data_enhanced.` -`search.sessions.notTouchedInProgressTimeout` {ess-icon} -| How long a search session can run after a user navigates away without saving a session. The default is `1m`. +`xpack.data_enhanced.search.sessions.notTouchedInProgressTimeout` {ess-icon}:: +How long a search session can run after a user navigates away without saving a session. The default is `1m`. -a| `xpack.data_enhanced.` -`search.sessions.maxUpdateRetries` {ess-icon} -| How many retries {kib} can perform while attempting to save a search session. The default is `3`. +`xpack.data_enhanced.search.sessions.maxUpdateRetries` {ess-icon}:: +How many retries {kib} can perform while attempting to save a search session. The default is `3`. -a| `xpack.data_enhanced.` -`search.sessions.defaultExpiration` {ess-icon} -| How long search session results are stored before they are deleted. +`xpack.data_enhanced.search.sessions.defaultExpiration` {ess-icon}:: +How long search session results are stored before they are deleted. Extending a search session resets the expiration by the same value. The default is `7d`. -|=== From 9594574d65df746613d485ee3a6676bf3c0816f1 Mon Sep 17 00:00:00 2001 From: Kaarina Tungseth Date: Tue, 12 Oct 2021 11:08:47 -0500 Subject: [PATCH 079/287] [DOCS] Reformats the Spaces settings tables into definition lists (#114146) --- docs/settings/spaces-settings.asciidoc | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index 8504464da1dfb..30b7beceb70ba 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -12,17 +12,13 @@ roles when Security is enabled. [[spaces-settings]] ==== Spaces settings -[cols="2*<"] -|=== -| `xpack.spaces.enabled` - | deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] - Set to `true` (default) to enable Spaces in {kib}. - This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. +`xpack.spaces.enabled`:: +deprecated:[7.16.0,"In 8.0 and later, this setting will no longer be supported."] +Set to `true` (default) to enable Spaces in {kib}. +This setting is deprecated. Starting in 8.0, it will not be possible to disable this plugin. -| `xpack.spaces.maxSpaces` - | The maximum amount of Spaces that can be used with this instance of {kib}. Some operations - in {kib} return all spaces using a single `_search` from {es}, so this must be - set lower than the `index.max_result_window` in {es}. - Defaults to `1000`. - -|=== +`xpack.spaces.maxSpaces`:: +The maximum amount of Spaces that can be used with this instance of {kib}. Some operations +in {kib} return all spaces using a single `_search` from {es}, so this must be +set lower than the `index.max_result_window` in {es}. +Defaults to `1000`. From 103869509c9e2253499c662a22001e38b06e0088 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Tue, 12 Oct 2021 12:12:34 -0400 Subject: [PATCH 080/287] [Security Solution] [Platform] Utilize SO resolve api for reading rules by `id` (#112478) * added outcome to backend routes * adds so resolved property alias_target_id to response * adds UI portion * working URL redirect on aliasMatch - todo -> update rule details page refresh button to use SO resolve. * cleanup * fix integration tests * fix jest tests * cleanup types * fix eslint.. I think vs code formatted this * WIP - undo me, working index.test.ts function * WIP - also undo me, probably * working test for aliasMatch, need to add test for outcome = conflict * add conflict callout when SO resolve yields conflict outcome * code cleanup * fix type issues * small cleanup, fix jest test after undoing changes for getFailingRuleStatus * cleanup tests * add alias_target_id to response validation too * unit test changes * update tests again * add all dependencies to useEffect and prefer useMemo * add type cast * adds integration tests for different outcomes after mocking a migrated rule leading to an aliasMatch and a migrated rule + accidental inserted rule to lead to a conflict. Also removes the outcome property if it is an exactMatch * remove unused import * fix test * functional WIP * cleanup * cleanup * finishing touches to address PR review comments * remove console.error * fix bug where spaces was not typed correctly in the plugin start method here https://github.com/elastic/kibana/pull/113983 --- .../schemas/common/schemas.ts | 12 + .../schemas/request/rule_schemas.ts | 4 + .../schemas/response/rules_schema.ts | 4 + .../detection_engine/rules/types.ts | 2 + .../rules/details/failure_history.test.tsx | 68 ++- .../rules/details/failure_history.tsx | 12 +- .../rules/details/index.test.tsx | 200 +++++++-- .../detection_engine/rules/details/index.tsx | 50 +++ .../use_hosts_risk_score.ts | 2 +- .../plugins/security_solution/public/types.ts | 2 +- .../routes/__mocks__/request_responses.ts | 20 +- .../routes/rules/delete_rules_route.test.ts | 6 +- .../routes/rules/read_rules_route.test.ts | 41 ++ .../detection_engine/rules/read_rules.test.ts | 18 +- .../lib/detection_engine/rules/read_rules.ts | 12 +- .../lib/detection_engine/rules/types.ts | 4 +- .../rules/update_rules.test.ts | 14 +- .../schemas/rule_converters.ts | 9 +- .../basic/tests/index.ts | 2 +- .../security_and_spaces/tests/index.ts | 1 + .../tests/resolve_read_rules.ts | 160 +++++++ .../detection_engine_api_integration/utils.ts | 33 +- .../resolve_read_rules/7_14/data.json | 101 +++++ .../resolve_read_rules/7_14/mappings.json | 397 ++++++++++++++++++ 24 files changed, 1101 insertions(+), 73 deletions(-) create mode 100644 x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts create mode 100644 x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json create mode 100644 x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index a9f7d96f1eb2e..3933d7e39275e 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -35,6 +35,18 @@ export type Description = t.TypeOf; export const descriptionOrUndefined = t.union([description, t.undefined]); export type DescriptionOrUndefined = t.TypeOf; +// outcome is a property of the saved object resolve api +// will tell us info about the rule after 8.0 migrations +export const outcome = t.union([ + t.literal('exactMatch'), + t.literal('aliasMatch'), + t.literal('conflict'), +]); +export type Outcome = t.TypeOf; + +export const alias_target_id = t.string; +export type AliasTargetId = t.TypeOf; + export const enabled = t.boolean; export type Enabled = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index 719337a231c1c..12e72fb6fc697 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -58,6 +58,8 @@ import { tags, interval, enabled, + outcome, + alias_target_id, updated_at, updated_by, created_at, @@ -150,6 +152,8 @@ const baseParams = { building_block_type, note, license, + outcome, + alias_target_id, output_index, timeline_id, timeline_title, diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts index 247829d5b9e7a..ac9329c3870f1 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/response/rules_schema.ts @@ -70,6 +70,8 @@ import { last_failure_message, filters, meta, + outcome, + alias_target_id, note, building_block_type, license, @@ -174,6 +176,8 @@ export const partialRulesSchema = t.partial({ last_failure_message, filters, meta, + outcome, + alias_target_id, index, namespace, note, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index 9faed2d0646e0..ecf68fa207b70 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -108,6 +108,8 @@ export const RuleSchema = t.intersection([ throttle: t.union([t.string, t.null]), }), t.partial({ + outcome: t.union([t.literal('exactMatch'), t.literal('aliasMatch'), t.literal('conflict')]), + alias_target_id: t.string, building_block_type, anomaly_threshold: t.number, filters: t.array(t.unknown), diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx index d95b6ca9f3435..c91aade50cbae 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.test.tsx @@ -6,19 +6,79 @@ */ import React from 'react'; -import { shallow } from 'enzyme'; - -import { TestProviders } from '../../../../../common/mock'; +import { shallow, mount } from 'enzyme'; +import { + TestProviders, + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, +} from '../../../../../common/mock'; import { FailureHistory } from './failure_history'; import { useRuleStatus } from '../../../../containers/detection_engine/rules'; jest.mock('../../../../containers/detection_engine/rules'); +import { waitFor } from '@testing-library/react'; + +import '../../../../../common/mock/match_media'; + +import { createStore, State } from '../../../../../common/store'; +import { mockHistory, Router } from '../../../../../common/mock/router'; + +const state: State = { + ...mockGlobalState, +}; +const { storage } = createSecuritySolutionStorageMock(); +const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + +describe('RuleDetailsPageComponent', () => { + beforeAll(() => { + (useRuleStatus as jest.Mock).mockReturnValue([ + false, + { + status: 'succeeded', + last_failure_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + failures: [ + { + alert_id: 'myfakeid', + status_date: new Date().toISOString(), + status: 'failed', + last_failure_at: new Date().toISOString(), + last_success_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + last_look_back_date: new Date().toISOString(), // NOTE: This is no longer used on the UI, but left here in case users are using it within the API + }, + ], + }, + ]); + }); + + it('renders reported rule failures correctly', async () => { + const wrapper = mount( + + + + + + ); + + await waitFor(() => { + expect(wrapper.find('EuiBasicTable')).toHaveLength(1); + // ensure the expected error message is displayed in the table + expect(wrapper.find('EuiTableRowCell').at(2).find('div').at(1).text()).toEqual( + 'my fake failure message' + ); + }); + }); +}); + describe('FailureHistory', () => { beforeAll(() => { (useRuleStatus as jest.Mock).mockReturnValue([false, null]); }); - it('renders correctly', () => { + it('renders correctly with no statuses', () => { const wrapper = shallow(, { wrappingComponent: TestProviders, }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx index a7db7ab57f6c2..5289e34b10046 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/failure_history.tsx @@ -23,6 +23,12 @@ interface FailureHistoryProps { id?: string | null; } +const renderStatus = () => {i18n.TYPE_FAILED}; +const renderLastFailureAt = (value: string) => ( + +); +const renderLastFailureMessage = (value: string) => <>{value}; + const FailureHistoryComponent: React.FC = ({ id }) => { const [loading, ruleStatus] = useRuleStatus(id); if (loading) { @@ -36,14 +42,14 @@ const FailureHistoryComponent: React.FC = ({ id }) => { const columns: Array> = [ { name: i18n.COLUMN_STATUS_TYPE, - render: () => {i18n.TYPE_FAILED}, + render: renderStatus, truncateText: false, width: '16%', }, { field: 'last_failure_at', name: i18n.COLUMN_FAILED_AT, - render: (value: string) => , + render: renderLastFailureAt, sortable: false, truncateText: false, width: '24%', @@ -51,7 +57,7 @@ const FailureHistoryComponent: React.FC = ({ id }) => { { field: 'last_failure_message', name: i18n.COLUMN_FAILED_MSG, - render: (value: string) => <>{value}, + render: renderLastFailureMessage, sortable: false, truncateText: false, width: '60%', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index 0c67a19e59e32..9c1667e7b4910 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -20,22 +20,51 @@ import { import { RuleDetailsPage } from './index'; import { createStore, State } from '../../../../../common/store'; import { useUserData } from '../../../../components/user_info'; +import { useRuleStatus } from '../../../../containers/detection_engine/rules'; +import { useRuleWithFallback } from '../../../../containers/detection_engine/rules/use_rule_with_fallback'; + import { useSourcererScope } from '../../../../../common/containers/sourcerer'; import { useParams } from 'react-router-dom'; import { mockHistory, Router } from '../../../../../common/mock/router'; -import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin'; + +import { useKibana } from '../../../../../common/lib/kibana'; + +import { fillEmptySeverityMappings } from '../helpers'; // Test will fail because we will to need to mock some core services to make the test work // For now let's forget about SiemSearchBar and QueryBar jest.mock('../../../../../common/components/search_bar', () => ({ SiemSearchBar: () => null, })); +jest.mock('../helpers', () => { + const original = jest.requireActual('../helpers'); + return { + ...original, + fillEmptySeverityMappings: jest.fn().mockReturnValue([]), + }; +}); jest.mock('../../../../../common/components/query_bar', () => ({ QueryBar: () => null, })); jest.mock('../../../../containers/detection_engine/lists/use_lists_config'); jest.mock('../../../../../common/components/link_to'); jest.mock('../../../../components/user_info'); +jest.mock('../../../../containers/detection_engine/rules', () => { + const original = jest.requireActual('../../../../containers/detection_engine/rules'); + return { + ...original, + useRuleStatus: jest.fn(), + }; +}); +jest.mock('../../../../containers/detection_engine/rules/use_rule_with_fallback', () => { + const original = jest.requireActual( + '../../../../containers/detection_engine/rules/use_rule_with_fallback' + ); + return { + ...original, + useRuleWithFallback: jest.fn(), + }; +}); jest.mock('../../../../../common/containers/sourcerer'); jest.mock('../../../../../common/containers/use_global_time', () => ({ useGlobalTime: jest.fn().mockReturnValue({ @@ -55,41 +84,42 @@ jest.mock('react-router-dom', () => { }; }); -jest.mock('../../../../../common/lib/kibana', () => { - const original = jest.requireActual('../../../../../common/lib/kibana'); +jest.mock('../../../../../common/lib/kibana'); - return { - ...original, - useUiSetting$: jest.fn().mockReturnValue([]), - useKibana: () => ({ - services: { - application: { - ...original.useKibana().services.application, - navigateToUrl: jest.fn(), - capabilities: { - actions: jest.fn().mockReturnValue({}), - siem: { crud_alerts: true, read_alerts: true }, - }, - }, - timelines: { ...mockTimelines }, - data: { - query: { - filterManager: jest.fn().mockReturnValue({}), - }, - }, - }, - }), - useToasts: jest.fn().mockReturnValue({ - addError: jest.fn(), - addSuccess: jest.fn(), - addWarning: jest.fn(), - }), - }; -}); +const mockRedirectLegacyUrl = jest.fn(); +const mockGetLegacyUrlConflict = jest.fn(); const state: State = { ...mockGlobalState, }; + +const mockRule = { + id: 'myfakeruleid', + author: [], + severity_mapping: [], + risk_score_mapping: [], + rule_id: 'rule-1', + risk_score: 50, + description: 'some description', + from: 'now-5m', + to: 'now', + name: 'some-name', + severity: 'low', + type: 'query', + query: 'some query', + index: ['index-1'], + interval: '5m', + references: [], + actions: [], + enabled: false, + false_positives: [], + max_signals: 100, + tags: [], + threat: [], + throttle: null, + version: 1, + exceptions_list: [], +}; const { storage } = createSecuritySolutionStorageMock(); const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); @@ -101,9 +131,108 @@ describe('RuleDetailsPageComponent', () => { indicesExist: true, indexPattern: {}, }); + (useRuleStatus as jest.Mock).mockReturnValue([ + false, + { + status: 'succeeded', + last_failure_at: new Date().toISOString(), + last_failure_message: 'my fake failure message', + failures: [], + }, + ]); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule }, + }); + (fillEmptySeverityMappings as jest.Mock).mockReturnValue([]); + }); + + async function setup() { + const useKibanaMock = useKibana as jest.Mocked; + + // eslint-disable-next-line react-hooks/rules-of-hooks + useKibanaMock().services.spaces = { + ui: { + // @ts-expect-error + components: { getLegacyUrlConflict: mockGetLegacyUrlConflict }, + redirectLegacyUrl: mockRedirectLegacyUrl, + }, + }; + } + + it('renders correctly with no outcome property on rule', async () => { + await setup(); + + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); + }); + + it('renders correctly with outcome === "exactMatch"', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'exactMatch' }, + }); + + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).not.toHaveBeenCalled(); + }); }); - it('renders correctly', async () => { + it('renders correctly with outcome === "aliasMatch"', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'aliasMatch' }, + }); + const wrapper = mount( + + + + + + ); + await waitFor(() => { + expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith(`rules/id/myfakeruleid`, `rule`); + }); + }); + + it('renders correctly when outcome = conflict', async () => { + await setup(); + (useRuleWithFallback as jest.Mock).mockReturnValue({ + error: null, + loading: false, + isExistingRule: true, + refresh: jest.fn(), + rule: { ...mockRule, outcome: 'conflict', alias_target_id: 'aliased_rule_id' }, + }); const wrapper = mount( @@ -113,6 +242,13 @@ describe('RuleDetailsPageComponent', () => { ); await waitFor(() => { expect(wrapper.find('[data-test-subj="header-page-title"]').exists()).toBe(true); + expect(mockRedirectLegacyUrl).toHaveBeenCalledWith(`rules/id/myfakeruleid`, `rule`); + expect(mockGetLegacyUrlConflict).toHaveBeenCalledWith({ + currentObjectId: 'myfakeruleid', + objectNoun: 'rule', + otherObjectId: 'aliased_rule_id', + otherObjectPath: `rules/id/aliased_rule_id`, + }); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 70d7faa47b9ee..492b8e461fb60 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -19,6 +19,8 @@ import { EuiToolTip, EuiWindowEvent, } from '@elastic/eui'; +import { i18n as i18nTranslate } from '@kbn/i18n'; + import { FormattedMessage } from '@kbn/i18n/react'; import { noop } from 'lodash/fp'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; @@ -261,6 +263,7 @@ const RuleDetailsPageComponent: React.FC = ({ capabilities: { actions }, }, timelines: timelinesUi, + spaces: spacesApi, }, } = useKibana(); const hasActionsPrivileges = useMemo(() => { @@ -277,6 +280,52 @@ const RuleDetailsPageComponent: React.FC = ({ } }, [maybeRule]); + useEffect(() => { + if (rule) { + const outcome = rule.outcome; + if (spacesApi && outcome === 'aliasMatch') { + // This rule has been resolved from a legacy URL - redirect the user to the new URL and display a toast. + const path = `rules/id/${rule.id}${window.location.search}${window.location.hash}`; + spacesApi.ui.redirectLegacyUrl( + path, + i18nTranslate.translate( + 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + { + defaultMessage: 'rule', + } + ) + ); + } + } + }, [rule, spacesApi]); + + const getLegacyUrlConflictCallout = useMemo(() => { + const outcome = rule?.outcome; + if (rule != null && spacesApi && outcome === 'conflict') { + const aliasTargetId = rule?.alias_target_id!; // This is always defined if outcome === 'conflict' + // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a + // callout with a warning for the user, and provide a way for them to navigate to the other rule. + const otherRulePath = `rules/id/${aliasTargetId}${window.location.search}${window.location.hash}`; + return ( + <> + + {spacesApi.ui.components.getLegacyUrlConflict({ + objectNoun: i18nTranslate.translate( + 'xpack.triggersActionsUI.sections.alertDetails.redirectObjectNoun', + { + defaultMessage: 'rule', + } + ), + currentObjectId: rule.id, + otherObjectId: aliasTargetId, + otherObjectPath: otherRulePath, + })} + + ); + } + return null; + }, [rule, spacesApi]); + useEffect(() => { if (!hasIndexRead) { setTabs(ruleDetailTabs.filter(({ id }) => id !== RuleDetailTabs.alerts)); @@ -721,6 +770,7 @@ const RuleDetailsPageComponent: React.FC = ({ {ruleError} + {getLegacyUrlConflictCallout} diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts index af663bb74f54a..15cb7ef7b1c46 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts @@ -98,7 +98,7 @@ export const useHostsRiskScore = ({ useEffect(() => { if (riskyHostsFeatureEnabled && (hostName || timerange)) { - spaces.getActiveSpace().then((space) => { + spaces?.getActiveSpace().then((space) => { start({ data, timerange: timerange diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index 1cec87fd35d1f..e595b905b998e 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -68,7 +68,7 @@ export interface StartPlugins { timelines: TimelinesUIStart; uiActions: UiActionsStart; ml?: MlPluginStart; - spaces: SpacesPluginStart; + spaces?: SpacesPluginStart; } export type StartServices = CoreStart & diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index c3c3ac47baf9a..200246ba1a367 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -34,7 +34,7 @@ import { getFinalizeSignalsMigrationSchemaMock } from '../../../../../common/det import { EqlSearchResponse } from '../../../../../common/detection_engine/types'; import { getSignalsMigrationStatusSchemaMock } from '../../../../../common/detection_engine/schemas/request/get_signals_migration_status_schema.mock'; import { RuleParams } from '../../schemas/rule_schemas'; -import { Alert } from '../../../../../../alerting/common'; +import { SanitizedAlert, ResolvedSanitizedRule } from '../../../../../../alerting/common'; import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; import { getPerformBulkActionSchemaMock } from '../../../../../common/detection_engine/schemas/request/perform_bulk_action_schema.mock'; import { RuleExecutionStatus } from '../../../../../common/detection_engine/schemas/common/schemas'; @@ -87,6 +87,13 @@ export const getReadRequest = () => query: { rule_id: 'rule-1' }, }); +export const getReadRequestWithId = (id: string) => + requestMock.create({ + method: 'get', + path: DETECTION_ENGINE_RULES_URL, + query: { id }, + }); + export const getFindRequest = () => requestMock.create({ method: 'get', @@ -362,7 +369,7 @@ export const nonRuleAlert = (isRuleRegistryEnabled: boolean) => ({ export const getAlertMock = ( isRuleRegistryEnabled: boolean, params: T -): Alert => ({ +): SanitizedAlert => ({ id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', name: 'Detect Root/Admin Users', tags: [`${INTERNAL_RULE_ID_KEY}:rule-1`, `${INTERNAL_IMMUTABLE_KEY}:false`], @@ -378,7 +385,6 @@ export const getAlertMock = ( notifyWhen: null, createdBy: 'elastic', updatedBy: 'elastic', - apiKey: null, apiKeyOwner: 'elastic', muteAll: false, mutedInstanceIds: [], @@ -389,6 +395,14 @@ export const getAlertMock = ( }, }); +export const resolveAlertMock = ( + isRuleRegistryEnabled: boolean, + params: T +): ResolvedSanitizedRule => ({ + outcome: 'exactMatch', + ...getAlertMock(isRuleRegistryEnabled, params), +}); + export const updateActionResult = (): ActionResult => ({ id: 'result-1', actionTypeId: 'action-id-1', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts index 35b3ef3d9cf85..7c447660acb45 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/delete_rules_route.test.ts @@ -8,7 +8,7 @@ import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; import { getEmptyFindResult, - getAlertMock, + resolveAlertMock, getDeleteRequest, getFindResultWithSingleHit, getDeleteRequestById, @@ -45,8 +45,8 @@ describe.each([ }); test('returns 200 when deleting a single rule with a valid actionClient and alertClient by id', async () => { - clients.rulesClient.get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + clients.rulesClient.resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); const response = await server.inject(getDeleteRequestById(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts index d6c18088800ba..37b8228ac1e9b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/read_rules_route.test.ts @@ -12,11 +12,14 @@ import { readRulesRoute } from './read_rules_route'; import { getEmptyFindResult, getReadRequest, + getReadRequestWithId, getFindResultWithSingleHit, nonRuleFindResult, getEmptySavedObjectsResponse, + resolveAlertMock, } from '../__mocks__/request_responses'; import { requestMock, requestContextMock, serverMock } from '../__mocks__'; +import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; describe.each([ ['Legacy', false], @@ -26,6 +29,7 @@ describe.each([ let { clients, context } = requestContextMock.createTools(); let logger: ReturnType; + const myFakeId = '99403909-ca9b-49ba-9d7a-7e5320e68d05'; beforeEach(() => { server = serverMock.create(); logger = loggingSystemMock.createLogger(); @@ -35,6 +39,12 @@ describe.each([ clients.savedObjectsClient.find.mockResolvedValue(getEmptySavedObjectsResponse()); // successful transform clients.ruleExecutionLogClient.find.mockResolvedValue([]); + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + }); readRulesRoute(server.router, logger, isRuleRegistryEnabled); }); @@ -44,6 +54,37 @@ describe.each([ expect(response.status).toEqual(200); }); + test('returns 200 when reading a single rule outcome === exactMatch', async () => { + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + }); + + test('returns 200 when reading a single rule outcome === aliasMatch', async () => { + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + outcome: 'aliasMatch', + }); + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + }); + + test('returns 200 when reading a single rule outcome === conflict', async () => { + clients.rulesClient.resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, { + ...getQueryRuleParams(), + }), + id: myFakeId, + outcome: 'conflict', + alias_target_id: 'myaliastargetid', + }); + const response = await server.inject(getReadRequestWithId(myFakeId), context); + expect(response.status).toEqual(200); + expect(response.body.alias_target_id).toEqual('myaliastargetid'); + }); + test('returns 404 if alertClient is not available on the route', async () => { context.alerting!.getRulesClient = jest.fn(); const response = await server.inject(getReadRequest(), context); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts index 6f89d725a458e..2e17b91fbcd54 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.test.ts @@ -7,7 +7,11 @@ import { readRules } from './read_rules'; import { rulesClientMock } from '../../../../../alerting/server/mocks'; -import { getAlertMock, getFindResultWithSingleHit } from '../routes/__mocks__/request_responses'; +import { + resolveAlertMock, + getAlertMock, + getFindResultWithSingleHit, +} from '../routes/__mocks__/request_responses'; import { getQueryRuleParams } from '../schemas/rule_schemas.mock'; export class TestError extends Error { @@ -33,7 +37,9 @@ describe.each([ describe('readRules', () => { test('should return the output from rulesClient if id is set but ruleId is undefined', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockResolvedValue(getAlertMock(isRuleRegistryEnabled, getQueryRuleParams())); + rulesClient.resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + ); const rule = await readRules({ isRuleRegistryEnabled, @@ -45,10 +51,10 @@ describe.each([ }); test('should return null if saved object found by alerts client given id is not alert type', async () => { const rulesClient = rulesClientMock.create(); - const result = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); + const result = resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); // @ts-expect-error delete result.alertTypeId; - rulesClient.get.mockResolvedValue(result); + rulesClient.resolve.mockResolvedValue(result); const rule = await readRules({ isRuleRegistryEnabled, @@ -61,7 +67,7 @@ describe.each([ test('should return error if alerts client throws 404 error on get', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockImplementation(() => { + rulesClient.resolve.mockImplementation(() => { throw new TestError(); }); @@ -76,7 +82,7 @@ describe.each([ test('should return error if alerts client throws error on get', async () => { const rulesClient = rulesClientMock.create(); - rulesClient.get.mockImplementation(() => { + rulesClient.resolve.mockImplementation(() => { throw new Error('Test error'); }); try { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts index 9578e3d4cb6d2..2571791164b6b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/read_rules.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SanitizedAlert } from '../../../../../alerting/common'; +import { ResolvedSanitizedRule, SanitizedAlert } from '../../../../../alerting/common'; import { INTERNAL_RULE_ID_KEY } from '../../../../common/constants'; import { RuleParams } from '../schemas/rule_schemas'; import { findRules } from './find_rules'; @@ -24,11 +24,17 @@ export const readRules = async ({ rulesClient, id, ruleId, -}: ReadRuleOptions): Promise | null> => { +}: ReadRuleOptions): Promise< + SanitizedAlert | ResolvedSanitizedRule | null +> => { if (id != null) { try { - const rule = await rulesClient.get({ id }); + const rule = await rulesClient.resolve({ id }); if (isAlertType(isRuleRegistryEnabled, rule)) { + if (rule?.outcome === 'exactMatch') { + const { outcome, ...restOfRule } = rule; + return restOfRule; + } return rule; } else { return null; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts index cceda063e987b..8adf19a53f92b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/types.ts @@ -100,14 +100,14 @@ import { } from '../../../../common/detection_engine/schemas/common/schemas'; import { RulesClient, PartialAlert } from '../../../../../alerting/server'; -import { Alert, SanitizedAlert } from '../../../../../alerting/common'; +import { SanitizedAlert } from '../../../../../alerting/common'; import { SIGNALS_ID } from '../../../../common/constants'; import { PartialFilter } from '../types'; import { RuleParams } from '../schemas/rule_schemas'; import { IRuleExecutionLogClient } from '../rule_execution_log/types'; import { ruleTypeMappings } from '../signals/utils'; -export type RuleAlertType = Alert; +export type RuleAlertType = SanitizedAlert; // eslint-disable-next-line @typescript-eslint/no-explicit-any export interface IRuleStatusSOAttributes extends Record { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts index 74301f3665ff8..703be3bdd76bd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/update_rules.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getAlertMock } from '../routes/__mocks__/request_responses'; +import { getAlertMock, resolveAlertMock } from '../routes/__mocks__/request_responses'; import { updateRules } from './update_rules'; import { getUpdateRulesOptionsMock, getUpdateMlRulesOptionsMock } from './update_rules.mock'; import { RulesClientMock } from '../../../../../alerting/server/rules_client.mock'; @@ -18,8 +18,8 @@ describe.each([ it('should call rulesClient.disable if the rule was enabled and enabled is false', async () => { const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled); rulesOptionsMock.ruleUpdate.enabled = false; - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) ); (rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue( getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) @@ -38,8 +38,8 @@ describe.each([ const rulesOptionsMock = getUpdateRulesOptionsMock(isRuleRegistryEnabled); rulesOptionsMock.ruleUpdate.enabled = true; - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue({ - ...getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue({ + ...resolveAlertMock(isRuleRegistryEnabled, getQueryRuleParams()), enabled: false, }); (rulesOptionsMock.rulesClient as unknown as RulesClientMock).update.mockResolvedValue( @@ -63,8 +63,8 @@ describe.each([ getAlertMock(isRuleRegistryEnabled, getMlRuleParams()) ); - (rulesOptionsMock.rulesClient as unknown as RulesClientMock).get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getMlRuleParams()) + (rulesOptionsMock.rulesClient as unknown as RulesClientMock).resolve.mockResolvedValue( + resolveAlertMock(isRuleRegistryEnabled, getMlRuleParams()) ); await updateRules(rulesOptionsMock); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index eef20af0e564d..240a226e86914 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -27,7 +27,7 @@ import { AppClient } from '../../../types'; import { addTags } from '../rules/add_tags'; import { DEFAULT_MAX_SIGNALS, SERVER_APP_ID, SIGNALS_ID } from '../../../../common/constants'; import { transformRuleToAlertAction } from '../../../../common/detection_engine/transform_actions'; -import { SanitizedAlert } from '../../../../../alerting/common'; +import { ResolvedSanitizedRule, SanitizedAlert } from '../../../../../alerting/common'; import { IRuleStatusSOAttributes } from '../rules/types'; import { transformTags } from '../routes/rules/utils'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; @@ -281,12 +281,17 @@ export const commonParamsCamelToSnake = (params: BaseRuleParams) => { }; export const internalRuleToAPIResponse = ( - rule: SanitizedAlert, + rule: SanitizedAlert | ResolvedSanitizedRule, ruleStatus?: IRuleStatusSOAttributes, legacyRuleActions?: LegacyRuleActions | null ): FullResponseSchema => { const mergedStatus = ruleStatus ? mergeAlertWithSidecarStatus(rule, ruleStatus) : undefined; + const isResolvedRule = (obj: unknown): obj is ResolvedSanitizedRule => + (obj as ResolvedSanitizedRule).outcome != null; return { + // saved object properties + outcome: isResolvedRule(rule) ? rule.outcome : undefined, + alias_target_id: isResolvedRule(rule) ? rule.alias_target_id : undefined, // Alerting framework params id: rule.id, updated_at: rule.updatedAt.toISOString(), diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 802b1e78930e8..5fa4540bbe854 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -9,7 +9,7 @@ import { FtrProviderContext } from '../../common/ftr_provider_context'; // eslint-disable-next-line import/no-default-export export default ({ loadTestFile }: FtrProviderContext): void => { - describe('detection engine api security and spaces enabled', function () { + describe('detection engine api basic license', function () { this.tags('ciGroup1'); loadTestFile(require.resolve('./add_prepackaged_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index b4bd74172920b..1b88c4fe21b49 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -33,6 +33,7 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./read_rules')); + loadTestFile(require.resolve('./resolve_read_rules')); loadTestFile(require.resolve('./update_rules')); loadTestFile(require.resolve('./update_rules_bulk')); loadTestFile(require.resolve('./patch_rules_bulk')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts new file mode 100644 index 0000000000000..6013398d4695d --- /dev/null +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/resolve_read_rules.ts @@ -0,0 +1,160 @@ +/* + * 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 { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { createSignalsIndex, deleteAllAlerts, deleteSignalsIndex } from '../../utils'; + +const spaceId = '714-space'; + +// eslint-disable-next-line import/no-default-export +export default ({ getService }: FtrProviderContext) => { + const supertest = getService('supertest'); + const es = getService('es'); + const esArchiver = getService('esArchiver'); + + describe('resolve_read_rules', () => { + describe('reading rules', () => { + beforeEach(async () => { + await createSignalsIndex(supertest); + await esArchiver.load( + 'x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14' + ); + }); + + afterEach(async () => { + await deleteSignalsIndex(supertest); + await deleteAllAlerts(supertest); + await esArchiver.unload( + 'x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14' + ); + }); + + it('should create a "migrated" rule where querying for the new SO _id will resolve the new object and not return the outcome field when outcome === exactMatch', async () => { + // link to the new URL with migrated SO id 74f3e6d7-b7bb-477d-ac28-92ee22728e6e + const URL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=90e3ca0e-71f7-513a-b60a-ac678efd8887`; + const readRulesAliasMatchRes = await supertest.get(URL).set('kbn-xsrf', 'true').send(); + expect(readRulesAliasMatchRes.body.outcome).to.eql('aliasMatch'); + + // now that we have the migrated alias_target_id, let's attempt an 'exactMatch' query + // the result of which should have the outcome as undefined when querying the read rules api. + const exactMatchURL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=${readRulesAliasMatchRes.body.alias_target_id}`; + const readRulesExactMatchRes = await supertest + .get(exactMatchURL) + .set('kbn-xsrf', 'true') + .send(); + expect(readRulesExactMatchRes.body.outcome).to.eql(undefined); + }); + + it('should create a rule and a "conflicting rule" where the SO _id matches the sourceId (see legacy-url-alias SO) of a migrated rule', async () => { + // mimic a rule SO that was inserted accidentally + // we have to insert this outside of esArchiver otherwise kibana will migrate this + // and we won't have a conflict + await es.index({ + id: 'alert:90e3ca0e-71f7-513a-b60a-ac678efd8887', + index: '.kibana', + refresh: true, + body: { + alert: { + name: 'test 7.14', + tags: [ + '__internal_rule_id:82747bb8-bae0-4b59-8119-7f65ac564e14', + '__internal_immutable:false', + ], + alertTypeId: 'siem.signals', + consumer: 'siem', + params: { + author: [], + description: 'test', + ruleId: '82747bb8-bae0-4b59-8119-7f65ac564e14', + falsePositives: [], + from: 'now-3615s', + immutable: false, + license: '', + outputIndex: '.siem-signals-devin-hurley-714-space', + meta: { + from: '1h', + kibana_siem_app_url: 'http://0.0.0.0:5601/s/714-space/app/security', + }, + maxSignals: 100, + riskScore: 21, + riskScoreMapping: [], + severity: 'low', + severityMapping: [], + threat: [], + to: 'now', + references: [], + version: 1, + exceptionsList: [], + type: 'query', + language: 'kuery', + index: [ + 'apm-*-transaction*', + 'traces-apm*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + query: '*:*', + filters: [], + }, + schedule: { + interval: '15s', + }, + enabled: true, + actions: [], + throttle: null, + notifyWhen: 'onActiveAlert', + apiKeyOwner: 'elastic', + apiKey: + 'HvwrIJ8NBshJav9vf3BSEEa2P7fXLTpmEKAx2bSyBF51N2cadFkltWLRRcFnj65RXsPzvRm3VKzAde4b1iGzsjxY/IVmfGGyiO0rk6vZVJVLeMSD+CAiflnwweypoKM8WgwXJnI0Oa/SWqKMtrDiFxCcZCwIuAhS0sjenaiEuedbAuStZv513zz/clpqRKFXBydJXKyjJUQLTA==', + createdBy: 'elastic', + updatedBy: 'elastic', + createdAt: '2021-10-05T19:52:25.865Z', + updatedAt: '2021-10-05T19:52:25.865Z', + muteAll: false, + mutedInstanceIds: [], + executionStatus: { + status: 'ok', + lastExecutionDate: '2021-10-05T19:52:51.260Z', + error: null, + }, + meta: { + versionApiKeyLastmodified: '7.14.2', + }, + scheduledTaskId: 'c4005e90-2615-11ec-811e-db7211397897', + legacyId: 'c364e1e0-2615-11ec-811e-db7211397897', + }, + type: 'alert', + references: [], + namespaces: [spaceId], + originId: 'c364e1e0-2615-11ec-811e-db7211397897', + migrationVersion: { + alert: '8.0.0', + }, + coreMigrationVersion: '8.0.0', + updated_at: '2021-10-05T19:52:56.014Z', + }, + }); + + // Now that we have a rule id and a legacy-url-alias with the same id, we should have a conflict + const conflictURL = `/s/${spaceId}${DETECTION_ENGINE_RULES_URL}?id=90e3ca0e-71f7-513a-b60a-ac678efd8887`; + const readRulesConflictRes = await supertest + .get(conflictURL) + .set('kbn-xsrf', 'true') + .send() + .expect(200); + expect(readRulesConflictRes.body.outcome).to.eql('conflict'); + }); + }); + }); +}; diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index ac27f06a149d9..eeae21c3b7bad 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -369,17 +369,31 @@ export const getSimpleRuleOutput = (ruleId = 'rule-1', enabled = false): Partial version: 1, }); +export const resolveSimpleRuleOutput = ( + ruleId = 'rule-1', + enabled = false +): Partial => ({ outcome: 'exactMatch', ...getSimpleRuleOutput(ruleId, enabled) }); + /** * This is the typical output of a simple rule that Kibana will output with all the defaults except * for all the server generated properties such as created_by. Useful for testing end to end tests. */ export const getSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); - // eslint-disable-next-line @typescript-eslint/naming-convention - const { rule_id, ...ruleWithoutRuleId } = rule; + const { rule_id: rId, ...ruleWithoutRuleId } = rule; return ruleWithoutRuleId; }; +/** + * This is the typical output of a simple rule that Kibana will output with all the defaults except + * for all the server generated properties such as created_by. Useful for testing end to end tests. + */ +export const resolveSimpleRuleOutputWithoutRuleId = (ruleId = 'rule-1'): Partial => { + const rule = getSimpleRuleOutput(ruleId); + const { rule_id: rId, ...ruleWithoutRuleId } = rule; + return { outcome: 'exactMatch', ...ruleWithoutRuleId }; +}; + export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial => { const rule = getSimpleRuleOutput(ruleId); const { query, language, index, ...rest } = rule; @@ -399,12 +413,17 @@ export const getSimpleMlRuleOutput = (ruleId = 'rule-1'): Partial = * @param supertest The supertest agent. */ export const deleteAllAlerts = async ( - supertest: SuperTest.SuperTest + supertest: SuperTest.SuperTest, + space?: string ): Promise => { await countDownTest( async () => { const { body } = await supertest - .get(`${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999`) + .get( + space + ? `/s/${space}${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999` + : `${DETECTION_ENGINE_RULES_URL}/_find?per_page=9999` + ) .set('kbn-xsrf', 'true') .send(); @@ -413,7 +432,11 @@ export const deleteAllAlerts = async ( })); await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_bulk_delete`) + .post( + space + ? `/s/${space}${DETECTION_ENGINE_RULES_URL}/_bulk_delete` + : `${DETECTION_ENGINE_RULES_URL}/_bulk_delete` + ) .send(ids) .set('kbn-xsrf', 'true'); diff --git a/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json new file mode 100644 index 0000000000000..498367c913dc0 --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/data.json @@ -0,0 +1,101 @@ +{ + "type" : "doc", + "value": { + "index" : ".kibana_1", + "id" : "space:714-space", + "source" : { + "space" : { + "name" : "714-space", + "initials" : "t", + "color" : "#B9A888", + "disabledFeatures" : [ ], + "imageUrl" : "" + }, + "type" : "space", + "references" : [ ], + "migrationVersion" : { + "space" : "6.6.0" + }, + "updated_at" : "2021-10-11T14:49:07.012Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "714-space:alert:90e3ca0e-71f7-513a-b60a-ac678efd8887", + "index": ".kibana_1", + "source": { + "alert": { + "actions": [ + ], + "alertTypeId" : "siem.signals", + "consumer" : "siem", + "apiKey": "QIUT8u0/kbOakEHSj50jDpVR90MrqOxanEscboYOoa8PxQvcA5jfHash+fqH3b+KNjJ1LpnBcisGuPkufY9j1e32gKzwGZV5Bfys87imHvygJvIM8uKiFF8bQ8Y4NTaxOJO9fAmZPrFy07ZcQMCAQz+DUTgBFqs=", + "apiKeyOwner": "elastic", + "createdAt": "2020-06-17T15:35:38.497Z", + "createdBy": "elastic", + "enabled": true, + "muteAll": false, + "mutedInstanceIds": [ + ], + "name": "always-firing-alert", + "params":{ + "author": [], + "description": "test", + "ruleId": "82747bb8-bae0-4b59-8119-7f65ac564e14", + "falsePositives": [], + "from": "now-3615s", + "immutable": false, + "license": "", + "outputIndex": ".siem-signals-devin-hurley-714-space", + "meta": { + "from": "1h", + "kibana_siem_app_url": "http://0.0.0.0:5601/s/714-space/app/security" + }, + "maxSignals": 100, + "riskScore": 21, + "riskScoreMapping": [], + "severity": "low", + "severityMapping": [], + "threat": [], + "to": "now", + "references": [], + "version": 1, + "exceptionsList": [], + "type": "query", + "language": "kuery", + "index": [ + "apm-*-transaction*", + "traces-apm*", + "auditbeat-*", + "endgame-*", + "filebeat-*", + "logs-*", + "packetbeat-*", + "winlogbeat-*" + ], + "query": "*:*", + "filters": [] + }, + "schedule": { + "interval": "1m" + }, + "scheduledTaskId": "329798f0-b0b0-11ea-9510-fdf248d5f2a4", + "tags": [ + ], + "throttle": null, + "updatedBy": "elastic" + }, + "migrationVersion": { + "alert": "7.8.0" + }, + "references": [ + ], + "namespace": "714-space", + "type": "alert", + "updated_at": "2020-06-17T15:35:39.839Z" + } + } +} diff --git a/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json new file mode 100644 index 0000000000000..069f70badce4e --- /dev/null +++ b/x-pack/test/functional/es_archives/security_solution/resolve_read_rules/7_14/mappings.json @@ -0,0 +1,397 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": {} + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "7b44fba6773e37c806ce290ea9b7024e", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3525d7c22c42bc80f5e6e9cb3f2b26a2", + "application_usage_totals": "c897e4310c5f24b07caaff3db53ae2c1", + "application_usage_transactional": "965839e75f809fefe04f92dc4d99722a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "cases": "32aa96a6d3855ddda53010ae2048ac22", + "cases-comments": "c2061fb929f585df57425102fa928b4b", + "cases-configure": "42711cbb311976c0687853f4c1354572", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "ae24d22d5986d04124cc6568f771066f", + "dashboard": "d00f614b29a80360e1190193fd333bab", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "66eccb05066c5a89924f48a9e9736499", + "infrastructure-ui-source": "ddc0ecb18383f6b26101a2fadb2dab0c", + "inventory-view": "88fc7e12fd1b45b6f0787323ce4f18d2", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "d33c68a69ff1e78c9888dedd2164ac22", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "23d7aa4a720d4938ccde3983f87bd58d", + "maps-telemetry": "bfd39d88aadadb4be597ea984d433dbe", + "metrics-explorer-view": "428e319af3e822c80a84cf87123ca35c", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "181661168bbadd1eff5902361e2a0d5c", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "todo": "082a2cc96a590268344d5cd74c159ac4", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "296a89039fc4260292be36b1b005d8f2", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "fcdb453a30092f022f2642db29523d80", + "url": "b675c3be8d76ecf029294d51dc7ec65d", + "visualization": "52d7a13ad68a150c4525b292d23e12cc" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "legacyId": { + "type": "keyword" + }, + "createdBy": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "legacy-url-alias": { + "properties": { + "sourceId": { + "type": "text" + }, + "targetNamespace": { + "type": "keyword" + }, + "targetType": { + "type": "keyword" + }, + "targetId": { + "type": "keyword" + }, + "resolveCounter": { + "type": "integer" + }, + "lastResolved": { + "type": "date" + } + } + }, + "config": { + "dynamic": "true", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "alert": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "coreMigrationVersion": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "search": { + "properties": { + "columns": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "sort": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "1", + "number_of_shards": "1" + } + } + } +} From 80c152c0eb83c90a2292651b615c8ab005c348bd Mon Sep 17 00:00:00 2001 From: Dominique Clarke Date: Tue, 12 Oct 2021 12:37:21 -0400 Subject: [PATCH 081/287] [Observability] [Exploratory View] add percentile ranks, show legend always, and fix field labels (#113765) * add percentile ranks, show legend always, and fix field labels * add 50th percentile * replace hard coded values with constant Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../configurations/constants/constants.ts | 16 ++++- .../configurations/lens_attributes.test.ts | 61 ++++++++++++++++++- .../configurations/lens_attributes.ts | 57 +++++++++++++++-- .../rum/kpi_over_time_config.ts | 2 + .../synthetics/kpi_over_time_config.ts | 12 +++- .../test_data/sample_attribute.ts | 1 + .../test_data/sample_attribute_cwv.ts | 1 + .../test_data/sample_attribute_kpi.ts | 1 + .../breakdown/breakdowns.test.tsx | 21 +++++++ .../series_editor/breakdown/breakdowns.tsx | 20 ++++-- .../expanded_series_row.test.tsx | 40 ++++++++++++ .../series_editor/expanded_series_row.tsx | 6 +- 12 files changed, 219 insertions(+), 19 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index 68dcd77e98990..e4473b183d729 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { OperationType } from '../../../../../../../lens/public'; import { ReportViewType } from '../../types'; import { CLS_FIELD, @@ -13,6 +13,7 @@ import { LCP_FIELD, TBT_FIELD, TRANSACTION_TIME_TO_FIRST_BYTE, + TRANSACTION_DURATION, } from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, @@ -45,6 +46,8 @@ import { TBT_LABEL, URL_LABEL, BACKEND_TIME_LABEL, + MONITORS_DURATION_LABEL, + PAGE_LOAD_TIME_LABEL, LABELS_FIELD, } from './labels'; @@ -69,9 +72,11 @@ export const FieldLabels: Record = { [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, [TRANSACTION_TIME_TO_FIRST_BYTE]: BACKEND_TIME_LABEL, + [TRANSACTION_DURATION]: PAGE_LOAD_TIME_LABEL, 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, + 'monitor.duration.us': MONITORS_DURATION_LABEL, 'agent.hostname': AGENT_HOST_LABEL, 'host.hostname': HOST_NAME_LABEL, @@ -86,6 +91,7 @@ export const FieldLabels: Record = { 'performance.metric': METRIC_LABEL, 'Business.KPI': KPI_LABEL, 'http.request.method': REQUEST_METHOD, + percentile: 'Percentile', LABEL_FIELDS_FILTER: LABELS_FIELD, LABEL_FIELDS_BREAKDOWN: 'Labels field', }; @@ -114,8 +120,16 @@ export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; export const FILTER_RECORDS = 'FILTER_RECORDS'; export const TERMS_COLUMN = 'TERMS_COLUMN'; export const OPERATION_COLUMN = 'operation'; +export const PERCENTILE = 'percentile'; export const REPORT_METRIC_FIELD = 'REPORT_METRIC_FIELD'; +export const PERCENTILE_RANKS = [ + '99th' as OperationType, + '95th' as OperationType, + '90th' as OperationType, + '75th' as OperationType, + '50th' as OperationType, +]; export const LABEL_FIELDS_FILTER = 'LABEL_FIELDS_FILTER'; export const LABEL_FIELDS_BREAKDOWN = 'LABEL_FIELDS_BREAKDOWN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 2781c26954234..139f9fe67c751 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -16,7 +16,7 @@ import { } from './constants/elasticsearch_fieldnames'; import { buildExistsFilter, buildPhrasesFilter } from './utils'; import { sampleAttributeKpi } from './test_data/sample_attribute_kpi'; -import { RECORDS_FIELD, REPORT_METRIC_FIELD, ReportTypes } from './constants'; +import { RECORDS_FIELD, REPORT_METRIC_FIELD, PERCENTILE_RANKS, ReportTypes } from './constants'; describe('Lens Attribute', () => { mockAppIndexPattern(); @@ -75,6 +75,63 @@ describe('Lens Attribute', () => { expect(lnsAttrKpi.getJSON()).toEqual(sampleAttributeKpi); }); + it('should return expected json for percentile breakdowns', function () { + const seriesConfigKpi = getDefaultConfigs({ + reportType: ReportTypes.KPI, + dataType: 'ux', + indexPattern: mockIndexPattern, + }); + + const lnsAttrKpi = new LensAttributes([ + { + filters: [], + seriesConfig: seriesConfigKpi, + time: { + from: 'now-1h', + to: 'now', + }, + indexPattern: mockIndexPattern, + name: 'ux-series-1', + breakdown: 'percentile', + reportDefinitions: {}, + selectedMetricField: 'transaction.duration.us', + color: '#54b399', + }, + ]); + + expect(lnsAttrKpi.getJSON().state.datasourceStates.indexpattern.layers.layer0.columns).toEqual({ + 'x-axis-column-layer0': { + dataType: 'date', + isBucketed: true, + label: '@timestamp', + operationType: 'date_histogram', + params: { + interval: 'auto', + }, + scale: 'interval', + sourceField: '@timestamp', + }, + ...PERCENTILE_RANKS.reduce((acc: Record, rank, index) => { + acc[`y-axis-column-${index === 0 ? 'layer' + index : index}`] = { + dataType: 'number', + filter: { + language: 'kuery', + query: 'transaction.type: page-load and processor.event: transaction', + }, + isBucketed: false, + label: `${rank} percentile of page load time`, + operationType: 'percentile', + params: { + percentile: Number(rank.slice(0, 2)), + }, + scale: 'ratio', + sourceField: 'transaction.duration.us', + }; + return acc; + }, {}), + }); + }); + it('should return main y axis', function () { expect(lnsAttr.getMainYAxis(layerConfig, 'layer0', '')).toEqual({ dataType: 'number', @@ -413,7 +470,7 @@ describe('Lens Attribute', () => { yConfig: [{ color: 'green', forAccessor: 'y-axis-column-layer0' }], }, ], - legend: { isVisible: true, position: 'right' }, + legend: { isVisible: true, showSingleSeries: true, position: 'right' }, preferredSeriesType: 'line', tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, valueLabels: 'hide', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index fa5a8beb0087d..e3dab3c4e91f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -37,6 +37,8 @@ import { REPORT_METRIC_FIELD, RECORDS_FIELD, RECORDS_PERCENTAGE_FIELD, + PERCENTILE, + PERCENTILE_RANKS, ReportTypes, } from './constants'; import { ColumnFilter, SeriesConfig, UrlFilter, URLReportDefinition } from '../types'; @@ -249,6 +251,30 @@ export class LensAttributes { }; } + getPercentileBreakdowns( + layerConfig: LayerConfig, + columnFilter?: string + ): Record { + const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; + const { sourceField: mainSourceField, label: mainLabel } = yAxisColumns[0]; + const lensColumns: Record = {}; + + // start at 1, because main y axis will have the first percentile breakdown + for (let i = 1; i < PERCENTILE_RANKS.length; i++) { + lensColumns[`y-axis-column-${i}`] = { + ...this.getColumnBasedOnType({ + sourceField: mainSourceField!, + operationType: PERCENTILE_RANKS[i], + label: mainLabel, + layerConfig, + colIndex: i, + }), + filter: { query: columnFilter || '', language: 'kuery' }, + }; + } + return lensColumns; + } + getPercentileNumberColumn( sourceField: string, percentileValue: string, @@ -258,7 +284,7 @@ export class LensAttributes { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: seriesConfig.labels[sourceField], percentileValue }, + values: { sourceField: seriesConfig.labels[sourceField]?.toLowerCase(), percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -328,6 +354,7 @@ export class LensAttributes { layerConfig: LayerConfig; colIndex?: number; }) { + const { breakdown, seriesConfig } = layerConfig; const { fieldMeta, columnType, fieldName, columnLabel, timeScale, columnFilters } = this.getFieldMeta(sourceField, layerConfig); @@ -348,6 +375,18 @@ export class LensAttributes { if (fieldType === 'date') { return this.getDateHistogramColumn(fieldName); } + + if (fieldType === 'number' && breakdown === PERCENTILE) { + return { + ...this.getPercentileNumberColumn( + fieldName, + operationType || PERCENTILE_RANKS[0], + seriesConfig! + ), + filter: colIndex !== undefined ? columnFilters?.[colIndex] : undefined, + }; + } + if (fieldType === 'number') { return this.getNumberColumn({ sourceField: fieldName, @@ -395,6 +434,7 @@ export class LensAttributes { } getMainYAxis(layerConfig: LayerConfig, layerId: string, columnFilter: string) { + const { breakdown } = layerConfig; const { sourceField, operationType, label } = layerConfig.seriesConfig.yAxisColumns[0]; if (sourceField === RECORDS_PERCENTAGE_FIELD) { @@ -407,7 +447,7 @@ export class LensAttributes { return this.getColumnBasedOnType({ sourceField, - operationType, + operationType: breakdown === PERCENTILE ? PERCENTILE_RANKS[0] : operationType, label, layerConfig, colIndex: 0, @@ -415,6 +455,7 @@ export class LensAttributes { } getChildYAxises(layerConfig: LayerConfig, layerId?: string, columnFilter?: string) { + const { breakdown } = layerConfig; const lensColumns: Record = {}; const yAxisColumns = layerConfig.seriesConfig.yAxisColumns; const { sourceField: mainSourceField, label: mainLabel } = yAxisColumns[0]; @@ -424,7 +465,10 @@ export class LensAttributes { .supportingColumns; } - // 1 means there is only main y axis + if (yAxisColumns.length === 1 && breakdown === PERCENTILE) { + return this.getPercentileBreakdowns(layerConfig, columnFilter); + } + if (yAxisColumns.length === 1) { return lensColumns; } @@ -574,7 +618,7 @@ export class LensAttributes { layers[layerId] = { columnOrder: [ `x-axis-column-${layerId}`, - ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE ? [`breakdown-column-${layerId}`] : []), `y-axis-column-${layerId}`, @@ -588,7 +632,7 @@ export class LensAttributes { filter: { query: columnFilter, language: 'kuery' }, ...(timeShift ? { timeShift } : {}), }, - ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN + ...(breakdown && sourceField !== USE_BREAK_DOWN_COLUMN && breakdown !== PERCENTILE ? // do nothing since this will be used a x axis source { [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ @@ -610,7 +654,7 @@ export class LensAttributes { getXyState(): XYState { return { - legend: { isVisible: true, position: 'right' }, + legend: { isVisible: true, showSingleSeries: true, position: 'right' }, valueLabels: 'hide', fittingFunction: 'Linear', curveType: 'CURVE_MONOTONE_X' as XYCurveType, @@ -636,6 +680,7 @@ export class LensAttributes { ], xAccessor: `x-axis-column-layer${index}`, ...(layerConfig.breakdown && + layerConfig.breakdown !== PERCENTILE && layerConfig.seriesConfig.xAxisColumn.sourceField !== USE_BREAK_DOWN_COLUMN ? { splitAccessor: `breakdown-column-layer${index}` } : {}), diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts index de4f6b2198dbd..000e50d7b3a52 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/rum/kpi_over_time_config.ts @@ -13,6 +13,7 @@ import { OPERATION_COLUMN, RECORDS_FIELD, REPORT_METRIC_FIELD, + PERCENTILE, ReportTypes, } from '../constants'; import { buildPhraseFilter } from '../utils'; @@ -81,6 +82,7 @@ export function getKPITrendsLensConfig({ indexPattern }: ConfigProps): SeriesCon USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE, + PERCENTILE, LABEL_FIELDS_BREAKDOWN, ], baseFilters: [ diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts index 65b43a83a8fb5..6df9cdcd0503a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/kpi_over_time_config.ts @@ -6,7 +6,13 @@ */ import { ConfigProps, SeriesConfig } from '../../types'; -import { FieldLabels, OPERATION_COLUMN, REPORT_METRIC_FIELD, ReportTypes } from '../constants'; +import { + FieldLabels, + OPERATION_COLUMN, + REPORT_METRIC_FIELD, + PERCENTILE, + ReportTypes, +} from '../constants'; import { CLS_LABEL, DCL_LABEL, @@ -44,7 +50,7 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon ], hasOperationType: false, filterFields: ['observer.geo.name', 'monitor.type', 'tags'], - breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name'], + breakdownFields: ['observer.geo.name', 'monitor.type', 'monitor.name', PERCENTILE], baseFilters: [], palette: { type: 'palette', name: 'status' }, definitionFields: ['monitor.name', 'url.full'], @@ -98,6 +104,6 @@ export function getSyntheticsKPIConfig({ indexPattern }: ConfigProps): SeriesCon columnType: OPERATION_COLUMN, }, ], - labels: { ...FieldLabels }, + labels: { ...FieldLabels, [SUMMARY_UP]: UP_LABEL, [SUMMARY_DOWN]: DOWN_LABEL }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 7e0ea1e575481..8254a5a816921 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -187,6 +187,7 @@ export const sampleAttribute = { ], legend: { isVisible: true, + showSingleSeries: true, position: 'right', }, preferredSeriesType: 'line', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts index dff3d6b3ad5ef..adc6d4bb14462 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_cwv.ts @@ -134,6 +134,7 @@ export const sampleAttributeCoreWebVital = { ], legend: { isVisible: true, + showSingleSeries: true, position: 'right', }, preferredSeriesType: 'line', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts index 6ed9b4face6e3..8fbda9f6adc52 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute_kpi.ts @@ -89,6 +89,7 @@ export const sampleAttributeKpi = { ], legend: { isVisible: true, + showSingleSeries: true, position: 'right', }, preferredSeriesType: 'line', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx index cb683119384d9..8ed279ace28f6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.test.tsx @@ -10,6 +10,7 @@ import { fireEvent, screen } from '@testing-library/react'; import { Breakdowns } from './breakdowns'; import { mockIndexPattern, mockUxSeries, render } from '../../rtl_helpers'; import { getDefaultConfigs } from '../../configurations/default_configs'; +import { RECORDS_FIELD } from '../../configurations/constants'; import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fieldnames'; describe('Breakdowns', function () { @@ -56,6 +57,26 @@ describe('Breakdowns', function () { expect(setSeries).toHaveBeenCalledTimes(1); }); + it('does not show percentile breakdown for records metrics', function () { + const kpiConfig = getDefaultConfigs({ + reportType: 'kpi-over-time', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + render( + + ); + + fireEvent.click(screen.getByTestId('seriesBreakdown')); + + expect(screen.queryByText('Percentile')).not.toBeInTheDocument(); + }); + it('should disable breakdowns when a different series has a breakdown', function () { const initSeries = { data: [mockUxSeries, { ...mockUxSeries, breakdown: undefined }], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx index 7964abdeeddc5..a235cbd8852ad 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/breakdown/breakdowns.tsx @@ -10,7 +10,12 @@ import styled from 'styled-components'; import { EuiSuperSelect, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { useSeriesStorage } from '../../hooks/use_series_storage'; -import { LABEL_FIELDS_BREAKDOWN, USE_BREAK_DOWN_COLUMN } from '../../configurations/constants'; +import { + LABEL_FIELDS_BREAKDOWN, + USE_BREAK_DOWN_COLUMN, + RECORDS_FIELD, + PERCENTILE, +} from '../../configurations/constants'; import { SeriesConfig, SeriesUrl } from '../../types'; interface Props { @@ -51,6 +56,7 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { } const hasUseBreakdownColumn = seriesConfig.xAxisColumn.sourceField === USE_BREAK_DOWN_COLUMN; + const isRecordsMetric = series.selectedMetricField === RECORDS_FIELD; const items = seriesConfig.breakdownFields.map((breakdown) => ({ id: breakdown, @@ -64,11 +70,13 @@ export function Breakdowns({ seriesConfig, seriesId, series }: Props) { }); } - const options = items.map(({ id, label }) => ({ - inputDisplay: label, - value: id, - dropdownDisplay: label, - })); + const options = items + .map(({ id, label }) => ({ + inputDisplay: label, + value: id, + dropdownDisplay: label, + })) + .filter(({ value }) => !(value === PERCENTILE && isRecordsMetric)); let valueOfSelected = selectedBreakdown || (hasUseBreakdownColumn ? options[0].value : NO_BREAKDOWN); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx new file mode 100644 index 0000000000000..83958840f63d9 --- /dev/null +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { screen } from '@testing-library/react'; +import { ExpandedSeriesRow } from './expanded_series_row'; +import { mockIndexPattern, mockUxSeries, render } from '../rtl_helpers'; +import { getDefaultConfigs } from '../configurations/default_configs'; +import { PERCENTILE } from '../configurations/constants'; + +describe('ExpandedSeriesRow', function () { + const dataViewSeries = getDefaultConfigs({ + reportType: 'kpi-over-time', + indexPattern: mockIndexPattern, + dataType: 'ux', + }); + + it('should render properly', async function () { + render(); + + expect(screen.getByText('Breakdown by')).toBeInTheDocument(); + expect(screen.getByText('Operation')).toBeInTheDocument(); + }); + + it('should not display operation field when percentile breakdowns are applied', async function () { + render( + + ); + + expect(screen.queryByText('Operation')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx index ac71f4ff5abe0..180be1ac0414f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/expanded_series_row.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiHorizontalRule } from '@elastic/eui'; import { SeriesConfig, SeriesUrl } from '../types'; +import { PERCENTILE } from '../configurations/constants'; import { ReportDefinitionCol } from './columns/report_definition_col'; import { OperationTypeSelect } from './columns/operation_type_select'; import { parseCustomFieldName } from '../configurations/lens_attributes'; @@ -42,6 +43,9 @@ export function ExpandedSeriesRow(seriesProps: Props) { const columnType = getColumnType(seriesConfig, selectedMetricField); + // if the breakdown field is percentiles, we can't apply further operations + const hasPercentileBreakdown = series.breakdown === PERCENTILE; + return (
@@ -69,7 +73,7 @@ export function ExpandedSeriesRow(seriesProps: Props) { - {(hasOperationType || columnType === 'operation') && ( + {(hasOperationType || (columnType === 'operation' && !hasPercentileBreakdown)) && ( Date: Tue, 12 Oct 2021 12:39:43 -0400 Subject: [PATCH 082/287] [Uptime] [Synthetics Integration] add new advanced options (#112454) * refactor common fields * add ignore_https_errors and journey filters options * adjust formatters and normalizers * adjust content and hide fields when zip url is not defined * adjust content again * update tests * adjust tests * adjust tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../browser/advanced_fields.test.tsx | 45 ++++++- .../fleet_package/browser/advanced_fields.tsx | 102 +++++++++++++++- .../fleet_package/browser/formatters.ts | 5 + .../fleet_package/browser/normalizers.ts | 7 ++ .../fleet_package/browser/simple_fields.tsx | 93 +------------- .../fleet_package/common/common_fields.tsx | 113 ++++++++++++++++++ .../contexts/browser_context_advanced.tsx | 3 + ..._context.tsx => http_context_advanced.tsx} | 0 .../fleet_package/contexts/index.ts | 4 +- ...p_context.tsx => tcp_context_advanced.tsx} | 0 .../fleet_package/http/simple_fields.tsx | 92 +------------- .../fleet_package/icmp/simple_fields.tsx | 90 +------------- .../fleet_package/tcp/simple_fields.tsx | 93 +------------- .../public/components/fleet_package/types.tsx | 8 +- 14 files changed, 289 insertions(+), 366 deletions(-) create mode 100644 x-pack/plugins/uptime/public/components/fleet_package/common/common_fields.tsx rename x-pack/plugins/uptime/public/components/fleet_package/contexts/{advanced_fields_http_context.tsx => http_context_advanced.tsx} (100%) rename x-pack/plugins/uptime/public/components/fleet_package/contexts/{advanced_fields_tcp_context.tsx => tcp_context_advanced.tsx} (100%) diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx index aa1f7ca07e3d8..fabf6da49cf47 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.test.tsx @@ -9,10 +9,12 @@ import React from 'react'; import { fireEvent } from '@testing-library/react'; import { render } from '../../../lib/helper/rtl_helpers'; import { BrowserAdvancedFields } from './advanced_fields'; -import { ConfigKeys, IBrowserAdvancedFields } from '../types'; +import { ConfigKeys, IBrowserAdvancedFields, IBrowserSimpleFields } from '../types'; import { BrowserAdvancedFieldsContextProvider, + BrowserSimpleFieldsContextProvider, defaultBrowserAdvancedFields as defaultConfig, + defaultBrowserSimpleFields, } from '../contexts'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ @@ -20,11 +22,19 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ })); describe('', () => { - const WrappedComponent = ({ defaultValues }: { defaultValues?: IBrowserAdvancedFields }) => { + const WrappedComponent = ({ + defaultValues = defaultConfig, + defaultSimpleFields = defaultBrowserSimpleFields, + }: { + defaultValues?: IBrowserAdvancedFields; + defaultSimpleFields?: IBrowserSimpleFields; + }) => { return ( - - - + + + + + ); }; @@ -46,4 +56,29 @@ describe('', () => { expect(screenshots.value).toEqual('off'); }); + + it('only displayed filter options when zip url is truthy', () => { + const { queryByText, getByText, rerender } = render(); + + expect( + queryByText( + /Use these options to apply the selected monitor settings to a subset of the tests in your suite./ + ) + ).not.toBeInTheDocument(); + + rerender( + + ); + + expect( + getByText( + /Use these options to apply the selected monitor settings to a subset of the tests in your suite./ + ) + ).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx index 28e2e39c79554..61af9f8ec6143 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/advanced_fields.tsx @@ -10,13 +10,15 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiAccordion, EuiSelect, + EuiFieldText, + EuiCheckbox, EuiFormRow, EuiDescribedFormGroup, EuiSpacer, } from '@elastic/eui'; import { ComboBox } from '../combo_box'; -import { useBrowserAdvancedFieldsContext } from '../contexts'; +import { useBrowserAdvancedFieldsContext, useBrowserSimpleFieldsContext } from '../contexts'; import { ConfigKeys, ScreenshotOption } from '../types'; @@ -24,6 +26,7 @@ import { OptionalLabel } from '../optional_label'; export const BrowserAdvancedFields = () => { const { fields, setFields } = useBrowserAdvancedFieldsContext(); + const { fields: simpleFields } = useBrowserSimpleFieldsContext(); const handleInputChange = useCallback( ({ value, configKey }: { value: unknown; configKey: ConfigKeys }) => { @@ -39,6 +42,75 @@ export const BrowserAdvancedFields = () => { data-test-subj="syntheticsBrowserAdvancedFieldsAccordion" > + {simpleFields[ConfigKeys.SOURCE_ZIP_URL] && ( + + +

+ } + description={ + + } + > + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ + value: event.target.value, + configKey: ConfigKeys.JOURNEY_FILTERS_MATCH, + }) + } + data-test-subj="syntheticsBrowserJourneyFiltersMatch" + /> + + + } + labelAppend={} + helpText={ + + } + > + + handleInputChange({ value, configKey: ConfigKeys.JOURNEY_FILTERS_TAGS }) + } + data-test-subj="syntheticsBrowserJourneyFiltersTags" + /> + + + )} @@ -56,6 +128,34 @@ export const BrowserAdvancedFields = () => { } > + + + + } + data-test-subj="syntheticsBrowserIgnoreHttpsErrors" + > + + } + onChange={(event) => + handleInputChange({ + value: event.target.checked, + configKey: ConfigKeys.IGNORE_HTTPS_ERRORS, + }) + } + /> + arrayToJsonFormatter(fields[ConfigKeys.SYNTHETICS_ARGS]), + [ConfigKeys.JOURNEY_FILTERS_MATCH]: (fields) => + stringToJsonFormatter(fields[ConfigKeys.JOURNEY_FILTERS_MATCH]), + [ConfigKeys.JOURNEY_FILTERS_TAGS]: (fields) => + arrayToJsonFormatter(fields[ConfigKeys.JOURNEY_FILTERS_TAGS]), + [ConfigKeys.IGNORE_HTTPS_ERRORS]: null, ...commonFormatters, }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts index 53bbf611d490c..0107fb3884f41 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts @@ -39,5 +39,12 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS), [ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS), [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.SYNTHETICS_ARGS), + [ConfigKeys.JOURNEY_FILTERS_MATCH]: getBrowserJsonToJavascriptNormalizer( + ConfigKeys.JOURNEY_FILTERS_MATCH + ), + [ConfigKeys.JOURNEY_FILTERS_TAGS]: getBrowserJsonToJavascriptNormalizer( + ConfigKeys.JOURNEY_FILTERS_TAGS + ), + [ConfigKeys.IGNORE_HTTPS_ERRORS]: getBrowserNormalizer(ConfigKeys.IGNORE_HTTPS_ERRORS), ...commonNormalizers, }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx index 0e2f10b96fe6d..7c7a6b199adcb 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -7,13 +7,12 @@ import React, { memo, useMemo, useCallback } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; +import { EuiFormRow } from '@elastic/eui'; import { ConfigKeys, Validation } from '../types'; import { useBrowserSimpleFieldsContext } from '../contexts'; -import { ComboBox } from '../combo_box'; -import { OptionalLabel } from '../optional_label'; import { ScheduleField } from '../schedule_field'; import { SourceField } from './source_field'; +import { CommonFields } from '../common/common_fields'; interface Props { validate: Validation; @@ -91,93 +90,7 @@ export const BrowserSimpleFields = memo(({ validate }) => { )} /> - - } - labelAppend={} - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.APM_SERVICE_NAME, - }) - } - data-test-subj="syntheticsAPMServiceName" - /> - - - } - isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} - error={ - parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( - - ) : ( - - ) - } - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.TIMEOUT, - }) - } - step={'any'} - /> - - - } - labelAppend={} - helpText={ - - } - > - handleInputChange({ value, configKey: ConfigKeys.TAGS })} - data-test-subj="syntheticsTags" - /> - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/common_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/common/common_fields.tsx new file mode 100644 index 0000000000000..57d5094958ca3 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/common_fields.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; +import { ConfigKeys, Validation, ICommonFields } from '../types'; +import { ComboBox } from '../combo_box'; +import { OptionalLabel } from '../optional_label'; + +interface Props { + validate: Validation; + fields: ICommonFields; + onChange: ({ value, configKey }: { value: string | string[]; configKey: ConfigKeys }) => void; +} + +export function CommonFields({ fields, onChange, validate }: Props) { + return ( + <> + + } + labelAppend={} + helpText={ + + } + > + + onChange({ + value: event.target.value, + configKey: ConfigKeys.APM_SERVICE_NAME, + }) + } + data-test-subj="syntheticsAPMServiceName" + /> + + + } + isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} + error={ + parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( + + ) : ( + + ) + } + helpText={ + + } + > + + onChange({ + value: event.target.value, + configKey: ConfigKeys.TIMEOUT, + }) + } + step={'any'} + /> + + + } + labelAppend={} + helpText={ + + } + > + onChange({ value, configKey: ConfigKeys.TAGS })} + data-test-subj="syntheticsTags" + /> + + + ); +} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx index 3f3bb8f14c269..bc766462f18ae 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/browser_context_advanced.tsx @@ -22,6 +22,9 @@ interface IBrowserAdvancedFieldsContextProvider { export const initialValues: IBrowserAdvancedFields = { [ConfigKeys.SCREENSHOTS]: ScreenshotOption.ON, [ConfigKeys.SYNTHETICS_ARGS]: [], + [ConfigKeys.JOURNEY_FILTERS_MATCH]: '', + [ConfigKeys.JOURNEY_FILTERS_TAGS]: [], + [ConfigKeys.IGNORE_HTTPS_ERRORS]: false, }; const defaultContext: IBrowserAdvancedFieldsContext = { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context_advanced.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_http_context.tsx rename to x-pack/plugins/uptime/public/components/fleet_package/contexts/http_context_advanced.tsx diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts index e955d2d7d4d50..4d76a6d8f8d67 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/contexts/index.ts @@ -21,7 +21,7 @@ export { HTTPAdvancedFieldsContextProvider, initialValues as defaultHTTPAdvancedFields, useHTTPAdvancedFieldsContext, -} from './advanced_fields_http_context'; +} from './http_context_advanced'; export { TCPSimpleFieldsContext, TCPSimpleFieldsContextProvider, @@ -39,7 +39,7 @@ export { TCPAdvancedFieldsContextProvider, initialValues as defaultTCPAdvancedFields, useTCPAdvancedFieldsContext, -} from './advanced_fields_tcp_context'; +} from './tcp_context_advanced'; export { BrowserSimpleFieldsContext, BrowserSimpleFieldsContextProvider, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx b/x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context_advanced.tsx similarity index 100% rename from x-pack/plugins/uptime/public/components/fleet_package/contexts/advanced_fields_tcp_context.tsx rename to x-pack/plugins/uptime/public/components/fleet_package/contexts/tcp_context_advanced.tsx diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx index c4de1d53fe998..90f94324fe657 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/simple_fields.tsx @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; import { ConfigKeys, Validation } from '../types'; import { useHTTPSimpleFieldsContext } from '../contexts'; -import { ComboBox } from '../combo_box'; import { OptionalLabel } from '../optional_label'; import { ScheduleField } from '../schedule_field'; +import { CommonFields } from '../common/common_fields'; interface Props { validate: Validation; @@ -50,7 +50,7 @@ export const HTTPSimpleFields = memo(({ validate }) => { /> (({ validate }) => { unit={fields[ConfigKeys.SCHEDULE].unit} /> - - } - labelAppend={} - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.APM_SERVICE_NAME, - }) - } - data-test-subj="syntheticsAPMServiceName" - /> - (({ validate }) => { } /> - - } - isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} - error={ - parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( - - ) : ( - - ) - } - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.TIMEOUT, - }) - } - step={'any'} - /> - - - } - labelAppend={} - helpText={ - - } - > - handleInputChange({ value, configKey: ConfigKeys.TAGS })} - data-test-subj="syntheticsTags" - /> - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 92afe4c5072e1..32c843f1ce114 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -10,9 +10,9 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; import { ConfigKeys, Validation } from '../types'; import { useICMPSimpleFieldsContext } from '../contexts'; -import { ComboBox } from '../combo_box'; import { OptionalLabel } from '../optional_label'; import { ScheduleField } from '../schedule_field'; +import { CommonFields } from '../common/common_fields'; interface Props { validate: Validation; @@ -113,93 +113,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { step={'any'} /> - - } - labelAppend={} - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.APM_SERVICE_NAME, - }) - } - data-test-subj="syntheticsAPMServiceName" - /> - - - } - isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} - error={ - parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( - - ) : ( - - ) - } - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.TIMEOUT, - }) - } - step={'any'} - /> - - - } - labelAppend={} - helpText={ - - } - > - handleInputChange({ value, configKey: ConfigKeys.TAGS })} - data-test-subj="syntheticsTags" - /> - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index 37f0c82595e02..53a0074a47d73 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -7,12 +7,11 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiFormRow, EuiFieldText, EuiFieldNumber } from '@elastic/eui'; +import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { ConfigKeys, Validation } from '../types'; import { useTCPSimpleFieldsContext } from '../contexts'; -import { ComboBox } from '../combo_box'; -import { OptionalLabel } from '../optional_label'; import { ScheduleField } from '../schedule_field'; +import { CommonFields } from '../common/common_fields'; interface Props { validate: Validation; @@ -80,93 +79,7 @@ export const TCPSimpleFields = memo(({ validate }) => { unit={fields[ConfigKeys.SCHEDULE].unit} /> - - } - labelAppend={} - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.APM_SERVICE_NAME, - }) - } - data-test-subj="syntheticsAPMServiceName" - /> - - - } - isInvalid={!!validate[ConfigKeys.TIMEOUT]?.(fields)} - error={ - parseInt(fields[ConfigKeys.TIMEOUT], 10) < 0 ? ( - - ) : ( - - ) - } - helpText={ - - } - > - - handleInputChange({ - value: event.target.value, - configKey: ConfigKeys.TIMEOUT, - }) - } - step={'any'} - /> - - - } - labelAppend={} - helpText={ - - } - > - handleInputChange({ value, configKey: ConfigKeys.TAGS })} - data-test-subj="syntheticsTags" - /> - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx index 89581bf993339..db736f1bae4d2 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/types.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/types.tsx @@ -76,9 +76,13 @@ export enum ScreenshotOption { export enum ConfigKeys { APM_SERVICE_NAME = 'service.name', HOSTS = 'hosts', + IGNORE_HTTPS_ERRORS = 'ignore_https_errors', + JOURNEY_FILTERS_MATCH = 'filter_journeys.match', + JOURNEY_FILTERS_TAGS = 'filter_journeys.tags', MAX_REDIRECTS = 'max_redirects', MONITOR_TYPE = 'type', NAME = 'name', + PARAMS = 'params', PASSWORD = 'password', PROXY_URL = 'proxy_url', PROXY_USE_LOCAL_RESOLVER = 'proxy_use_local_resolver', @@ -101,7 +105,6 @@ export enum ConfigKeys { SOURCE_ZIP_PASSWORD = 'source.zip_url.password', SOURCE_ZIP_FOLDER = 'source.zip_url.folder', SYNTHETICS_ARGS = 'synthetics_args', - PARAMS = 'params', TLS_CERTIFICATE_AUTHORITIES = 'ssl.certificate_authorities', TLS_CERTIFICATE = 'ssl.certificate', TLS_KEY = 'ssl.key', @@ -198,6 +201,9 @@ export type IBrowserSimpleFields = { export interface IBrowserAdvancedFields { [ConfigKeys.SYNTHETICS_ARGS]: string[]; [ConfigKeys.SCREENSHOTS]: string; + [ConfigKeys.JOURNEY_FILTERS_MATCH]: string; + [ConfigKeys.JOURNEY_FILTERS_TAGS]: string[]; + [ConfigKeys.IGNORE_HTTPS_ERRORS]: boolean; } export type HTTPFields = IHTTPSimpleFields & IHTTPAdvancedFields & ITLSFields; From ec0fdee81451de6eea9327264acbff8a3abfdf0f Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Tue, 12 Oct 2021 12:28:14 -0500 Subject: [PATCH 083/287] [Workplace Search] Wire up write view for Sync Frequency (#114522) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor to use correct kea way of doing reset The listener is not needed as the actions can set the state themselves * Wire up reset button for Frequency section * Extract shareble updateServerSettings method We use a PATCH call to the server to update the state. We can extract this to a shared actions.updateServerSettings method that can be used in the main toggle on the Synchronization landing page, the Frequency section and the Object and assets section. As a part of this commit, I refactored `updateSyncEnabled` to use this method as well. In doing so, to simplify things, I removed the granular enabled/disabled message in favor of the generic “"Source synchronization settings updated.” message that the other sections use. The state of the toggle is a good enough indication of the state the server is in. I also renamed the updateSyncSettings method to updateObjectsAndAssetsSettings, as it was incorrectly named. * Add schema for schedules * Use mutable schedule in component Originally used the immutable version directly on the content source for initial read view. We now use a mutable one found in SynchronizationLogic * Set local copies of schedule after persisting to server * Wire up form change handlers We pass the ‘type’ to the component to inform the logic file which section is being changed. We then update the mutable `schedule` reducer with the correct value * Wire up save button and persist changes * Add type and make server prop optional If there are no blciked windows, we send undefined, so the type has been updated. * Add unsaved changes propmpt Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../applications/workplace_search/types.ts | 2 + .../components/synchronization/frequency.tsx | 19 +- .../synchronization/frequency_item.test.tsx | 49 ++++++ .../synchronization/frequency_item.tsx | 20 ++- .../objects_and_assets.test.tsx | 4 +- .../synchronization/objects_and_assets.tsx | 8 +- .../sync_frequency_tab.test.tsx | 7 +- .../synchronization/sync_frequency_tab.tsx | 32 ++-- .../synchronization_logic.test.ts | 163 ++++++++++-------- .../synchronization/synchronization_logic.ts | 163 +++++++++++++----- .../views/content_sources/constants.ts | 14 -- .../server/routes/workplace_search/sources.ts | 4 + 12 files changed, 331 insertions(+), 154 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index f81672e71e013..72bcf850fbcd9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -148,6 +148,8 @@ export interface IndexingSchedule extends SyncIndexItem { blockedWindows?: BlockedWindow[]; } +export type TimeUnit = 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'; + export type SyncJobType = 'full' | 'incremental' | 'delete' | 'permissions'; export const DAYS_OF_WEEK_VALUES = [ diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx index 914ec9dfe6eff..2ada5b64be889 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx @@ -21,6 +21,7 @@ import { } from '@elastic/eui'; import { SAVE_BUTTON_LABEL } from '../../../../../shared/constants'; +import { UnsavedChangesPrompt } from '../../../../../shared/unsaved_changes_prompt'; import { ViewContentHeader } from '../../../../components/shared/view_content_header'; import { NAV, RESET_BUTTON } from '../../../../constants'; import { DIFFERENT_SYNC_TYPES_DOCS_URL, SYNC_BEST_PRACTICES_DOCS_URL } from '../../../../routes'; @@ -30,6 +31,7 @@ import { BLOCKED_TIME_WINDOWS_TITLE, DIFFERENT_SYNC_TYPES_LINK_LABEL, SYNC_BEST_PRACTICES_LINK_LABEL, + SYNC_UNSAVED_CHANGES_MESSAGE, } from '../../constants'; import { SourceLogic } from '../../source_logic'; import { SourceLayout } from '../source_layout'; @@ -44,7 +46,10 @@ interface FrequencyProps { export const Frequency: React.FC = ({ tabId }) => { const { contentSource } = useValues(SourceLogic); - const { handleSelectedTabChanged } = useActions(SynchronizationLogic({ contentSource })); + const { hasUnsavedFrequencyChanges } = useValues(SynchronizationLogic({ contentSource })); + const { handleSelectedTabChanged, resetSyncSettings, updateFrequencySettings } = useActions( + SynchronizationLogic({ contentSource }) + ); const tabs = [ { @@ -66,10 +71,14 @@ export const Frequency: React.FC = ({ tabId }) => { const actions = ( - {RESET_BUTTON}{' '} + + {RESET_BUTTON} + - {SAVE_BUTTON_LABEL}{' '} + + {SAVE_BUTTON_LABEL} + ); @@ -98,6 +107,10 @@ export const Frequency: React.FC = ({ tabId }) => { pageViewTelemetry="source_synchronization_frequency" isLoading={false} > + { + const setSyncFrequency = jest.fn(); + const mockActions = { + setSyncFrequency, + }; + const mockValues = { + contentSource: fullContentSources[0], + }; + + beforeEach(() => { + setMockActions(mockActions); + setMockValues(mockValues); + }); + const estimate = { duration: 'PT3D', nextStart: '2021-09-27T21:39:24+00:00', @@ -22,6 +41,7 @@ describe('FrequencyItem', () => { }; const props = { + type: 'full' as SyncJobType, label: 'Item', description: 'My item', duration: 'PT2D', @@ -73,4 +93,33 @@ describe('FrequencyItem', () => { ).toEqual('in 2 days'); }); }); + + describe('onChange handlers', () => { + it('calls setSyncFrequency for "days"', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="durationDays"]') + .simulate('change', { target: { value: '3' } }); + + expect(setSyncFrequency).toHaveBeenCalledWith('full', '3', 'days'); + }); + + it('calls setSyncFrequency for "hours"', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="durationHours"]') + .simulate('change', { target: { value: '3' } }); + + expect(setSyncFrequency).toHaveBeenCalledWith('full', '3', 'hours'); + }); + + it('calls setSyncFrequency for "minutes"', () => { + const wrapper = shallow(); + wrapper + .find('[data-test-subj="durationMinutes"]') + .simulate('change', { target: { value: '3' } }); + + expect(setSyncFrequency).toHaveBeenCalledWith('full', '3', 'minutes'); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx index 618f5c73d6099..a51500e3076a8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency_item.tsx @@ -7,6 +7,7 @@ import React from 'react'; +import { useActions, useValues } from 'kea'; import moment from 'moment'; import { @@ -24,18 +25,30 @@ import { HOURS_UNIT_LABEL, DAYS_UNIT_LABEL, } from '../../../../../shared/constants'; -import { SyncEstimate } from '../../../../types'; +import { SyncEstimate, SyncJobType } from '../../../../types'; import { NEXT_SYNC_RUNNING_MESSAGE } from '../../constants'; +import { SourceLogic } from '../../source_logic'; + +import { SynchronizationLogic } from './synchronization_logic'; interface Props { + type: SyncJobType; label: string; description: string; duration: string; estimate: SyncEstimate; } -export const FrequencyItem: React.FC = ({ label, description, duration, estimate }) => { +export const FrequencyItem: React.FC = ({ + type, + label, + description, + duration, + estimate, +}) => { + const { contentSource } = useValues(SourceLogic); + const { setSyncFrequency } = useActions(SynchronizationLogic({ contentSource })); const { lastRun, nextStart, duration: durationEstimate } = estimate; const estimateDisplay = durationEstimate && moment.duration(durationEstimate).humanize(); const nextStartIsPast = moment().isAfter(nextStart); @@ -107,6 +120,7 @@ export const FrequencyItem: React.FC = ({ label, description, duration, e data-test-subj="durationDays" value={moment.duration(duration).days()} append={DAYS_UNIT_LABEL} + onChange={(e) => setSyncFrequency(type, e.target.value, 'days')} />
@@ -114,6 +128,7 @@ export const FrequencyItem: React.FC = ({ label, description, duration, e data-test-subj="durationHours" value={moment.duration(duration).hours()} append={HOURS_UNIT_LABEL} + onChange={(e) => setSyncFrequency(type, e.target.value, 'hours')} /> @@ -121,6 +136,7 @@ export const FrequencyItem: React.FC = ({ label, description, duration, e data-test-subj="durationMinutes" value={moment.duration(duration).minutes()} append={MINUTES_UNIT_LABEL} + onChange={(e) => setSyncFrequency(type, e.target.value, 'minutes')} /> diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx index 42a08084db418..ee865e4c7c4ea 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.test.tsx @@ -21,14 +21,14 @@ import { ObjectsAndAssets } from './objects_and_assets'; describe('ObjectsAndAssets', () => { const setThumbnailsChecked = jest.fn(); const setContentExtractionChecked = jest.fn(); - const updateSyncSettings = jest.fn(); + const updateObjectsAndAssetsSettings = jest.fn(); const resetSyncSettings = jest.fn(); const contentSource = fullContentSources[0]; const mockActions = { setThumbnailsChecked, setContentExtractionChecked, - updateSyncSettings, + updateObjectsAndAssetsSettings, resetSyncSettings, }; const mockValues = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx index 98abdb8bf67ea..d59c812d9ffa0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx @@ -47,7 +47,7 @@ export const ObjectsAndAssets: React.FC = () => { const { setThumbnailsChecked, setContentExtractionChecked, - updateSyncSettings, + updateObjectsAndAssetsSettings, resetSyncSettings, } = useActions(SynchronizationLogic({ contentSource })); @@ -61,7 +61,11 @@ export const ObjectsAndAssets: React.FC = () => { - + {SAVE_BUTTON_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx index 6c382b0addab8..c85f2df1adb03 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.test.tsx @@ -22,9 +22,12 @@ describe('SyncFrequency', () => { const sourceWithNoDLP = cloneDeep(contentSource); sourceWithNoDLP.indexing.schedule.permissions = undefined as any; sourceWithNoDLP.indexing.schedule.estimates.permissions = undefined as any; + const { + indexing: { schedule }, + } = contentSource; it('renders with DLP', () => { - setMockValues({ contentSource }); + setMockValues({ contentSource, schedule }); const wrapper = shallow(); expect(wrapper.find(FrequencyItem)).toHaveLength(4); @@ -32,7 +35,7 @@ describe('SyncFrequency', () => { it('renders without DLP', () => { setMockValues({ - contentSource: sourceWithNoDLP, + schedule: sourceWithNoDLP.indexing.schedule, }); const wrapper = shallow(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx index 2a0ccb1fdb2c9..4e79f22977855 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/sync_frequency_tab.tsx @@ -24,31 +24,30 @@ import { import { SourceLogic } from '../../source_logic'; import { FrequencyItem } from './frequency_item'; +import { SynchronizationLogic } from './synchronization_logic'; export const SyncFrequency: React.FC = () => { + const { contentSource } = useValues(SourceLogic); const { - contentSource: { - indexing: { - schedule: { - full: fullDuration, - incremental: incrementalDuration, - delete: deleteDuration, - permissions: permissionsDuration, - estimates: { - full: fullEstimate, - incremental: incrementalEstimate, - delete: deleteEstimate, - permissions: permissionsEstimate, - }, - }, + schedule: { + full: fullDuration, + incremental: incrementalDuration, + delete: deleteDuration, + permissions: permissionsDuration, + estimates: { + full: fullEstimate, + incremental: incrementalEstimate, + delete: deleteEstimate, + permissions: permissionsEstimate, }, }, - } = useValues(SourceLogic); + } = useValues(SynchronizationLogic({ contentSource })); return ( <> { /> { /> { <> { const defaultValues = { navigatingBetweenTabs: false, hasUnsavedObjectsAndAssetsChanges: false, - hasUnsavedFrequencyChanges: true, + hasUnsavedFrequencyChanges: false, contentExtractionChecked: true, thumbnailsChecked: true, blockedWindows: [], @@ -83,6 +83,35 @@ describe('SynchronizationLogic', () => { expect(SynchronizationLogic.values.contentExtractionChecked).toEqual(false); }); + + it('resetSyncSettings', () => { + SynchronizationLogic.actions.setContentExtractionChecked(false); + SynchronizationLogic.actions.setThumbnailsChecked(false); + SynchronizationLogic.actions.resetSyncSettings(); + + expect(SynchronizationLogic.values.thumbnailsChecked).toEqual(true); + expect(SynchronizationLogic.values.contentExtractionChecked).toEqual(true); + }); + + describe('setSyncFrequency', () => { + it('sets "days"', () => { + SynchronizationLogic.actions.setSyncFrequency('full', '1', 'days'); + + expect(SynchronizationLogic.values.schedule.full).toEqual('P1D'); + }); + + it('sets "hours"', () => { + SynchronizationLogic.actions.setSyncFrequency('full', '10', 'hours'); + + expect(SynchronizationLogic.values.schedule.full).toEqual('P1DT10H'); + }); + + it('sets "minutes"', () => { + SynchronizationLogic.actions.setSyncFrequency('full', '30', 'minutes'); + + expect(SynchronizationLogic.values.schedule.full).toEqual('P1DT30M'); + }); + }); }); describe('listeners', () => { @@ -110,103 +139,91 @@ describe('SynchronizationLogic', () => { }); describe('updateSyncEnabled', () => { - it('calls API and sets values for false value', async () => { - const setContentSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource'); - const promise = Promise.resolve(contentSource); - http.patch.mockReturnValue(promise); + it('calls updateServerSettings method', async () => { + const updateServerSettingsSpy = jest.spyOn( + SynchronizationLogic.actions, + 'updateServerSettings' + ); SynchronizationLogic.actions.updateSyncEnabled(false); - expect(http.patch).toHaveBeenCalledWith( - '/internal/workplace_search/org/sources/123/settings', - { - body: JSON.stringify({ - content_source: { - indexing: { enabled: false }, - }, - }), - } - ); - await promise; - expect(setContentSourceSpy).toHaveBeenCalledWith(contentSource); - expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization disabled.'); + expect(updateServerSettingsSpy).toHaveBeenCalledWith({ + content_source: { + indexing: { enabled: false }, + }, + }); }); + }); - it('calls API and sets values for true value', async () => { - const promise = Promise.resolve(contentSource); - http.patch.mockReturnValue(promise); - SynchronizationLogic.actions.updateSyncEnabled(true); - - expect(http.patch).toHaveBeenCalledWith( - '/internal/workplace_search/org/sources/123/settings', - { - body: JSON.stringify({ - content_source: { - indexing: { enabled: true }, - }, - }), - } + describe('updateObjectsAndAssetsSettings', () => { + it('calls updateServerSettings method', async () => { + const updateServerSettingsSpy = jest.spyOn( + SynchronizationLogic.actions, + 'updateServerSettings' ); - await promise; - expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization enabled.'); - }); - - it('handles error', async () => { - const error = { - response: { - error: 'this is an error', - status: 400, + SynchronizationLogic.actions.updateObjectsAndAssetsSettings(); + + expect(updateServerSettingsSpy).toHaveBeenCalledWith({ + content_source: { + indexing: { + features: { + content_extraction: { enabled: true }, + thumbnails: { enabled: true }, + }, + }, }, - }; - const promise = Promise.reject(error); - http.patch.mockReturnValue(promise); - SynchronizationLogic.actions.updateSyncEnabled(false); - await expectedAsyncError(promise); - - expect(flashAPIErrors).toHaveBeenCalledWith(error); + }); }); }); - describe('resetSyncSettings', () => { - it('calls methods', async () => { - const setThumbnailsCheckedSpy = jest.spyOn( + describe('updateFrequencySettings', () => { + it('calls updateServerSettings method', async () => { + const updateServerSettingsSpy = jest.spyOn( SynchronizationLogic.actions, - 'setThumbnailsChecked' + 'updateServerSettings' ); - const setContentExtractionCheckedSpy = jest.spyOn( - SynchronizationLogic.actions, - 'setContentExtractionChecked' - ); - SynchronizationLogic.actions.resetSyncSettings(); - - expect(setThumbnailsCheckedSpy).toHaveBeenCalledWith(true); - expect(setContentExtractionCheckedSpy).toHaveBeenCalledWith(true); + SynchronizationLogic.actions.updateFrequencySettings(); + + expect(updateServerSettingsSpy).toHaveBeenCalledWith({ + content_source: { + indexing: { + schedule: { + full: 'P1D', + incremental: 'PT2H', + delete: 'PT10M', + }, + }, + }, + }); }); }); - describe('updateSyncSettings', () => { + describe('updateServerSettings', () => { + const body = { + content_source: { + indexing: { + features: { + content_extraction: { enabled: true }, + thumbnails: { enabled: true }, + }, + }, + }, + }; it('calls API and sets values', async () => { const setContentSourceSpy = jest.spyOn(SourceLogic.actions, 'setContentSource'); + const setServerScheduleSpy = jest.spyOn(SynchronizationLogic.actions, 'setServerSchedule'); const promise = Promise.resolve(contentSource); http.patch.mockReturnValue(promise); - SynchronizationLogic.actions.updateSyncSettings(); + SynchronizationLogic.actions.updateServerSettings(body); expect(http.patch).toHaveBeenCalledWith( '/internal/workplace_search/org/sources/123/settings', { - body: JSON.stringify({ - content_source: { - indexing: { - features: { - content_extraction: { enabled: true }, - thumbnails: { enabled: true }, - }, - }, - }, - }), + body: JSON.stringify(body), } ); await promise; expect(setContentSourceSpy).toHaveBeenCalledWith(contentSource); + expect(setServerScheduleSpy).toHaveBeenCalledWith(contentSource.indexing.schedule); expect(flashSuccessToast).toHaveBeenCalledWith('Source synchronization settings updated.'); }); @@ -219,7 +236,7 @@ describe('SynchronizationLogic', () => { }; const promise = Promise.reject(error); http.patch.mockReturnValue(promise); - SynchronizationLogic.actions.updateSyncSettings(); + SynchronizationLogic.actions.updateServerSettings(body); await expectedAsyncError(promise); expect(flashAPIErrors).toHaveBeenCalledWith(error); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts index 3aaa7f5fdfbf3..95dbb8c75fce4 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization_logic.ts @@ -20,24 +20,54 @@ import { BLOCKED_TIME_WINDOWS_PATH, getContentSourcePath, } from '../../../../routes'; -import { BlockedWindow, IndexingSchedule } from '../../../../types'; +import { BlockedWindow, IndexingSchedule, SyncJobType, TimeUnit } from '../../../../types'; -import { - SYNC_ENABLED_MESSAGE, - SYNC_DISABLED_MESSAGE, - SYNC_SETTINGS_UPDATED_MESSAGE, -} from '../../constants'; +import { SYNC_SETTINGS_UPDATED_MESSAGE } from '../../constants'; import { SourceLogic } from '../../source_logic'; +interface ServerBlockedWindow { + job_type: string; + day: string; + start: string; + end: string; +} + +interface ServerSyncSettingsBody { + content_source: { + indexing: { + enabled?: boolean; + features?: { + content_extraction: { enabled: boolean }; + thumbnails: { enabled: boolean }; + }; + schedule?: { + full: string; + incremental: string; + delete: string; + permissions?: string; + blocked_windows?: ServerBlockedWindow[]; + }; + }; + }; +} + interface SynchronizationActions { setNavigatingBetweenTabs(navigatingBetweenTabs: boolean): boolean; handleSelectedTabChanged(tabId: TabId): TabId; addBlockedWindow(): void; - updateSyncSettings(): void; + updateFrequencySettings(): void; + updateObjectsAndAssetsSettings(): void; resetSyncSettings(): void; updateSyncEnabled(enabled: boolean): boolean; setThumbnailsChecked(checked: boolean): boolean; + setSyncFrequency( + type: SyncJobType, + value: string, + unit: TimeUnit + ): { type: SyncJobType; value: number; unit: TimeUnit }; setContentExtractionChecked(checked: boolean): boolean; + setServerSchedule(schedule: IndexingSchedule): IndexingSchedule; + updateServerSettings(body: ServerSyncSettingsBody): ServerSyncSettingsBody; } interface SynchronizationValues { @@ -67,8 +97,16 @@ export const SynchronizationLogic = kea< handleSelectedTabChanged: (tabId: TabId) => tabId, updateSyncEnabled: (enabled: boolean) => enabled, setThumbnailsChecked: (checked: boolean) => checked, + setSyncFrequency: (type: SyncJobType, value: string, unit: TimeUnit) => ({ + type, + value, + unit, + }), setContentExtractionChecked: (checked: boolean) => checked, - updateSyncSettings: true, + updateServerSettings: (body: ServerSyncSettingsBody) => body, + setServerSchedule: (schedule: IndexingSchedule) => schedule, + updateFrequencySettings: true, + updateObjectsAndAssetsSettings: true, resetSyncSettings: true, addBlockedWindow: true, }, @@ -89,16 +127,57 @@ export const SynchronizationLogic = kea< props.contentSource.indexing.features.thumbnails.enabled, { setThumbnailsChecked: (_, thumbnailsChecked) => thumbnailsChecked, + resetSyncSettings: () => props.contentSource.indexing.features.thumbnails.enabled, }, ], contentExtractionChecked: [ props.contentSource.indexing.features.contentExtraction.enabled, { setContentExtractionChecked: (_, contentExtractionChecked) => contentExtractionChecked, + resetSyncSettings: () => props.contentSource.indexing.features.contentExtraction.enabled, + }, + ], + cachedSchedule: [ + stripScheduleSeconds(props.contentSource.indexing.schedule), + { + setServerSchedule: (_, schedule) => schedule, + }, + ], + schedule: [ + stripScheduleSeconds(props.contentSource.indexing.schedule), + { + resetSyncSettings: () => stripScheduleSeconds(props.contentSource.indexing.schedule), + setServerSchedule: (_, schedule) => schedule, + setSyncFrequency: (state, { type, value, unit }) => { + let currentValue; + const schedule = cloneDeep(state); + const duration = schedule[type]; + + switch (unit) { + case 'days': + currentValue = moment.duration(duration).days(); + break; + case 'hours': + currentValue = moment.duration(duration).hours(); + break; + default: + currentValue = moment.duration(duration).minutes(); + break; + } + + // momentJS doesn't seem to have a way to simply set the minutes/hours/days, so we have + // to subtract the current value and then add the new value. + // https://momentjs.com/docs/#/durations/ + schedule[type] = moment + .duration(duration) + .subtract(currentValue, unit) + .add(value, unit) + .toISOString(); + + return schedule; + }, }, ], - cachedSchedule: [stripScheduleSeconds(props.contentSource.indexing.schedule)], - schedule: [stripScheduleSeconds(props.contentSource.indexing.schedule)], }), selectors: ({ selectors }) => ({ hasUnsavedObjectsAndAssetsChanges: [ @@ -125,7 +204,7 @@ export const SynchronizationLogic = kea< ], hasUnsavedFrequencyChanges: [ () => [selectors.cachedSchedule, selectors.schedule], - (cachedSchedule, schedule) => isEqual(cachedSchedule, schedule), + (cachedSchedule, schedule) => !isEqual(cachedSchedule, schedule), ], }), listeners: ({ actions, values, props }) => ({ @@ -149,46 +228,48 @@ export const SynchronizationLogic = kea< actions.setNavigatingBetweenTabs(false); }, updateSyncEnabled: async (enabled) => { - const { id: sourceId } = props.contentSource; - const route = `/internal/workplace_search/org/sources/${sourceId}/settings`; - const successMessage = enabled ? SYNC_ENABLED_MESSAGE : SYNC_DISABLED_MESSAGE; - - try { - const response = await HttpLogic.values.http.patch(route, { - body: JSON.stringify({ content_source: { indexing: { enabled } } }), - }); - - SourceLogic.actions.setContentSource(response); - flashSuccessToast(successMessage); - } catch (e) { - flashAPIErrors(e); - } + actions.updateServerSettings({ + content_source: { + indexing: { enabled }, + }, + }); }, - resetSyncSettings: () => { - actions.setThumbnailsChecked(props.contentSource.indexing.features.thumbnails.enabled); - actions.setContentExtractionChecked( - props.contentSource.indexing.features.contentExtraction.enabled - ); + updateObjectsAndAssetsSettings: () => { + actions.updateServerSettings({ + content_source: { + indexing: { + features: { + content_extraction: { enabled: values.contentExtractionChecked }, + thumbnails: { enabled: values.thumbnailsChecked }, + }, + }, + }, + }); + }, + updateFrequencySettings: () => { + actions.updateServerSettings({ + content_source: { + indexing: { + schedule: { + full: values.schedule.full, + incremental: values.schedule.incremental, + delete: values.schedule.delete, + }, + }, + }, + }); }, - updateSyncSettings: async () => { + updateServerSettings: async (body: ServerSyncSettingsBody) => { const { id: sourceId } = props.contentSource; const route = `/internal/workplace_search/org/sources/${sourceId}/settings`; try { const response = await HttpLogic.values.http.patch(route, { - body: JSON.stringify({ - content_source: { - indexing: { - features: { - content_extraction: { enabled: values.contentExtractionChecked }, - thumbnails: { enabled: values.thumbnailsChecked }, - }, - }, - }, - }), + body: JSON.stringify(body), }); SourceLogic.actions.setContentSource(response); + SynchronizationLogic.actions.setServerSchedule(response.indexing.schedule); flashSuccessToast(SYNC_SETTINGS_UPDATED_MESSAGE); } catch (e) { flashAPIErrors(e); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index 1f76d667949fb..91e32834f3fbd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -705,20 +705,6 @@ export const BLOCKED_EMPTY_STATE_DESCRIPTION = i18n.translate( } ); -export const SYNC_ENABLED_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.syncEnabledMessage', - { - defaultMessage: 'Source synchronization enabled.', - } -); - -export const SYNC_DISABLED_MESSAGE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.syncDisabledMessage', - { - defaultMessage: 'Source synchronization disabled.', - } -); - export const SYNC_SETTINGS_UPDATED_MESSAGE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.syncSettingsUpdatedMessage', { diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 69a6470b5b9ce..660294a5e1ddd 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -79,6 +79,10 @@ const sourceSettingsSchema = schema.object({ ), schedule: schema.maybe( schema.object({ + full: schema.maybe(schema.string()), + incremental: schema.maybe(schema.string()), + delete: schema.maybe(schema.string()), + permissions: schema.maybe(schema.string()), blocked_windows: schema.maybe( schema.arrayOf( schema.object({ From 0fa95fdfcb3167d0fc29462684b84512f441f65c Mon Sep 17 00:00:00 2001 From: Nicolas Chaulet Date: Tue, 12 Oct 2021 13:30:48 -0400 Subject: [PATCH 084/287] [Fleet] Handle existing default output in preconfiguration output service (#114631) --- .../server/services/preconfiguration.test.ts | 64 +++++++++++++++++++ .../fleet/server/services/preconfiguration.ts | 16 ++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts index d0ae995358632..102b059515151 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.test.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.test.ts @@ -503,6 +503,8 @@ describe('output preconfiguration', () => { beforeEach(() => { mockedOutputService.create.mockReset(); mockedOutputService.update.mockReset(); + mockedOutputService.delete.mockReset(); + mockedOutputService.getDefaultOutputId.mockReset(); mockedOutputService.getDefaultESHosts.mockReturnValue(['http://default-es:9200']); mockedOutputService.bulkGet.mockImplementation(async (soClient, id): Promise => { return [ @@ -537,6 +539,26 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); }); + it('should delete existing default output if a new preconfigured output is added', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-output-123'); + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'non-existing-default-output-1', + name: 'Output 1', + type: 'elasticsearch', + is_default: true, + hosts: ['http://test.fr'], + }, + ]); + + expect(mockedOutputService.delete).toBeCalled(); + expect(mockedOutputService.create).toBeCalled(); + expect(mockedOutputService.update).not.toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).not.toBeCalled(); + }); + it('should set default hosts if hosts is not set output that does not exists', async () => { const soClient = savedObjectsClientMock.create(); const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; @@ -572,6 +594,48 @@ describe('output preconfiguration', () => { expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); }); + it('should delete default output if preconfigured output exists and another default output exists', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); + mockedOutputService.getDefaultOutputId.mockResolvedValue('default-123'); + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-output-1', + is_default: true, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://newhostichanged.co:9201'], // field that changed + }, + ]); + + expect(mockedOutputService.delete).toBeCalled(); + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + + it('should not delete default output if preconfigured default output exists and changed', async () => { + const soClient = savedObjectsClientMock.create(); + const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; + soClient.find.mockResolvedValue({ saved_objects: [], page: 0, per_page: 0, total: 0 }); + mockedOutputService.getDefaultOutputId.mockResolvedValue('existing-output-1'); + await ensurePreconfiguredOutputs(soClient, esClient, [ + { + id: 'existing-output-1', + is_default: true, + name: 'Output 1', + type: 'elasticsearch', + hosts: ['http://newhostichanged.co:9201'], // field that changed + }, + ]); + + expect(mockedOutputService.delete).not.toBeCalled(); + expect(mockedOutputService.create).not.toBeCalled(); + expect(mockedOutputService.update).toBeCalled(); + expect(spyAgentPolicyServicBumpAllAgentPoliciesForOutput).toBeCalled(); + }); + const SCENARIOS: Array<{ name: string; data: PreconfiguredOutput }> = [ { name: 'no changes', diff --git a/x-pack/plugins/fleet/server/services/preconfiguration.ts b/x-pack/plugins/fleet/server/services/preconfiguration.ts index a444f8bdaa4da..a878af64aa05e 100644 --- a/x-pack/plugins/fleet/server/services/preconfiguration.ts +++ b/x-pack/plugins/fleet/server/services/preconfiguration.ts @@ -95,9 +95,21 @@ export async function ensurePreconfiguredOutputs( data.hosts = outputService.getDefaultESHosts(); } - if (!existingOutput) { + const isCreate = !existingOutput; + const isUpdateWithNewData = + existingOutput && isPreconfiguredOutputDifferentFromCurrent(existingOutput, data); + // If a default output already exists, delete it in favor of the preconfigured one + if (isCreate || isUpdateWithNewData) { + const defaultOutputId = await outputService.getDefaultOutputId(soClient); + + if (defaultOutputId && defaultOutputId !== output.id) { + await outputService.delete(soClient, defaultOutputId); + } + } + + if (isCreate) { await outputService.create(soClient, data, { id, overwrite: true }); - } else if (isPreconfiguredOutputDifferentFromCurrent(existingOutput, data)) { + } else if (isUpdateWithNewData) { await outputService.update(soClient, id, data); // Bump revision of all policies using that output if (outputData.is_default) { From e9f0d7b9b466123da25dcbe5f5cbab063843f5db Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Tue, 12 Oct 2021 19:46:58 +0200 Subject: [PATCH 085/287] Short url docs (#113084) --- docs/api/short-urls.asciidoc | 9 ++ docs/api/short-urls/create-short-url.asciidoc | 86 +++++++++++++++++++ docs/api/short-urls/delete-short-url.asciidoc | 39 +++++++++ docs/api/short-urls/get-short-url.asciidoc | 56 ++++++++++++ .../api/short-urls/resolve-short-url.asciidoc | 56 ++++++++++++ docs/api/url-shortening.asciidoc | 62 ------------- docs/user/api.asciidoc | 16 ++-- 7 files changed, 254 insertions(+), 70 deletions(-) create mode 100644 docs/api/short-urls.asciidoc create mode 100644 docs/api/short-urls/create-short-url.asciidoc create mode 100644 docs/api/short-urls/delete-short-url.asciidoc create mode 100644 docs/api/short-urls/get-short-url.asciidoc create mode 100644 docs/api/short-urls/resolve-short-url.asciidoc delete mode 100644 docs/api/url-shortening.asciidoc diff --git a/docs/api/short-urls.asciidoc b/docs/api/short-urls.asciidoc new file mode 100644 index 0000000000000..ded639c897f3f --- /dev/null +++ b/docs/api/short-urls.asciidoc @@ -0,0 +1,9 @@ +[[short-urls-api]] +== Short URLs APIs + +Manage {kib} short URLs. + +include::short-urls/create-short-url.asciidoc[] +include::short-urls/get-short-url.asciidoc[] +include::short-urls/delete-short-url.asciidoc[] +include::short-urls/resolve-short-url.asciidoc[] diff --git a/docs/api/short-urls/create-short-url.asciidoc b/docs/api/short-urls/create-short-url.asciidoc new file mode 100644 index 0000000000000..a9138a4c555da --- /dev/null +++ b/docs/api/short-urls/create-short-url.asciidoc @@ -0,0 +1,86 @@ +[[short-urls-api-create]] +=== Create short URL API +++++ +Create short URL +++++ + +experimental[] Create a {kib} short URL. {kib} URLs may be long and cumbersome, short URLs are much +easier to remember and share. + +Short URLs are created by specifying the locator ID and locator parameters. When a short URL is +resolved, the locator ID and locator parameters are used to redirect user to the right {kib} page. + + +[[short-urls-api-create-request]] +==== Request + +`POST :/api/short_url` + + +[[short-urls-api-create-request-body]] +==== Request body + +`locatorId`:: + (Required, string) ID of the locator. + +`params`:: + (Required, object) Object which contains all necessary parameters for the given locator to resolve + to a {kib} location. ++ +WARNING: When you create a short URL, locator params are not validated, which allows you to pass +arbitrary and ill-formed data into the API that can break {kib}. Make sure +any data that you send to the API is properly formed. + +`slug`:: + (Optional, string) A custom short URL slug. Slug is the part of the short URL that identifies it. + You can provide a custom slug which consists of latin alphabet letters, numbers and `-._` + characters. The slug must be at least 3 characters long, but no longer than 255 characters. + +`humanReadableSlug`:: + (Optional, boolean) When the `slug` parameter is omitted, the API will generate a random + human-readable slug, if `humanReadableSlug` is set to `true`. + + +[[short-urls-api-create-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + + +[[short-urls-api-create-example]] +==== Example + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/short_url -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d ' +{ + "locatorId": "LOCATOR_ID", + "params": {}, + "humanReadableSlug": true +}' +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", <1> + "slug": "adjective-adjective-noun", <2> + "locator": { + "id": "LOCATOR_ID", + "version": "x.x.x", <3> + "state": {} <4> + }, + "accessCount": 0, + "accessDate": 1632680100000, + "createDate": 1632680100000 +} +-------------------------------------------------- + +<1> A random ID is automatically generated. +<2> A random human-readable slug is automatically generated if the `humanReadableSlug` parameter is set to `true`. If set to `false` a random short string is generated. +<3> The version of {kib} when short URL was created is stored. +<4> Locator params provided as `params` property are stored. diff --git a/docs/api/short-urls/delete-short-url.asciidoc b/docs/api/short-urls/delete-short-url.asciidoc new file mode 100644 index 0000000000000..f405ccf1a7472 --- /dev/null +++ b/docs/api/short-urls/delete-short-url.asciidoc @@ -0,0 +1,39 @@ +[[short-urls-api-delete]] +=== Delete short URL API +++++ +Delete short URL +++++ + +experimental[] Delete a {kib} short URL. + + +[[short-urls-api-delete-request]] +==== Request + +`DELETE :/api/short_url/` + + +[[short-urls-api-delete-path-params]] +==== Path parameters + +`id`:: + (Required, string) The short URL ID that you want to remove. + + +[[short-urls-api-delete-response-codes]] +==== Response code + +`200`:: + Indicates a successful call. + + +[[short-urls-api-delete-example]] +==== Example + +Delete a short URL `12345` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X DELETE api/short_url/12345 +-------------------------------------------------- +// KIBANA diff --git a/docs/api/short-urls/get-short-url.asciidoc b/docs/api/short-urls/get-short-url.asciidoc new file mode 100644 index 0000000000000..bf4303d442fb0 --- /dev/null +++ b/docs/api/short-urls/get-short-url.asciidoc @@ -0,0 +1,56 @@ +[[short-urls-api-get]] +=== Get short URL API +++++ +Get short URL +++++ + +experimental[] Retrieve a single {kib} short URL. + +[[short-urls-api-get-request]] +==== Request + +`GET :/api/short_url/` + + +[[short-urls-api-get-params]] +==== Path parameters + +`id`:: + (Required, string) The ID of the short URL. + + +[[short-urls-api-get-codes]] +==== Response code + +`200`:: + Indicates a successful call. + + +[[short-urls-api-get-example]] +==== Example + +Retrieve the short URL with the `12345` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/short_url/12345 +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "12345", + "slug": "adjective-adjective-noun", + "locator": { + "id": "LOCATOR_ID", + "version": "x.x.x", + "state": {} + }, + "accessCount": 0, + "accessDate": 1632680100000, + "createDate": 1632680100000 +} +-------------------------------------------------- diff --git a/docs/api/short-urls/resolve-short-url.asciidoc b/docs/api/short-urls/resolve-short-url.asciidoc new file mode 100644 index 0000000000000..32ad7ba7625c0 --- /dev/null +++ b/docs/api/short-urls/resolve-short-url.asciidoc @@ -0,0 +1,56 @@ +[[short-urls-api-resolve]] +=== Resolve short URL API +++++ +Resolve short URL +++++ + +experimental[] Resolve a single {kib} short URL by its slug. + +[[short-urls-api-resolve-request]] +==== Request + +`GET :/api/short_url/_slug/` + + +[[short-urls-api-resolve-params]] +==== Path parameters + +`slug`:: + (Required, string) The slug of the short URL. + + +[[short-urls-api-resolve-codes]] +==== Response code + +`200`:: + Indicates a successful call. + + +[[short-urls-api-resolve-example]] +==== Example + +Retrieve the short URL with the `hello-world` ID: + +[source,sh] +-------------------------------------------------- +$ curl -X GET api/short_url/_slug/hello-world +-------------------------------------------------- +// KIBANA + +The API returns the following: + +[source,sh] +-------------------------------------------------- +{ + "id": "12345", + "slug": "hello-world", + "locator": { + "id": "LOCATOR_ID", + "version": "x.x.x", + "state": {} + }, + "accessCount": 0, + "accessDate": 1632680100000, + "createDate": 1632680100000 +} +-------------------------------------------------- diff --git a/docs/api/url-shortening.asciidoc b/docs/api/url-shortening.asciidoc deleted file mode 100644 index ffe1d925e5dcb..0000000000000 --- a/docs/api/url-shortening.asciidoc +++ /dev/null @@ -1,62 +0,0 @@ -[[url-shortening-api]] -== Shorten URL API -++++ -Shorten URL -++++ - -experimental[] Convert a {kib} URL into a token. {kib} URLs contain the state of the application, which makes them long and cumbersome. -Internet Explorer has URL length restrictions, and some wiki and markup parsers don't do well with the full-length version of the {kib} URL. - -Short URLs are designed to make sharing {kib} URLs easier. - -[float] -[[url-shortening-api-request]] -=== Request - -`POST :/api/shorten_url` - -[float] -[[url-shortening-api-request-body]] -=== Request body - -`url`:: - (Required, string) The {kib} URL that you want to shorten, relative to `/app/kibana`. - -[float] -[[url-shortening-api-response-body]] -=== Response body - -urlId:: A top-level property that contains the shortened URL token for the provided request body. - -[float] -[[url-shortening-api-codes]] -=== Response code - -`200`:: - Indicates a successful call. - -[float] -[[url-shortening-api-example]] -=== Example - -[source,sh] --------------------------------------------------- -$ curl -X POST api/shorten_url -{ - "url": "/app/kibana#/dashboard?_g=()&_a=(description:'',filters:!(),fullScreenMode:!f,options:(hidePanelTitles:!f,useMargins:!t),panels:!((embeddableConfig:(),gridData:(h:15,i:'1',w:24,x:0,y:0),id:'8f4d0c00-4c86-11e8-b3d7-01146121b73d',panelIndex:'1',type:visualization,version:'7.0.0-alpha1')),query:(language:lucene,query:''),timeRestore:!f,title:'New%20Dashboard',viewMode:edit)" -} --------------------------------------------------- -// KIBANA - -The API returns the following: - -[source,sh] --------------------------------------------------- -{ - "urlId": "f73b295ff92718b26bc94edac766d8e3" -} --------------------------------------------------- - -For easy sharing, construct the shortened {kib} URL: - -`http://localhost:5601/goto/f73b295ff92718b26bc94edac766d8e3` diff --git a/docs/user/api.asciidoc b/docs/user/api.asciidoc index 12e200bb0ba27..e17d52675437e 100644 --- a/docs/user/api.asciidoc +++ b/docs/user/api.asciidoc @@ -1,8 +1,8 @@ [[api]] = REST API -Some {kib} features are provided via a REST API, which is ideal for creating an -integration with {kib}, or automating certain aspects of configuring and +Some {kib} features are provided via a REST API, which is ideal for creating an +integration with {kib}, or automating certain aspects of configuring and deploying {kib}. [float] @@ -18,15 +18,15 @@ NOTE: The {kib} Console supports only Elasticsearch APIs. You are unable to inte [float] [[api-authentication]] === Authentication -The {kib} APIs support key- and token-based authentication. +The {kib} APIs support key- and token-based authentication. [float] [[token-api-authentication]] ==== Token-based authentication -To use token-based authentication, you use the same username and password that you use to log into Elastic. -In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, -which is where the username and password are stored in order to be passed as part of the call. +To use token-based authentication, you use the same username and password that you use to log into Elastic. +In a given HTTP tool, and when available, you can select to use its 'Basic Authentication' option, +which is where the username and password are stored in order to be passed as part of the call. [float] [[key-authentication]] @@ -65,7 +65,7 @@ For all APIs, you must use a request header. The {kib} APIs support the `kbn-xsr * XSRF protections are disabled using the <> setting `Content-Type: application/json`:: - Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. + Applicable only when you send a payload in the API request. {kib} API requests and responses use JSON. Typically, if you include the `kbn-xsrf` header, you must also include the `Content-Type` header. Request header example: @@ -97,6 +97,6 @@ include::{kib-repo-dir}/api/actions-and-connectors.asciidoc[] include::{kib-repo-dir}/api/dashboard-api.asciidoc[] include::{kib-repo-dir}/api/logstash-configuration-management.asciidoc[] include::{kib-repo-dir}/api/machine-learning.asciidoc[] -include::{kib-repo-dir}/api/url-shortening.asciidoc[] +include::{kib-repo-dir}/api/short-urls.asciidoc[] include::{kib-repo-dir}/api/task-manager/health.asciidoc[] include::{kib-repo-dir}/api/upgrade-assistant.asciidoc[] From b4010c86ad7787ee0df8e0a59ac46a5fc2063096 Mon Sep 17 00:00:00 2001 From: Kate Farrar Date: Tue, 12 Oct 2021 11:49:25 -0600 Subject: [PATCH 086/287] Move interval help text to be a tooltip on the datepicker (#108761) * moving interval help text as a tooltip on the datepicker * [Metrics UI] Refactor Snapshot data for use in FilterBar Co-authored-by: Chris Cowan --- .../inventory_view/components/filter_bar.tsx | 4 +- .../inventory_view/components/layout.tsx | 43 ++++++------------- .../inventory_view/components/layout_view.tsx | 12 +++++- .../components/snapshot_container.tsx | 43 +++++++++++++++++++ .../waffle/waffle_time_controls.tsx | 38 ++++++++++------ .../pages/metrics/inventory_view/index.tsx | 16 ++++++- 6 files changed, 106 insertions(+), 50 deletions(-) create mode 100644 x-pack/plugins/infra/public/pages/metrics/inventory_view/components/snapshot_container.tsx diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx index f29f87191bc13..deef63892ac95 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/filter_bar.tsx @@ -11,13 +11,13 @@ import React from 'react'; import { WaffleTimeControls } from './waffle/waffle_time_controls'; import { SearchBar } from './search_bar'; -export const FilterBar = () => ( +export const FilterBar = ({ interval }: { interval: string }) => ( - + ); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index f241f5d118147..de0a56c5be73d 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -9,13 +9,12 @@ import React, { useCallback, useEffect, useState } from 'react'; import useInterval from 'react-use/lib/useInterval'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { SnapshotNode } from '../../../../../common/http_api'; import { SavedView } from '../../../../containers/saved_view/saved_view'; import { AutoSizer } from '../../../../components/auto_sizer'; -import { convertIntervalToString } from '../../../../utils/convert_interval_to_string'; import { NodesOverview } from './nodes_overview'; import { calculateBoundsFromNodes } from '../lib/calculate_bounds_from_nodes'; import { PageContent } from '../../../../components/page'; -import { useSnapshot } from '../hooks/use_snaphot'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; @@ -24,30 +23,30 @@ import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; import { ViewSwitcher } from './waffle/view_switcher'; -import { IntervalLabel } from './waffle/interval_label'; import { createInventoryMetricFormatter } from '../lib/create_inventory_metric_formatter'; import { createLegend } from '../lib/create_legend'; import { useWaffleViewState } from '../hooks/use_waffle_view_state'; import { BottomDrawer } from './bottom_drawer'; import { Legend } from './waffle/legend'; +interface Props { + shouldLoadDefault: boolean; + currentView: SavedView | null; + reload: () => Promise; + interval: string; + nodes: SnapshotNode[]; + loading: boolean; +} + export const Layout = React.memo( - ({ - shouldLoadDefault, - currentView, - }: { - shouldLoadDefault: boolean; - currentView: SavedView | null; - }) => { + ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { const [showLoading, setShowLoading] = useState(true); - const { sourceId, source } = useSourceContext(); + const { source } = useSourceContext(); const { metric, groupBy, sort, nodeType, - accountId, - region, changeView, view, autoBounds, @@ -55,19 +54,7 @@ export const Layout = React.memo( legend, } = useWaffleOptionsContext(); const { currentTime, jumpToTime, isAutoReloading } = useWaffleTimeContext(); - const { filterQueryAsJson, applyFilterQuery } = useWaffleFiltersContext(); - const { loading, nodes, reload, interval } = useSnapshot( - filterQueryAsJson, - [metric], - groupBy, - nodeType, - sourceId, - currentTime, - accountId, - region, - false - ); - + const { applyFilterQuery } = useWaffleFiltersContext(); const legendPalette = legend?.palette ?? DEFAULT_LEGEND.palette; const legendSteps = legend?.steps ?? DEFAULT_LEGEND.steps; const legendReverseColors = legend?.reverseColors ?? DEFAULT_LEGEND.reverseColors; @@ -91,7 +78,6 @@ export const Layout = React.memo( isAutoReloading ? 5000 : null ); - const intervalAsString = convertIntervalToString(interval); const dataBounds = calculateBoundsFromNodes(nodes); const bounds = autoBounds ? dataBounds : boundsOverride; /* eslint-disable-next-line react-hooks/exhaustive-deps */ @@ -151,9 +137,6 @@ export const Layout = React.memo( gutterSize="m" > - - - diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx index 1e66fe22ac45e..af9c9ab5e2b30 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout_view.tsx @@ -6,10 +6,18 @@ */ import React from 'react'; +import { SnapshotNode } from '../../../../../common/http_api'; import { useSavedViewContext } from '../../../../containers/saved_view/saved_view'; import { Layout } from './layout'; -export const LayoutView = () => { +interface Props { + reload: () => Promise; + interval: string; + nodes: SnapshotNode[]; + loading: boolean; +} + +export const LayoutView = (props: Props) => { const { shouldLoadDefault, currentView } = useSavedViewContext(); - return ; + return ; }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/snapshot_container.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/snapshot_container.tsx new file mode 100644 index 0000000000000..e64e79c99f6fc --- /dev/null +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/snapshot_container.tsx @@ -0,0 +1,43 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { useSourceContext } from '../../../../containers/metrics_source'; +import { SnapshotNode } from '../../../../../common/http_api'; +import { useSnapshot } from '../hooks/use_snaphot'; +import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; +import { useWaffleOptionsContext } from '../hooks/use_waffle_options'; +import { useWaffleTimeContext } from '../hooks/use_waffle_time'; + +interface RenderProps { + reload: () => Promise; + interval: string; + nodes: SnapshotNode[]; + loading: boolean; +} + +interface Props { + render: React.FC; +} +export const SnapshotContainer = ({ render }: Props) => { + const { sourceId } = useSourceContext(); + const { metric, groupBy, nodeType, accountId, region } = useWaffleOptionsContext(); + const { currentTime } = useWaffleTimeContext(); + const { filterQueryAsJson } = useWaffleFiltersContext(); + const { loading, nodes, reload, interval } = useSnapshot( + filterQueryAsJson, + [metric], + groupBy, + nodeType, + sourceId, + currentTime, + accountId, + region, + false + ); + return render({ loading, nodes, reload, interval }); +}; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx index 39150a98c2e89..7ac618987b422 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/waffle_time_controls.tsx @@ -5,22 +5,25 @@ * 2.0. */ -import { EuiButton, EuiDatePicker, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { EuiButton, EuiDatePicker, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import moment, { Moment } from 'moment'; import React, { useCallback } from 'react'; +import { convertIntervalToString } from '../../../../../utils/convert_interval_to_string'; import { withTheme, EuiTheme } from '../../../../../../../../../src/plugins/kibana_react/common'; import { useWaffleTimeContext } from '../../hooks/use_waffle_time'; interface Props { theme: EuiTheme | undefined; + interval: string; } -export const WaffleTimeControls = withTheme(({ theme }: Props) => { +export const WaffleTimeControls = withTheme(({ interval }: Props) => { const { currentTime, isAutoReloading, startAutoReload, stopAutoReload, jumpToTime } = useWaffleTimeContext(); const currentMoment = moment(currentTime); + const intervalAsString = convertIntervalToString(interval); const liveStreamingButton = isAutoReloading ? ( @@ -50,18 +53,25 @@ export const WaffleTimeControls = withTheme(({ theme }: Props) => { return ( - + + + {liveStreamingButton} diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx index 67e39a11c12e7..87765088d6343 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/index.tsx @@ -26,6 +26,7 @@ import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common' import { APP_WRAPPER_CLASS } from '../../../../../../../src/core/public'; import { inventoryTitle } from '../../../translations'; import { SavedViews } from './components/saved_views'; +import { SnapshotContainer } from './components/snapshot_container'; export const SnapshotPage = () => { const { @@ -78,8 +79,19 @@ export const SnapshotPage = () => { paddingSize: 'none', }} > - - + ( + <> + + + + )} + /> From 396ed0925929310c59694c8ab49b5d9a706cc148 Mon Sep 17 00:00:00 2001 From: Zacqary Adam Xeper Date: Tue, 12 Oct 2021 12:54:39 -0500 Subject: [PATCH 087/287] [Monitoring] Fix Set Up Monitoring redirect to setup mode (#114354) * [Monitoring] Fix Set Up Monitoring redirect to setup mode * Fix initt setup mode from route init --- .../pages/elasticsearch/elasticsearch_template.tsx | 6 +++--- .../plugins/monitoring/public/application/route_init.tsx | 6 +++--- .../monitoring/public/application/setup_mode/index.ts | 8 ++++++++ .../public/application/setup_mode/setup_mode.tsx | 4 ++-- 4 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/setup_mode/index.ts diff --git a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx index 13e21912df896..aa7ca97219206 100644 --- a/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/elasticsearch/elasticsearch_template.tsx @@ -12,7 +12,7 @@ import { TabMenuItem, PageTemplateProps } from '../page_template'; import { ML_SUPPORTED_LICENSES } from '../../../../common/constants'; interface ElasticsearchTemplateProps extends PageTemplateProps { - cluster: any; + cluster?: any; } export const ElasticsearchTemplate: React.FC = ({ @@ -43,7 +43,7 @@ export const ElasticsearchTemplate: React.FC = ({ }, ]; - if (mlIsSupported(cluster.license)) { + if (cluster && mlIsSupported(cluster.license)) { tabs.push({ id: 'ml', label: i18n.translate('xpack.monitoring.esNavigation.jobsLinkText', { @@ -53,7 +53,7 @@ export const ElasticsearchTemplate: React.FC = ({ }); } - if (cluster.isCcrEnabled) { + if (cluster?.isCcrEnabled) { tabs.push({ id: 'ccr', label: i18n.translate('xpack.monitoring.esNavigation.ccrLinkText', { diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx index 092b3f54036c9..c620229eb059a 100644 --- a/x-pack/plugins/monitoring/public/application/route_init.tsx +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -9,6 +9,7 @@ import { Route, Redirect, useLocation } from 'react-router-dom'; import { useClusters } from './hooks/use_clusters'; import { GlobalStateContext } from './contexts/global_state_context'; import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; +import { isInSetupMode } from './setup_mode'; import { LoadingPage } from './pages/loading_page'; export interface ComponentProps { @@ -35,13 +36,12 @@ export const RouteInit: React.FC = ({ const { clusters, loaded } = useClusters(clusterUuid, undefined, codePaths); - // TODO: we will need this when setup mode is migrated - // const inSetupMode = isInSetupMode(); + const inSetupMode = isInSetupMode(undefined, globalState); const cluster = getClusterFromClusters(clusters, globalState, unsetGlobalState); // TODO: check for setupMode too when the setup mode is migrated - if (loaded && !cluster) { + if (loaded && !cluster && !inSetupMode) { return ; } diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/index.ts b/x-pack/plugins/monitoring/public/application/setup_mode/index.ts new file mode 100644 index 0000000000000..1bcdcdef09c28 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/setup_mode/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './setup_mode'; diff --git a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx index bfdf96ef5b2c1..828d5a2d20ae6 100644 --- a/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx +++ b/x-pack/plugins/monitoring/public/application/setup_mode/setup_mode.tsx @@ -177,7 +177,7 @@ export const initSetupModeState = async ( } }; -export const isInSetupMode = (context?: ISetupModeContext) => { +export const isInSetupMode = (context?: ISetupModeContext, gState: GlobalState = globalState) => { if (context?.setupModeSupported === false) { return false; } @@ -185,7 +185,7 @@ export const isInSetupMode = (context?: ISetupModeContext) => { return true; } - return globalState.inSetupMode; + return gState.inSetupMode; }; export const isSetupModeFeatureEnabled = (feature: SetupModeFeature) => { From 7ffebf1fa31253d816b7715587267db896898258 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Tue, 12 Oct 2021 20:58:45 +0300 Subject: [PATCH 088/287] [Connectors] ServiceNow ITSM & SIR Application (#105440) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/developer/plugin-list.asciidoc | 3 +- docs/management/action-types.asciidoc | 6 +- .../action-types/servicenow-sir.asciidoc | 89 +++ .../action-types/servicenow.asciidoc | 14 +- .../images/servicenow-sir-params-test.png | Bin 190659 -> 46762 bytes docs/management/connectors/index.asciidoc | 1 + x-pack/plugins/actions/README.md | 105 ++- .../server/builtin_action_types/pagerduty.ts | 4 +- .../servicenow/api.test.ts | 50 ++ .../builtin_action_types/servicenow/api.ts | 11 +- .../servicenow/api_sir.test.ts | 286 +++++++ .../servicenow/api_sir.ts | 154 ++++ .../servicenow/config.test.ts | 40 + .../builtin_action_types/servicenow/config.ts | 37 + .../builtin_action_types/servicenow/index.ts | 56 +- .../builtin_action_types/servicenow/mocks.ts | 107 ++- .../builtin_action_types/servicenow/schema.ts | 23 +- .../servicenow/service.test.ts | 706 +++++++++++++----- .../servicenow/service.ts | 233 +++--- .../servicenow/service_sir.test.ts | 129 ++++ .../servicenow/service_sir.ts | 104 +++ .../builtin_action_types/servicenow/types.ts | 122 ++- .../servicenow/utils.test.ts | 84 +++ .../builtin_action_types/servicenow/utils.ts | 46 ++ .../actions/server/constants/connectors.ts | 12 + .../saved_objects/actions_migrations.test.ts | 57 ++ .../saved_objects/actions_migrations.ts | 37 +- x-pack/plugins/cases/README.md | 12 +- x-pack/plugins/cases/common/ui/types.ts | 3 + .../public/common/mock/register_connectors.ts | 27 + .../all_cases/all_cases_generic.test.tsx | 10 +- .../components/all_cases/columns.test.tsx | 10 +- .../components/all_cases/index.test.tsx | 10 +- .../configure_cases/connectors.test.tsx | 38 +- .../components/configure_cases/connectors.tsx | 13 +- .../connectors_dropdown.test.tsx | 81 +- .../configure_cases/connectors_dropdown.tsx | 20 +- .../configure_cases/translations.ts | 14 + .../components/connectors/card.test.tsx | 10 +- .../connectors/deprecated_callout.test.tsx | 32 + .../connectors/deprecated_callout.tsx | 42 ++ .../servicenow_itsm_case_fields.test.tsx | 13 +- .../servicenow_itsm_case_fields.tsx | 190 +++-- .../servicenow_sir_case_fields.test.tsx | 21 +- .../servicenow/servicenow_sir_case_fields.tsx | 237 +++--- .../connectors/servicenow/translations.ts | 8 +- .../connectors/servicenow/validator.test.ts | 37 + .../connectors/servicenow/validator.ts | 26 + .../components/create/connector.test.tsx | 10 +- .../plugins/cases/public/components/types.ts | 4 +- .../plugins/cases/public/components/utils.ts | 25 + .../cases/public/containers/configure/mock.ts | 10 + .../connectors/servicenow/itsm_format.test.ts | 9 +- .../connectors/servicenow/itsm_format.ts | 10 +- .../connectors/servicenow/sir_format.test.ts | 55 +- .../connectors/servicenow/sir_format.ts | 23 +- .../server/connectors/servicenow/types.ts | 19 +- .../security_solution/common/constants.ts | 1 + .../integration/cases/connectors.spec.ts | 13 +- .../security_solution/cypress/objects/case.ts | 16 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../email/email_connector.test.tsx | 18 + .../es_index/es_index_connector.test.tsx | 2 + .../jira/jira_connectors.test.tsx | 10 + .../pagerduty/pagerduty_connectors.test.tsx | 8 + .../resilient/resilient_connectors.test.tsx | 10 + .../servicenow/api.test.ts | 113 ++- .../builtin_action_types/servicenow/api.ts | 44 ++ .../application_required_callout.test.tsx | 30 + .../application_required_callout.tsx | 60 ++ .../builtin_action_types/servicenow/config.ts | 9 + .../servicenow/credentials.tsx | 191 +++++ .../servicenow/deprecated_callout.test.tsx | 35 + .../servicenow/deprecated_callout.tsx | 55 ++ .../servicenow/helpers.test.ts | 47 ++ .../servicenow/helpers.ts | 33 +- .../servicenow/installation_callout.test.tsx | 27 + .../servicenow/installation_callout.tsx | 32 + .../servicenow/servicenow.test.tsx | 3 + .../servicenow/servicenow.tsx | 1 + .../servicenow/servicenow_connectors.test.tsx | 13 +- .../servicenow/servicenow_connectors.tsx | 260 +++---- .../servicenow_itsm_params.test.tsx | 12 +- .../servicenow/servicenow_itsm_params.tsx | 81 +- .../servicenow/servicenow_sir_params.test.tsx | 12 +- .../servicenow/servicenow_sir_params.tsx | 63 +- .../servicenow/sn_store_button.test.tsx | 27 + .../servicenow/sn_store_button.tsx | 27 + .../servicenow/translations.ts | 135 +++- .../builtin_action_types/servicenow/types.ts | 15 + .../servicenow/update_connector_modal.tsx | 156 ++++ .../servicenow/use_get_app_info.test.tsx | 95 +++ .../servicenow/use_get_app_info.tsx | 69 ++ .../slack/slack_connectors.test.tsx | 8 + .../builtin_action_types/swimlane/api.test.ts | 25 +- .../swimlane/swimlane_connectors.test.tsx | 14 + .../teams/teams_connectors.test.tsx | 8 + .../webhook/webhook_connectors.test.tsx | 8 + .../action_connector_form.test.tsx | 2 + .../action_connector_form.tsx | 7 + .../action_connector_form/action_form.tsx | 14 +- .../connector_add_flyout.tsx | 15 + .../connector_add_modal.tsx | 14 + .../connector_edit_flyout.tsx | 36 +- .../components/actions_connectors_list.tsx | 31 + .../public/common/constants/index.ts | 2 - .../triggers_actions_ui/public/types.ts | 10 + .../uptime/public/state/api/alert_actions.ts | 2 + .../alerting_api_integration/common/config.ts | 1 + .../actions_simulators/server/plugin.ts | 13 +- .../server/servicenow_simulation.ts | 331 ++++---- .../server/swimlane_simulation.ts | 2 +- .../{servicenow.ts => servicenow_itsm.ts} | 196 +++-- .../builtin_action_types/servicenow_sir.ts | 544 ++++++++++++++ .../tests/actions/index.ts | 3 +- .../case_api_integration/common/lib/utils.ts | 24 +- .../tests/trial/cases/push_case.ts | 60 +- .../user_actions/get_all_user_actions.ts | 26 +- .../tests/trial/configure/get_configure.ts | 25 +- .../tests/trial/configure/get_connectors.ts | 6 +- .../tests/trial/configure/patch_configure.ts | 26 +- .../tests/trial/configure/post_configure.ts | 24 +- .../tests/trial/cases/push_case.ts | 31 +- .../tests/trial/cases/push_case.ts | 34 +- .../tests/trial/configure/get_configure.ts | 27 +- .../tests/trial/configure/get_connectors.ts | 6 +- .../tests/trial/configure/patch_configure.ts | 26 +- .../tests/trial/configure/post_configure.ts | 24 +- 129 files changed, 5592 insertions(+), 1293 deletions(-) create mode 100644 docs/management/connectors/action-types/servicenow-sir.asciidoc create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts create mode 100644 x-pack/plugins/actions/server/constants/connectors.ts create mode 100644 x-pack/plugins/cases/public/common/mock/register_connectors.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx rename x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/{servicenow.ts => servicenow_itsm.ts} (76%) create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index 0e728a4dada24..1ef00aa9de115 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -362,7 +362,8 @@ The plugin exposes the static DefaultEditorController class to consume. |{kib-repo}blob/{branch}/x-pack/plugins/cases/README.md[cases] -|Case management in Kibana +|[![Issues][issues-shield]][issues-url] +[![Pull Requests][pr-shield]][pr-url] |{kib-repo}blob/{branch}/x-pack/plugins/cloud/README.md[cloud] diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 92adbaf97d8c5..93d0ee3d2cab6 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -35,10 +35,14 @@ a| <> | Add a message to a Kibana log. -a| <> +a| <> | Create an incident in ServiceNow. +a| <> + +| Create a security incident in ServiceNow. + a| <> | Send a message to a Slack channel or user. diff --git a/docs/management/connectors/action-types/servicenow-sir.asciidoc b/docs/management/connectors/action-types/servicenow-sir.asciidoc new file mode 100644 index 0000000000000..4556746284d5b --- /dev/null +++ b/docs/management/connectors/action-types/servicenow-sir.asciidoc @@ -0,0 +1,89 @@ +[role="xpack"] +[[servicenow-sir-action-type]] +=== ServiceNow connector and action +++++ +ServiceNow SecOps +++++ + +The ServiceNow SecOps connector uses the https://docs.servicenow.com/bundle/orlando-application-development/page/integrate/inbound-rest/concept/c_TableAPI.html[V2 Table API] to create ServiceNow security incidents. + +[float] +[[servicenow-sir-connector-configuration]] +==== Connector configuration + +ServiceNow SecOps connectors have the following configuration properties. + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: ServiceNow instance URL. +Username:: Username for HTTP Basic authentication. +Password:: Password for HTTP Basic authentication. + +The ServiceNow user requires at minimum read, create, and update access to the Security Incident table and read access to the https://docs.servicenow.com/bundle/paris-platform-administration/page/administer/localization/reference/r_ChoicesTable.html[sys_choice]. If you don't provide access to sys_choice, then the choices will not render. + +[float] +[[servicenow-sir-connector-networking-configuration]] +==== Connector networking configuration + +Use the <> to customize connector networking configurations, such as proxies, certificates, or TLS settings. You can set configurations that apply to all your connectors or use `xpack.actions.customHostSettings` to set per-host configurations. + +[float] +[[Preconfigured-servicenow-sir-configuration]] +==== Preconfigured connector type + +[source,text] +-- + my-servicenow-sir: + name: preconfigured-servicenow-connector-type + actionTypeId: .servicenow-sir + config: + apiUrl: https://dev94428.service-now.com/ + secrets: + username: testuser + password: passwordkeystorevalue +-- + +Config defines information for the connector type. + +`apiUrl`:: An address that corresponds to *URL*. + +Secrets defines sensitive information for the connector type. + +`username`:: A string that corresponds to *Username*. +`password`:: A string that corresponds to *Password*. Should be stored in the <>. + +[float] +[[define-servicenow-sir-ui]] +==== Define connector in Stack Management + +Define ServiceNow SecOps connector properties. + +[role="screenshot"] +image::management/connectors/images/servicenow-sir-connector.png[ServiceNow SecOps connector] + +Test ServiceNow SecOps action parameters. + +[role="screenshot"] +image::management/connectors/images/servicenow-sir-params-test.png[ServiceNow SecOps params test] + +[float] +[[servicenow-sir-action-configuration]] +==== Action configuration + +ServiceNow SecOps actions have the following configuration properties. + +Short description:: A short description for the incident, used for searching the contents of the knowledge base. +Source Ips:: A list of source IPs related to the incident. The IPs will be added as observables to the security incident. +Destination Ips:: A list of destination IPs related to the incident. The IPs will be added as observables to the security incident. +Malware URLs:: A list of malware URLs related to the incident. The URLs will be added as observables to the security incident. +Malware Hashes:: A list of malware hashes related to the incident. The hashes will be added as observables to the security incident. +Priority:: The priority of the incident. +Category:: The category of the incident. +Subcategory:: The subcategory of the incident. +Description:: The details about the incident. +Additional comments:: Additional information for the client, such as how to troubleshoot the issue. + +[float] +[[configuring-servicenow-sir]] +==== Configure ServiceNow SecOps + +ServiceNow offers free https://developer.servicenow.com/dev.do#!/guides/madrid/now-platform/pdi-guide/obtaining-a-pdi[Personal Developer Instances], which you can use to test incidents. diff --git a/docs/management/connectors/action-types/servicenow.asciidoc b/docs/management/connectors/action-types/servicenow.asciidoc index 3a4134cbf982e..cf5244a9e3f9e 100644 --- a/docs/management/connectors/action-types/servicenow.asciidoc +++ b/docs/management/connectors/action-types/servicenow.asciidoc @@ -2,16 +2,16 @@ [[servicenow-action-type]] === ServiceNow connector and action ++++ -ServiceNow +ServiceNow ITSM ++++ -The ServiceNow connector uses the https://docs.servicenow.com/bundle/orlando-application-development/page/integrate/inbound-rest/concept/c_TableAPI.html[V2 Table API] to create ServiceNow incidents. +The ServiceNow ITSM connector uses the https://docs.servicenow.com/bundle/orlando-application-development/page/integrate/inbound-rest/concept/c_TableAPI.html[V2 Table API] to create ServiceNow incidents. [float] [[servicenow-connector-configuration]] ==== Connector configuration -ServiceNow connectors have the following configuration properties. +ServiceNow ITSM connectors have the following configuration properties. Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. URL:: ServiceNow instance URL. @@ -55,12 +55,12 @@ Secrets defines sensitive information for the connector type. [[define-servicenow-ui]] ==== Define connector in Stack Management -Define ServiceNow connector properties. +Define ServiceNow ITSM connector properties. [role="screenshot"] image::management/connectors/images/servicenow-connector.png[ServiceNow connector] -Test ServiceNow action parameters. +Test ServiceNow ITSM action parameters. [role="screenshot"] image::management/connectors/images/servicenow-params-test.png[ServiceNow params test] @@ -69,11 +69,13 @@ image::management/connectors/images/servicenow-params-test.png[ServiceNow params [[servicenow-action-configuration]] ==== Action configuration -ServiceNow actions have the following configuration properties. +ServiceNow ITSM actions have the following configuration properties. Urgency:: The extent to which the incident resolution can delay. Severity:: The severity of the incident. Impact:: The effect an incident has on business. Can be measured by the number of affected users or by how critical it is to the business in question. +Category:: The category of the incident. +Subcategory:: The category of the incident. Short description:: A short description for the incident, used for searching the contents of the knowledge base. Description:: The details about the incident. Additional comments:: Additional information for the client, such as how to troubleshoot the issue. diff --git a/docs/management/connectors/images/servicenow-sir-params-test.png b/docs/management/connectors/images/servicenow-sir-params-test.png index 16ea83c60b3c328f4580bdd08d4f2544e8c8cffc..80103a4272bfacd0a6ccb23bb138b98efe327f51 100644 GIT binary patch literal 46762 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ì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%0AwWoSpN;xE=Y5Ue-?=mI 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 diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index 033b1c3ac150e..536d05705181d 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[] +include::action-types/servicenow-sir.asciidoc[] include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index b19e89a599840..0d66c9d30f8b9 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -33,29 +33,36 @@ Table of Contents - [actionsClient.execute(options)](#actionsclientexecuteoptions) - [Example](#example-2) - [Built-in Action Types](#built-in-action-types) - - [ServiceNow](#servicenow) + - [ServiceNow ITSM](#servicenow-itsm) - [`params`](#params) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice) - [`subActionParams (getFields)`](#subactionparams-getfields) - [`subActionParams (getIncident)`](#subactionparams-getincident) - [`subActionParams (getChoices)`](#subactionparams-getchoices) - - [Jira](#jira) + - [ServiceNow Sec Ops](#servicenow-sec-ops) - [`params`](#params-1) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) + - [`subActionParams (getFields)`](#subactionparams-getfields-1) - [`subActionParams (getIncident)`](#subactionparams-getincident-1) + - [`subActionParams (getChoices)`](#subactionparams-getchoices-1) + - [| fields | An array of fields. Example: `[priority, category]`. | string[] |](#-fields----an-array-of-fields-example-priority-category--string-) + - [Jira](#jira) + - [`params`](#params-2) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (getIncident)`](#subactionparams-getincident-2) - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) - [`subActionParams (fieldsByIssueType)`](#subactionparams-fieldsbyissuetype) - [`subActionParams (issues)`](#subactionparams-issues) - [`subActionParams (issue)`](#subactionparams-issue) - - [`subActionParams (getFields)`](#subactionparams-getfields-1) - - [IBM Resilient](#ibm-resilient) - - [`params`](#params-2) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) - [`subActionParams (getFields)`](#subactionparams-getfields-2) + - [IBM Resilient](#ibm-resilient) + - [`params`](#params-3) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) + - [`subActionParams (getFields)`](#subactionparams-getfields-3) - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) - [`subActionParams (severity)`](#subactionparams-severity) - [Swimlane](#swimlane) - - [`params`](#params-3) + - [`params`](#params-4) - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) @@ -246,9 +253,9 @@ Kibana ships with a set of built-in action types. See [Actions and connector typ In addition to the documented configurations, several built in action type offer additional `params` configurations. -## ServiceNow +## ServiceNow ITSM -The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. +The [ServiceNow ITSM user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. ### `params` | Property | Description | Type | @@ -265,16 +272,18 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | -| short_description | The title of the incident. | string | -| description | The description of the incident. | string _(optional)_ | -| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| severity | The severity in ServiceNow. | string _(optional)_ | -| urgency | The urgency in ServiceNow. | string _(optional)_ | -| impact | The impact in ServiceNow. | string _(optional)_ | -| category | The category in ServiceNow. | string _(optional)_ | -| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| Property | Description | Type | +| ------------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | +| externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | +| severity | The severity in ServiceNow. | string _(optional)_ | +| urgency | The urgency in ServiceNow. | string _(optional)_ | +| impact | The impact in ServiceNow. | string _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| correlation_id | The correlation id of the incident. | string _(optional)_ | +| correlation_display | The correlation display of the ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -289,12 +298,64 @@ No parameters for the `getFields` subaction. Provide an empty object `{}`. #### `subActionParams (getChoices)` -| Property | Description | Type | -| -------- | ------------------------------------------------------------ | -------- | -| fields | An array of fields. Example: `[priority, category, impact]`. | string[] | +| Property | Description | Type | +| -------- | -------------------------------------------------- | -------- | +| fields | An array of fields. Example: `[category, impact]`. | string[] | --- +## ServiceNow Sec Ops + +The [ServiceNow SecOps user documentation `params`](https://www.elastic.co/guide/en/kibana/master/servicenow-sir-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. + +### `params` + +| Property | Description | Type | +| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`, `getFields`, `getIncident`, and `getChoices`. | string | +| subActionParams | The parameters of the subaction. | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The ServiceNow security incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- | +| short_description | The title of the security incident. | string | +| description | The description of the security incident. | string _(optional)_ | +| externalId | The ID of the security incident in ServiceNow. If present, the security incident is updated. Otherwise, a new security incident is created. | string _(optional)_ | +| priority | The priority in ServiceNow. | string _(optional)_ | +| dest_ip | A list of destination IPs related to the security incident. The IPs will be added as observables to the security incident. | (string \| string[]) _(optional)_ | +| source_ip | A list of source IPs related to the security incident. The IPs will be added as observables to the security incident. | (string \| string[]) _(optional)_ | +| malware_hash | A list of malware hashes related to the security incident. The hashes will be added as observables to the security incident. | (string \| string[]) _(optional)_ | +| malware_url | A list of malware URLs related to the security incident. The URLs will be added as observables to the security incident. | (string \| string[]) _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| correlation_id | The correlation id of the security incident. | string _(optional)_ | +| correlation_display | The correlation display of the security incident. | string _(optional)_ | + +#### `subActionParams (getFields)` + +No parameters for the `getFields` subaction. Provide an empty object `{}`. + +#### `subActionParams (getIncident)` + +| Property | Description | Type | +| ---------- | ---------------------------------------------- | ------ | +| externalId | The ID of the security incident in ServiceNow. | string | + + +#### `subActionParams (getChoices)` + +| Property | Description | Type | +| -------- | ---------------------------------------------------- | -------- | +| fields | An array of fields. Example: `[priority, category]`. | string[] | +--- ## Jira The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/master/jira-action-type.html) lists configuration properties for the `pushToService` subaction. In addition, several other subaction types are available. diff --git a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts index 5d83b658111e4..7710ff79d08b4 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/pagerduty.ts @@ -143,7 +143,7 @@ export function getActionType({ }), validate: { config: schema.object(configSchemaProps, { - validate: curry(valdiateActionTypeConfig)(configurationUtilities), + validate: curry(validateActionTypeConfig)(configurationUtilities), }), secrets: SecretsSchema, params: ParamsSchema, @@ -152,7 +152,7 @@ export function getActionType({ }; } -function valdiateActionTypeConfig( +function validateActionTypeConfig( configurationUtilities: ActionsConfigurationUtilities, configObject: ActionTypeConfigType ) { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 8d24e48d4d515..e1f66263729e2 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -25,6 +25,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -57,6 +58,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -78,6 +80,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: { username: 'elastic', password: 'elastic' }, logger: mockedLogger, commentFieldKey: 'comments', @@ -93,6 +96,9 @@ describe('api', () => { caller_id: 'elastic', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', + opened_by: 'elastic', }, }); expect(externalService.updateIncident).not.toHaveBeenCalled(); @@ -103,6 +109,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -118,6 +125,8 @@ describe('api', () => { comments: 'A comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-1', }); @@ -132,6 +141,8 @@ describe('api', () => { comments: 'Another comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-1', }); @@ -142,6 +153,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -157,6 +169,8 @@ describe('api', () => { work_notes: 'A comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-1', }); @@ -171,6 +185,8 @@ describe('api', () => { work_notes: 'Another comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-1', }); @@ -182,6 +198,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, params: apiParams, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -210,6 +227,7 @@ describe('api', () => { const res = await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -228,6 +246,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -243,6 +262,8 @@ describe('api', () => { subcategory: 'os', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, }); expect(externalService.createIncident).not.toHaveBeenCalled(); @@ -253,6 +274,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'comments', @@ -267,6 +289,8 @@ describe('api', () => { subcategory: 'os', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-3', }); @@ -281,6 +305,8 @@ describe('api', () => { comments: 'A comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-2', }); @@ -291,6 +317,7 @@ describe('api', () => { await api.pushToService({ externalService, params, + config: {}, secrets: {}, logger: mockedLogger, commentFieldKey: 'work_notes', @@ -305,6 +332,8 @@ describe('api', () => { subcategory: 'os', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-3', }); @@ -319,6 +348,8 @@ describe('api', () => { work_notes: 'A comment', description: 'Incident description', short_description: 'Incident title', + correlation_display: 'Alerting', + correlation_id: 'ruleId', }, incidentId: 'incident-2', }); @@ -344,4 +375,23 @@ describe('api', () => { expect(res).toEqual(serviceNowChoices); }); }); + + describe('getIncident', () => { + test('it gets the incident correctly', async () => { + const res = await api.getIncident({ + externalService, + params: { + externalId: 'incident-1', + }, + }); + expect(res).toEqual({ + description: 'description from servicenow', + id: 'incident-1', + pushedDate: '2020-03-10T12:24:20.000Z', + short_description: 'title from servicenow', + title: 'INC01', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + }); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 4120c07c32303..88cdfd069cf1b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -6,7 +6,7 @@ */ import { - ExternalServiceApi, + ExternalServiceAPI, GetChoicesHandlerArgs, GetChoicesResponse, GetCommonFieldsHandlerArgs, @@ -19,7 +19,11 @@ import { } from './types'; const handshakeHandler = async ({ externalService, params }: HandshakeApiHandlerArgs) => {}; -const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => {}; +const getIncidentHandler = async ({ externalService, params }: GetIncidentApiHandlerArgs) => { + const { externalId: id } = params; + const res = await externalService.getIncident(id); + return res; +}; const pushToServiceHandler = async ({ externalService, @@ -42,6 +46,7 @@ const pushToServiceHandler = async ({ incident: { ...incident, caller_id: secrets.username, + opened_by: secrets.username, }, }); } @@ -84,7 +89,7 @@ const getChoicesHandler = async ({ return res; }; -export const api: ExternalServiceApi = { +export const api: ExternalServiceAPI = { getChoices: getChoicesHandler, getFields: getFieldsHandler, getIncident: getIncidentHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts new file mode 100644 index 0000000000000..358af7cd2e9ef --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.test.ts @@ -0,0 +1,286 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Logger } from '../../../../../../src/core/server'; +import { externalServiceSIRMock, sirParams } from './mocks'; +import { ExternalServiceSIR, ObservableTypes } from './types'; +import { apiSIR, combineObservables, formatObservables, prepareParams } from './api_sir'; +let mockedLogger: jest.Mocked; + +describe('api_sir', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceSIRMock.create(); + jest.clearAllMocks(); + }); + + describe('combineObservables', () => { + test('it returns an empty array when both arguments are an empty array', async () => { + expect(combineObservables([], [])).toEqual([]); + }); + + test('it returns an empty array when both arguments are an empty string', async () => { + expect(combineObservables('', '')).toEqual([]); + }); + + test('it returns an empty array when a="" and b=[]', async () => { + expect(combineObservables('', [])).toEqual([]); + }); + + test('it returns an empty array when a=[] and b=""', async () => { + expect(combineObservables([], '')).toEqual([]); + }); + + test('it returns a if b is empty', async () => { + expect(combineObservables('a', '')).toEqual(['a']); + }); + + test('it returns b if a is empty', async () => { + expect(combineObservables([], ['b'])).toEqual(['b']); + }); + + test('it combines two strings', async () => { + expect(combineObservables('a,b', 'c,d')).toEqual(['a', 'b', 'c', 'd']); + }); + + test('it combines two arrays', async () => { + expect(combineObservables(['a'], ['b'])).toEqual(['a', 'b']); + }); + + test('it combines a string with an array', async () => { + expect(combineObservables('a', ['b'])).toEqual(['a', 'b']); + }); + + test('it combines an array with a string ', async () => { + expect(combineObservables(['a'], 'b')).toEqual(['a', 'b']); + }); + + test('it combines a "," concatenated string', async () => { + expect(combineObservables(['a'], 'b,c,d')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b,c,d', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines a "|" concatenated string', async () => { + expect(combineObservables(['a'], 'b|c|d')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b|c|d', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines a space concatenated string', async () => { + expect(combineObservables(['a'], 'b c d')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b c d', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines a "\\n" concatenated string', async () => { + expect(combineObservables(['a'], 'b\nc\nd')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b\nc\nd', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines a "\\r" concatenated string', async () => { + expect(combineObservables(['a'], 'b\rc\rd')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b\rc\rd', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines a "\\t" concatenated string', async () => { + expect(combineObservables(['a'], 'b\tc\td')).toEqual(['a', 'b', 'c', 'd']); + expect(combineObservables('b\tc\td', ['a'])).toEqual(['b', 'c', 'd', 'a']); + }); + + test('it combines two strings with different delimiter', async () => { + expect(combineObservables('a|b|c', 'd e f')).toEqual(['a', 'b', 'c', 'd', 'e', 'f']); + }); + }); + + describe('formatObservables', () => { + test('it formats array observables correctly', async () => { + const expectedTypes: Array<[ObservableTypes, string]> = [ + [ObservableTypes.ip4, 'ipv4-addr'], + [ObservableTypes.sha256, 'SHA256'], + [ObservableTypes.url, 'URL'], + ]; + + for (const type of expectedTypes) { + expect(formatObservables(['a', 'b', 'c'], type[0])).toEqual([ + { type: type[1], value: 'a' }, + { type: type[1], value: 'b' }, + { type: type[1], value: 'c' }, + ]); + } + }); + + test('it removes duplicates from array observables correctly', async () => { + expect(formatObservables(['a', 'a', 'c'], ObservableTypes.ip4)).toEqual([ + { type: 'ipv4-addr', value: 'a' }, + { type: 'ipv4-addr', value: 'c' }, + ]); + }); + + test('it formats an empty array correctly', async () => { + expect(formatObservables([], ObservableTypes.ip4)).toEqual([]); + }); + + test('it removes empty observables correctly', async () => { + expect(formatObservables(['a', '', 'c'], ObservableTypes.ip4)).toEqual([ + { type: 'ipv4-addr', value: 'a' }, + { type: 'ipv4-addr', value: 'c' }, + ]); + }); + }); + + describe('prepareParams', () => { + test('it prepares the params correctly when the connector is legacy', async () => { + expect(prepareParams(true, sirParams)).toEqual({ + ...sirParams, + incident: { + ...sirParams.incident, + dest_ip: '192.168.1.1,192.168.1.3', + source_ip: '192.168.1.2,192.168.1.4', + malware_hash: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + malware_url: 'https://example.com', + }, + }); + }); + + test('it prepares the params correctly when the connector is not legacy', async () => { + expect(prepareParams(false, sirParams)).toEqual({ + ...sirParams, + incident: { + ...sirParams.incident, + dest_ip: null, + source_ip: null, + malware_hash: null, + malware_url: null, + }, + }); + }); + + test('it prepares the params correctly when the connector is legacy and the observables are undefined', async () => { + const { + dest_ip: destIp, + source_ip: sourceIp, + malware_hash: malwareHash, + malware_url: malwareURL, + ...incidentWithoutObservables + } = sirParams.incident; + + expect( + prepareParams(true, { + ...sirParams, + // @ts-expect-error + incident: incidentWithoutObservables, + }) + ).toEqual({ + ...sirParams, + incident: { + ...sirParams.incident, + dest_ip: null, + source_ip: null, + malware_hash: null, + malware_url: null, + }, + }); + }); + }); + + describe('pushToService', () => { + test('it creates an incident correctly', async () => { + const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } }; + const res = await apiSIR.pushToService({ + externalService, + params, + config: { isLegacy: false }, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + + expect(res).toEqual({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', + comments: [ + { + commentId: 'case-comment-1', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + { + commentId: 'case-comment-2', + pushedDate: '2020-03-10T12:24:20.000Z', + }, + ], + }); + }); + + test('it adds observables correctly', async () => { + const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } }; + await apiSIR.pushToService({ + externalService, + params, + config: { isLegacy: false }, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + + expect(externalService.bulkAddObservableToIncident).toHaveBeenCalledWith( + [ + { type: 'ipv4-addr', value: '192.168.1.1' }, + { type: 'ipv4-addr', value: '192.168.1.3' }, + { type: 'ipv4-addr', value: '192.168.1.2' }, + { type: 'ipv4-addr', value: '192.168.1.4' }, + { + type: 'SHA256', + value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + }, + { type: 'URL', value: 'https://example.com' }, + ], + // createIncident mock returns this incident id + 'incident-1' + ); + }); + + test('it does not call bulkAddObservableToIncident if it a legacy connector', async () => { + const params = { ...sirParams, incident: { ...sirParams.incident, externalId: null } }; + await apiSIR.pushToService({ + externalService, + params, + config: { isLegacy: true }, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + + expect(externalService.bulkAddObservableToIncident).not.toHaveBeenCalled(); + }); + + test('it does not call bulkAddObservableToIncident if there are no observables', async () => { + const params = { + ...sirParams, + incident: { + ...sirParams.incident, + dest_ip: null, + source_ip: null, + malware_hash: null, + malware_url: null, + externalId: null, + }, + }; + + await apiSIR.pushToService({ + externalService, + params, + config: { isLegacy: false }, + secrets: {}, + logger: mockedLogger, + commentFieldKey: 'work_notes', + }); + + expect(externalService.bulkAddObservableToIncident).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts new file mode 100644 index 0000000000000..326bb79a0e708 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api_sir.ts @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isEmpty, isString } from 'lodash'; + +import { + ExecutorSubActionPushParamsSIR, + ExternalServiceAPI, + ExternalServiceSIR, + ObservableTypes, + PushToServiceApiHandlerArgs, + PushToServiceApiParamsSIR, + PushToServiceResponse, +} from './types'; + +import { api } from './api'; + +const SPLIT_REGEX = /[ ,|\r\n\t]+/; + +export const formatObservables = (observables: string[], type: ObservableTypes) => { + /** + * ServiceNow accepted formats are: comma, new line, tab, or pipe separators. + * Before the application the observables were being sent to ServiceNow as a concatenated string with + * delimiter. With the application the format changed to an array of observables. + */ + const uniqueObservables = new Set(observables); + return [...uniqueObservables].filter((obs) => !isEmpty(obs)).map((obs) => ({ value: obs, type })); +}; + +const obsAsArray = (obs: string | string[]): string[] => { + if (isEmpty(obs)) { + return []; + } + + if (isString(obs)) { + return obs.split(SPLIT_REGEX); + } + + return obs; +}; + +export const combineObservables = (a: string | string[], b: string | string[]): string[] => { + const first = obsAsArray(a); + const second = obsAsArray(b); + + return [...first, ...second]; +}; + +const observablesToString = (obs: string | string[] | null | undefined): string | null => { + if (Array.isArray(obs)) { + return obs.join(','); + } + + return obs ?? null; +}; + +export const prepareParams = ( + isLegacy: boolean, + params: PushToServiceApiParamsSIR +): PushToServiceApiParamsSIR => { + if (isLegacy) { + /** + * The schema has change to accept an array of observables + * or a string. In the case of a legacy connector we need to + * convert the observables to a string + */ + return { + ...params, + incident: { + ...params.incident, + dest_ip: observablesToString(params.incident.dest_ip), + malware_hash: observablesToString(params.incident.malware_hash), + malware_url: observablesToString(params.incident.malware_url), + source_ip: observablesToString(params.incident.source_ip), + }, + }; + } + + /** + * For non legacy connectors the observables + * will be added in a different call. + * They need to be set to null when sending the fields + * to ServiceNow + */ + return { + ...params, + incident: { + ...params.incident, + dest_ip: null, + malware_hash: null, + malware_url: null, + source_ip: null, + }, + }; +}; + +const pushToServiceHandler = async ({ + externalService, + params, + config, + secrets, + commentFieldKey, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const res = await api.pushToService({ + externalService, + params: prepareParams(!!config.isLegacy, params as PushToServiceApiParamsSIR), + config, + secrets, + commentFieldKey, + logger, + }); + + const { + incident: { + dest_ip: destIP, + malware_hash: malwareHash, + malware_url: malwareUrl, + source_ip: sourceIP, + }, + } = params as ExecutorSubActionPushParamsSIR; + + /** + * Add bulk observables is only available for new connectors + * Old connectors gonna add their observables + * through the pushToService call. + */ + + if (!config.isLegacy) { + const sirExternalService = externalService as ExternalServiceSIR; + + const obsWithType: Array<[string[], ObservableTypes]> = [ + [combineObservables(destIP ?? [], sourceIP ?? []), ObservableTypes.ip4], + [obsAsArray(malwareHash ?? []), ObservableTypes.sha256], + [obsAsArray(malwareUrl ?? []), ObservableTypes.url], + ]; + + const observables = obsWithType.map(([obs, type]) => formatObservables(obs, type)).flat(); + if (observables.length > 0) { + await sirExternalService.bulkAddObservableToIncident(observables, res.id); + } + } + + return res; +}; + +export const apiSIR: ExternalServiceAPI = { + ...api, + pushToService: pushToServiceHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts new file mode 100644 index 0000000000000..babd360cbcb82 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { snExternalServiceConfig } from './config'; + +/** + * The purpose of this test is to + * prevent developers from accidentally + * change important configuration values + * such as the scope or the import set table + * of our ServiceNow application + */ + +describe('config', () => { + test('ITSM: the config are correct', async () => { + const snConfig = snExternalServiceConfig['.servicenow']; + expect(snConfig).toEqual({ + importSetTable: 'x_elas2_inc_int_elastic_incident', + appScope: 'x_elas2_inc_int', + table: 'incident', + useImportAPI: true, + commentFieldKey: 'work_notes', + }); + }); + + test('SIR: the config are correct', async () => { + const snConfig = snExternalServiceConfig['.servicenow-sir']; + expect(snConfig).toEqual({ + importSetTable: 'x_elas2_sir_int_elastic_si_incident', + appScope: 'x_elas2_sir_int', + table: 'sn_si_incident', + useImportAPI: true, + commentFieldKey: 'work_notes', + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts new file mode 100644 index 0000000000000..37e4c6994b403 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + ENABLE_NEW_SN_ITSM_CONNECTOR, + ENABLE_NEW_SN_SIR_CONNECTOR, +} from '../../constants/connectors'; +import { SNProductsConfig } from './types'; + +export const serviceNowITSMTable = 'incident'; +export const serviceNowSIRTable = 'sn_si_incident'; + +export const ServiceNowITSMActionTypeId = '.servicenow'; +export const ServiceNowSIRActionTypeId = '.servicenow-sir'; + +export const snExternalServiceConfig: SNProductsConfig = { + '.servicenow': { + importSetTable: 'x_elas2_inc_int_elastic_incident', + appScope: 'x_elas2_inc_int', + table: 'incident', + useImportAPI: ENABLE_NEW_SN_ITSM_CONNECTOR, + commentFieldKey: 'work_notes', + }, + '.servicenow-sir': { + importSetTable: 'x_elas2_sir_int_elastic_si_incident', + appScope: 'x_elas2_sir_int', + table: 'sn_si_incident', + useImportAPI: ENABLE_NEW_SN_SIR_CONNECTOR, + commentFieldKey: 'work_notes', + }, +}; + +export const FIELD_PREFIX = 'u_'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index f2b500df6ccb3..29907381d45da 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -18,7 +18,7 @@ import { import { ActionsConfigurationUtilities } from '../../actions_config'; import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; import { createExternalService } from './service'; -import { api } from './api'; +import { api as commonAPI } from './api'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; import { @@ -30,7 +30,25 @@ import { ExecutorSubActionCommonFieldsParams, ServiceNowExecutorResultData, ExecutorSubActionGetChoicesParams, + ServiceFactory, + ExternalServiceAPI, } from './types'; +import { + ServiceNowITSMActionTypeId, + serviceNowITSMTable, + ServiceNowSIRActionTypeId, + serviceNowSIRTable, + snExternalServiceConfig, +} from './config'; +import { createExternalServiceSIR } from './service_sir'; +import { apiSIR } from './api_sir'; + +export { + ServiceNowITSMActionTypeId, + serviceNowITSMTable, + ServiceNowSIRActionTypeId, + serviceNowSIRTable, +}; export type ActionParamsType = | TypeOf @@ -41,12 +59,6 @@ interface GetActionTypeParams { configurationUtilities: ActionsConfigurationUtilities; } -const serviceNowITSMTable = 'incident'; -const serviceNowSIRTable = 'sn_si_incident'; - -export const ServiceNowITSMActionTypeId = '.servicenow'; -export const ServiceNowSIRActionTypeId = '.servicenow-sir'; - export type ServiceNowActionType = ActionType< ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, @@ -79,8 +91,9 @@ export function getServiceNowITSMActionType(params: GetActionTypeParams): Servic executor: curry(executor)({ logger, configurationUtilities, - table: serviceNowITSMTable, - commentFieldKey: 'work_notes', + actionTypeId: ServiceNowITSMActionTypeId, + createService: createExternalService, + api: commonAPI, }), }; } @@ -103,8 +116,9 @@ export function getServiceNowSIRActionType(params: GetActionTypeParams): Service executor: curry(executor)({ logger, configurationUtilities, - table: serviceNowSIRTable, - commentFieldKey: 'work_notes', + actionTypeId: ServiceNowSIRActionTypeId, + createService: createExternalServiceSIR, + api: apiSIR, }), }; } @@ -115,28 +129,31 @@ async function executor( { logger, configurationUtilities, - table, - commentFieldKey = 'comments', + actionTypeId, + createService, + api, }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; - table: string; - commentFieldKey?: string; + actionTypeId: string; + createService: ServiceFactory; + api: ExternalServiceAPI; }, execOptions: ServiceNowActionTypeExecutorOptions ): Promise> { const { actionId, config, params, secrets } = execOptions; const { subAction, subActionParams } = params; + const externalServiceConfig = snExternalServiceConfig[actionTypeId]; let data: ServiceNowExecutorResultData | null = null; - const externalService = createExternalService( - table, + const externalService = createService( { config, secrets, }, logger, - configurationUtilities + configurationUtilities, + externalServiceConfig ); if (!api[subAction]) { @@ -156,9 +173,10 @@ async function executor( data = await api.pushToService({ externalService, params: pushToServiceParams, + config, secrets, logger, - commentFieldKey, + commentFieldKey: externalServiceConfig.commentFieldKey, }); logger.debug(`response push to service for incident id: ${data.id}`); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 909200472be33..3629fb33915ae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -5,7 +5,14 @@ * 2.0. */ -import { ExternalService, ExecutorSubActionPushParams } from './types'; +import { + ExternalService, + ExecutorSubActionPushParams, + PushToServiceApiParamsSIR, + ExternalServiceSIR, + Observable, + ObservableTypes, +} from './types'; export const serviceNowCommonFields = [ { @@ -74,6 +81,10 @@ const createMock = (): jest.Mocked => { getFields: jest.fn().mockImplementation(() => Promise.resolve(serviceNowCommonFields)), getIncident: jest.fn().mockImplementation(() => Promise.resolve({ + id: 'incident-1', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://instance.service-now.com/nav_to.do?uri=incident.do?sys_id=123', short_description: 'title from servicenow', description: 'description from servicenow', }) @@ -95,16 +106,60 @@ const createMock = (): jest.Mocked => { }) ), findIncidents: jest.fn(), + getApplicationInformation: jest.fn().mockImplementation(() => + Promise.resolve({ + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', + }) + ), + checkIfApplicationIsInstalled: jest.fn(), + getUrl: jest.fn().mockImplementation(() => 'https://instance.service-now.com'), + checkInstance: jest.fn(), }; return service; }; -const externalServiceMock = { +const createSIRMock = (): jest.Mocked => { + const service = { + ...createMock(), + addObservableToIncident: jest.fn().mockImplementation(() => + Promise.resolve({ + value: 'https://example.com', + observable_sys_id: '3', + }) + ), + bulkAddObservableToIncident: jest.fn().mockImplementation(() => + Promise.resolve([ + { + value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + observable_sys_id: '1', + }, + { + value: '127.0.0.1', + observable_sys_id: '2', + }, + { + value: 'https://example.com', + observable_sys_id: '3', + }, + ]) + ), + }; + + return service; +}; + +export const externalServiceMock = { create: createMock, }; -const executorParams: ExecutorSubActionPushParams = { +export const externalServiceSIRMock = { + create: createSIRMock, +}; + +export const executorParams: ExecutorSubActionPushParams = { incident: { externalId: 'incident-3', short_description: 'Incident title', @@ -114,6 +169,8 @@ const executorParams: ExecutorSubActionPushParams = { impact: '3', category: 'software', subcategory: 'os', + correlation_id: 'ruleId', + correlation_display: 'Alerting', }, comments: [ { @@ -127,6 +184,46 @@ const executorParams: ExecutorSubActionPushParams = { ], }; -const apiParams = executorParams; +export const sirParams: PushToServiceApiParamsSIR = { + incident: { + externalId: 'incident-3', + short_description: 'Incident title', + description: 'Incident description', + dest_ip: ['192.168.1.1', '192.168.1.3'], + source_ip: ['192.168.1.2', '192.168.1.4'], + malware_hash: ['5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9'], + malware_url: ['https://example.com'], + category: 'software', + subcategory: 'os', + correlation_id: 'ruleId', + correlation_display: 'Alerting', + priority: '1', + }, + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + ], +}; + +export const observables: Observable[] = [ + { + value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + type: ObservableTypes.sha256, + }, + { + value: '127.0.0.1', + type: ObservableTypes.ip4, + }, + { + value: 'https://example.com', + type: ObservableTypes.url, + }, +]; -export { externalServiceMock, executorParams, apiParams }; +export const apiParams = executorParams; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 6fec30803d6d7..dab68bb9d3e9d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -9,6 +9,7 @@ import { schema } from '@kbn/config-schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), + isLegacy: schema.boolean({ defaultValue: false }), }; export const ExternalIncidentServiceConfigurationSchema = schema.object( @@ -39,6 +40,8 @@ const CommonAttributes = { externalId: schema.nullable(schema.string()), category: schema.nullable(schema.string()), subcategory: schema.nullable(schema.string()), + correlation_id: schema.nullable(schema.string()), + correlation_display: schema.nullable(schema.string()), }; // Schema for ServiceNow Incident Management (ITSM) @@ -56,10 +59,22 @@ export const ExecutorSubActionPushParamsSchemaITSM = schema.object({ export const ExecutorSubActionPushParamsSchemaSIR = schema.object({ incident: schema.object({ ...CommonAttributes, - dest_ip: schema.nullable(schema.string()), - malware_hash: schema.nullable(schema.string()), - malware_url: schema.nullable(schema.string()), - source_ip: schema.nullable(schema.string()), + dest_ip: schema.oneOf( + [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))], + { defaultValue: null } + ), + malware_hash: schema.oneOf( + [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))], + { defaultValue: null } + ), + malware_url: schema.oneOf( + [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))], + { defaultValue: null } + ), + source_ip: schema.oneOf( + [schema.nullable(schema.string()), schema.nullable(schema.arrayOf(schema.string()))], + { defaultValue: null } + ), priority: schema.nullable(schema.string()), }), comments: CommentsSchema, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts index 37bfb662508a2..b8499b01e6a02 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.test.ts @@ -5,15 +5,16 @@ * 2.0. */ -import axios from 'axios'; +import axios, { AxiosResponse } from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; -import { ExternalService } from './types'; +import { ExternalService, ServiceNowITSMIncident } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; import { actionsConfigMock } from '../../actions_config.mock'; import { serviceNowCommonFields, serviceNowChoices } from './mocks'; +import { snExternalServiceConfig } from './config'; const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); @@ -28,24 +29,134 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; -const patchMock = utils.patch as jest.Mock; const configurationUtilities = actionsConfigMock.create(); -const table = 'incident'; + +const getImportSetAPIResponse = (update = false) => ({ + import_set: 'ISET01', + staging_table: 'x_elas2_inc_int_elastic_incident', + result: [ + { + transform_map: 'Elastic Incident', + table: 'incident', + display_name: 'number', + display_value: 'INC01', + record_link: 'https://example.com/api/now/table/incident/1', + status: update ? 'updated' : 'inserted', + sys_id: '1', + }, + ], +}); + +const getImportSetAPIError = () => ({ + import_set: 'ISET01', + staging_table: 'x_elas2_inc_int_elastic_incident', + result: [ + { + transform_map: 'Elastic Incident', + status: 'error', + error_message: 'An error has occurred while importing the incident', + status_message: 'failure', + }, + ], +}); + +const mockApplicationVersion = () => + requestMock.mockImplementationOnce(() => ({ + data: { + result: { name: 'Elastic', scope: 'x_elas2_inc_int', version: '1.0.0' }, + }, + })); + +const mockImportIncident = (update: boolean) => + requestMock.mockImplementationOnce(() => ({ + data: getImportSetAPIResponse(update), + })); + +const mockIncidentResponse = (update: boolean) => + requestMock.mockImplementation(() => ({ + data: { + result: { + sys_id: '1', + number: 'INC01', + ...(update + ? { sys_updated_on: '2020-03-10 12:24:20' } + : { sys_created_on: '2020-03-10 12:24:20' }), + }, + }, + })); + +const createIncident = async (service: ExternalService) => { + // Get application version + mockApplicationVersion(); + // Import set api response + mockImportIncident(false); + // Get incident response + mockIncidentResponse(false); + + return await service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); +}; + +const updateIncident = async (service: ExternalService) => { + // Get application version + mockApplicationVersion(); + // Import set api response + mockImportIncident(true); + // Get incident response + mockIncidentResponse(true); + + return await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); +}; + +const expectImportedIncident = (update: boolean) => { + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/x_elas2_inc_int/elastic_api/health', + method: 'get', + }); + + expect(requestMock).toHaveBeenNthCalledWith(2, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/import/x_elas2_inc_int_elastic_incident', + method: 'post', + data: { + u_short_description: 'title', + u_description: 'desc', + ...(update ? { elastic_incident_id: '1' } : {}), + }, + }); + + expect(requestMock).toHaveBeenNthCalledWith(3, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/incident/1', + method: 'get', + }); +}; describe('ServiceNow service', () => { let service: ExternalService; beforeEach(() => { service = createExternalService( - table, { // The trailing slash at the end of the url is intended. // All API calls need to have the trailing slash removed. - config: { apiUrl: 'https://dev102283.service-now.com/' }, + config: { apiUrl: 'https://example.com/' }, secrets: { username: 'admin', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + snExternalServiceConfig['.servicenow'] ); }); @@ -57,13 +168,13 @@ describe('ServiceNow service', () => { test('throws without url', () => { expect(() => createExternalService( - table, { config: { apiUrl: null }, secrets: { username: 'admin', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + snExternalServiceConfig['.servicenow'] ) ).toThrow(); }); @@ -71,13 +182,13 @@ describe('ServiceNow service', () => { test('throws without username', () => { expect(() => createExternalService( - table, { config: { apiUrl: 'test.com' }, secrets: { username: '', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + snExternalServiceConfig['.servicenow'] ) ).toThrow(); }); @@ -85,13 +196,13 @@ describe('ServiceNow service', () => { test('throws without password', () => { expect(() => createExternalService( - table, { config: { apiUrl: 'test.com' }, secrets: { username: '', password: undefined }, }, logger, - configurationUtilities + configurationUtilities, + snExternalServiceConfig['.servicenow'] ) ).toThrow(); }); @@ -116,19 +227,20 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', + url: 'https://example.com/api/now/v2/table/incident/1', + method: 'get', }); }); test('it should call request with correct arguments when table changes', async () => { service = createExternalService( - 'sn_si_incident', { - config: { apiUrl: 'https://dev102283.service-now.com/' }, + config: { apiUrl: 'https://example.com/' }, secrets: { username: 'admin', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' } ); requestMock.mockImplementation(() => ({ @@ -140,7 +252,8 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1', + url: 'https://example.com/api/now/v2/table/sn_si_incident/1', + method: 'get', }); }); @@ -166,214 +279,346 @@ describe('ServiceNow service', () => { }); describe('createIncident', () => { - test('it creates the incident correctly', async () => { - requestMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - })); - - const res = await service.createIncident({ - incident: { short_description: 'title', description: 'desc' }, + // new connectors + describe('import set table', () => { + test('it creates the incident correctly', async () => { + const res = await createIncident(service); + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1', + }); }); - expect(res).toEqual({ - title: 'INC01', - id: '1', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + test('it should call request with correct arguments', async () => { + await createIncident(service); + expect(requestMock).toHaveBeenCalledTimes(3); + expectImportedIncident(false); }); - }); - test('it should call request with correct arguments', async () => { - requestMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - })); + test('it should call request with correct arguments when table changes', async () => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + snExternalServiceConfig['.servicenow-sir'] + ); - await service.createIncident({ - incident: { short_description: 'title', description: 'desc' }, - }); + const res = await createIncident(service); - expect(requestMock).toHaveBeenCalledWith({ - axios, - logger, - configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/incident', - method: 'post', - data: { short_description: 'title', description: 'desc' }, - }); - }); + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health', + method: 'get', + }); - test('it should call request with correct arguments when table changes', async () => { - service = createExternalService( - 'sn_si_incident', - { - config: { apiUrl: 'https://dev102283.service-now.com/' }, - secrets: { username: 'admin', password: 'admin' }, - }, - logger, - configurationUtilities - ); + expect(requestMock).toHaveBeenNthCalledWith(2, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident', + method: 'post', + data: { u_short_description: 'title', u_description: 'desc' }, + }); + + expect(requestMock).toHaveBeenNthCalledWith(3, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/sn_si_incident/1', + method: 'get', + }); - requestMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' } }, - })); + expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'); + }); - const res = await service.createIncident({ - incident: { short_description: 'title', description: 'desc' }, + test('it should throw an error when the application is not installed', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create incident. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null' + ); }); - expect(requestMock).toHaveBeenCalledWith({ - axios, - logger, - configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident', - method: 'post', - data: { short_description: 'title', description: 'desc' }, + test('it should throw an error when instance is not alive', async () => { + requestMock.mockImplementation(() => ({ + status: 200, + data: {}, + request: { connection: { servername: 'Developer instance' } }, + })); + await expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + 'There is an issue with your Service Now Instance. Please check Developer instance.' + ); }); - expect(res.url).toEqual( - 'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1' - ); + test('it should throw an error when there is an import set api error', async () => { + requestMock.mockImplementation(() => ({ data: getImportSetAPIError() })); + await expect( + service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred while importing the incident Reason: unknown' + ); + }); }); - test('it should throw an error', async () => { - requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + // old connectors + describe('table API', () => { + beforeEach(() => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], useImportAPI: false } + ); }); - await expect( - service.createIncident({ - incident: { short_description: 'title', description: 'desc' }, - }) - ).rejects.toThrow( - '[Action][ServiceNow]: Unable to create incident. Error: An error has occurred' - ); - }); + test('it creates the incident correctly', async () => { + mockIncidentResponse(false); + const res = await service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1', + }); + + expect(requestMock).toHaveBeenCalledTimes(2); + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/incident', + method: 'post', + data: { short_description: 'title', description: 'desc' }, + }); + }); - test('it should throw an error when instance is not alive', async () => { - requestMock.mockImplementation(() => ({ - status: 200, - data: {}, - request: { connection: { servername: 'Developer instance' } }, - })); - await expect(service.getIncident('1')).rejects.toThrow( - 'There is an issue with your Service Now Instance. Please check Developer instance.' - ); - }); - }); + test('it should call request with correct arguments when table changes', async () => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false } + ); - describe('updateIncident', () => { - test('it updates the incident correctly', async () => { - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - })); + mockIncidentResponse(false); - const res = await service.updateIncident({ - incidentId: '1', - incident: { short_description: 'title', description: 'desc' }, - }); + const res = await service.createIncident({ + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); + + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/sn_si_incident', + method: 'post', + data: { short_description: 'title', description: 'desc' }, + }); - expect(res).toEqual({ - title: 'INC01', - id: '1', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=1', + expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'); }); }); + }); - test('it should call request with correct arguments', async () => { - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - })); - - await service.updateIncident({ - incidentId: '1', - incident: { short_description: 'title', description: 'desc' }, + describe('updateIncident', () => { + // new connectors + describe('import set table', () => { + test('it updates the incident correctly', async () => { + const res = await updateIncident(service); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1', + }); }); - expect(patchMock).toHaveBeenCalledWith({ - axios, - logger, - configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/incident/1', - data: { short_description: 'title', description: 'desc' }, + test('it should call request with correct arguments', async () => { + await updateIncident(service); + expectImportedIncident(true); }); - }); - test('it should call request with correct arguments when table changes', async () => { - service = createExternalService( - 'sn_si_incident', - { - config: { apiUrl: 'https://dev102283.service-now.com/' }, - secrets: { username: 'admin', password: 'admin' }, - }, - logger, - configurationUtilities - ); + test('it should call request with correct arguments when table changes', async () => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + snExternalServiceConfig['.servicenow-sir'] + ); - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '1', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' } }, - })); + const res = await updateIncident(service); + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health', + method: 'get', + }); - const res = await service.updateIncident({ - incidentId: '1', - incident: { short_description: 'title', description: 'desc' }, + expect(requestMock).toHaveBeenNthCalledWith(2, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/import/x_elas2_sir_int_elastic_si_incident', + method: 'post', + data: { u_short_description: 'title', u_description: 'desc', elastic_incident_id: '1' }, + }); + + expect(requestMock).toHaveBeenNthCalledWith(3, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/sn_si_incident/1', + method: 'get', + }); + + expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'); }); - expect(patchMock).toHaveBeenCalledWith({ - axios, - logger, - configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sn_si_incident/1', - data: { short_description: 'title', description: 'desc' }, + test('it should throw an error when the application is not installed', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to update incident with id 1. Error: [Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown: errorResponse was null Reason: unknown: errorResponse was null' + ); }); - expect(res.url).toEqual( - 'https://dev102283.service-now.com/nav_to.do?uri=sn_si_incident.do?sys_id=1' - ); + test('it should throw an error when instance is not alive', async () => { + requestMock.mockImplementation(() => ({ + status: 200, + data: {}, + request: { connection: { servername: 'Developer instance' } }, + })); + await expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + 'There is an issue with your Service Now Instance. Please check Developer instance.' + ); + }); + + test('it should throw an error when there is an import set api error', async () => { + requestMock.mockImplementation(() => ({ data: getImportSetAPIError() })); + await expect( + service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }) + ).rejects.toThrow( + '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred while importing the incident Reason: unknown' + ); + }); }); - test('it should throw an error', async () => { - patchMock.mockImplementation(() => { - throw new Error('An error has occurred'); + // old connectors + describe('table API', () => { + beforeEach(() => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], useImportAPI: false } + ); }); - await expect( - service.updateIncident({ + test('it updates the incident correctly', async () => { + mockIncidentResponse(true); + const res = await service.updateIncident({ incidentId: '1', - incident: { short_description: 'title', description: 'desc' }, - }) - ).rejects.toThrow( - '[Action][ServiceNow]: Unable to update incident with id 1. Error: An error has occurred' - ); - }); + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); + + expect(res).toEqual({ + title: 'INC01', + id: '1', + pushedDate: '2020-03-10T12:24:20.000Z', + url: 'https://example.com/nav_to.do?uri=incident.do?sys_id=1', + }); + + expect(requestMock).toHaveBeenCalledTimes(2); + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/incident/1', + method: 'patch', + data: { short_description: 'title', description: 'desc' }, + }); + }); - test('it creates the comment correctly', async () => { - patchMock.mockImplementation(() => ({ - data: { result: { sys_id: '11', number: 'INC011', sys_updated_on: '2020-03-10 12:24:20' } }, - })); + test('it should call request with correct arguments when table changes', async () => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + { ...snExternalServiceConfig['.servicenow-sir'], useImportAPI: false } + ); - const res = await service.updateIncident({ - incidentId: '1', - comment: 'comment-1', - }); + mockIncidentResponse(false); - expect(res).toEqual({ - title: 'INC011', - id: '11', - pushedDate: '2020-03-10T12:24:20.000Z', - url: 'https://dev102283.service-now.com/nav_to.do?uri=incident.do?sys_id=11', - }); - }); + const res = await service.updateIncident({ + incidentId: '1', + incident: { short_description: 'title', description: 'desc' } as ServiceNowITSMIncident, + }); - test('it should throw an error when instance is not alive', async () => { - requestMock.mockImplementation(() => ({ - status: 200, - data: {}, - request: { connection: { servername: 'Developer instance' } }, - })); - await expect(service.getIncident('1')).rejects.toThrow( - 'There is an issue with your Service Now Instance. Please check Developer instance.' - ); + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/now/v2/table/sn_si_incident/1', + method: 'patch', + data: { short_description: 'title', description: 'desc' }, + }); + + expect(res.url).toEqual('https://example.com/nav_to.do?uri=sn_si_incident.do?sys_id=1'); + }); }); }); @@ -388,7 +633,7 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', + url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', }); }); @@ -402,13 +647,13 @@ describe('ServiceNow service', () => { test('it should call request with correct arguments when table changes', async () => { service = createExternalService( - 'sn_si_incident', { - config: { apiUrl: 'https://dev102283.service-now.com/' }, + config: { apiUrl: 'https://example.com/' }, secrets: { username: 'admin', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' } ); requestMock.mockImplementation(() => ({ @@ -420,7 +665,7 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', + url: 'https://example.com/api/now/table/sys_dictionary?sysparm_query=name=task^ORname=sn_si_incident^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory', }); }); @@ -456,7 +701,7 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element', + url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element', }); }); @@ -470,13 +715,13 @@ describe('ServiceNow service', () => { test('it should call request with correct arguments when table changes', async () => { service = createExternalService( - 'sn_si_incident', { - config: { apiUrl: 'https://dev102283.service-now.com/' }, + config: { apiUrl: 'https://example.com/' }, secrets: { username: 'admin', password: 'admin' }, }, logger, - configurationUtilities + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], table: 'sn_si_incident' } ); requestMock.mockImplementation(() => ({ @@ -489,7 +734,7 @@ describe('ServiceNow service', () => { axios, logger, configurationUtilities, - url: 'https://dev102283.service-now.com/api/now/v2/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element', + url: 'https://example.com/api/now/table/sys_choice?sysparm_query=name=task^ORname=sn_si_incident^element=priority^ORelement=category&sysparm_fields=label,value,dependent_value,element', }); }); @@ -513,4 +758,79 @@ describe('ServiceNow service', () => { ); }); }); + + describe('getUrl', () => { + test('it returns the instance url', async () => { + expect(service.getUrl()).toBe('https://example.com'); + }); + }); + + describe('checkInstance', () => { + test('it throws an error if there is no result on data', () => { + const res = { status: 200, data: {} } as AxiosResponse; + expect(() => service.checkInstance(res)).toThrow(); + }); + + test('it does NOT throws an error if the status > 400', () => { + const res = { status: 500, data: {} } as AxiosResponse; + expect(() => service.checkInstance(res)).not.toThrow(); + }); + + test('it shows the servername', () => { + const res = { + status: 200, + data: {}, + request: { connection: { servername: 'https://example.com' } }, + } as AxiosResponse; + expect(() => service.checkInstance(res)).toThrow( + 'There is an issue with your Service Now Instance. Please check https://example.com.' + ); + }); + + describe('getApplicationInformation', () => { + test('it returns the application information', async () => { + mockApplicationVersion(); + const res = await service.getApplicationInformation(); + expect(res).toEqual({ + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + await expect(service.getApplicationInformation()).rejects.toThrow( + '[Action][ServiceNow]: Unable to get application version. Error: An error has occurred Reason: unknown' + ); + }); + }); + + describe('checkIfApplicationIsInstalled', () => { + test('it logs the application information', async () => { + mockApplicationVersion(); + await service.checkIfApplicationIsInstalled(); + expect(logger.debug).toHaveBeenCalledWith( + 'Create incident: Application scope: x_elas2_inc_int: Application version1.0.0' + ); + }); + + test('it does not log if useOldApi = true', async () => { + service = createExternalService( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + { ...snExternalServiceConfig['.servicenow'], useImportAPI: false } + ); + await service.checkIfApplicationIsInstalled(); + expect(requestMock).not.toHaveBeenCalled(); + expect(logger.debug).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts index 07ed9edc94d39..cb030c7bb6933 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts @@ -7,28 +7,35 @@ import axios, { AxiosResponse } from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from './types'; +import { + ExternalServiceCredentials, + ExternalService, + ExternalServiceParamsCreate, + ExternalServiceParamsUpdate, + ImportSetApiResponse, + ImportSetApiResponseError, + ServiceNowIncident, + GetApplicationInfoResponse, + SNProductsConfigValue, + ServiceFactory, +} from './types'; import * as i18n from './translations'; import { Logger } from '../../../../../../src/core/server'; -import { - ServiceNowPublicConfigurationType, - ServiceNowSecretConfigurationType, - ResponseError, -} from './types'; -import { request, getErrorMessage, addTimeZoneToDate, patch } from '../lib/axios_utils'; +import { ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType } from './types'; +import { request } from '../lib/axios_utils'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { createServiceError, getPushedDate, prepareIncident } from './utils'; -const API_VERSION = 'v2'; -const SYS_DICTIONARY = `api/now/${API_VERSION}/table/sys_dictionary`; +export const SYS_DICTIONARY_ENDPOINT = `api/now/table/sys_dictionary`; -export const createExternalService = ( - table: string, +export const createExternalService: ServiceFactory = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, - configurationUtilities: ActionsConfigurationUtilities + configurationUtilities: ActionsConfigurationUtilities, + { table, importSetTable, useImportAPI, appScope }: SNProductsConfigValue ): ExternalService => { - const { apiUrl: url } = config as ServiceNowPublicConfigurationType; + const { apiUrl: url, isLegacy } = config as ServiceNowPublicConfigurationType; const { username, password } = secrets as ServiceNowSecretConfigurationType; if (!url || !username || !password) { @@ -36,13 +43,26 @@ export const createExternalService = ( } const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; - const incidentUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/${table}`; - const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY}?sysparm_query=name=task^ORname=${table}^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`; - const choicesUrl = `${urlWithoutTrailingSlash}/api/now/${API_VERSION}/table/sys_choice`; + const importSetTableUrl = `${urlWithoutTrailingSlash}/api/now/import/${importSetTable}`; + const tableApiIncidentUrl = `${urlWithoutTrailingSlash}/api/now/v2/table/${table}`; + const fieldsUrl = `${urlWithoutTrailingSlash}/${SYS_DICTIONARY_ENDPOINT}?sysparm_query=name=task^ORname=${table}^internal_type=string&active=true&array=false&read_only=false&sysparm_fields=max_length,element,column_label,mandatory`; + const choicesUrl = `${urlWithoutTrailingSlash}/api/now/table/sys_choice`; + /** + * Need to be set the same at: + * x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts + */ + const getVersionUrl = () => `${urlWithoutTrailingSlash}/api/${appScope}/elastic_api/health`; + const axiosInstance = axios.create({ auth: { username, password }, }); + const useOldApi = !useImportAPI || isLegacy; + + const getCreateIncidentUrl = () => (useOldApi ? tableApiIncidentUrl : importSetTableUrl); + const getUpdateIncidentUrl = (incidentId: string) => + useOldApi ? `${tableApiIncidentUrl}/${incidentId}` : importSetTableUrl; + const getIncidentViewURL = (id: string) => { // Based on: https://docs.servicenow.com/bundle/orlando-platform-user-interface/page/use/navigation/reference/r_NavigatingByURLExamples.html return `${urlWithoutTrailingSlash}/nav_to.do?uri=${table}.do?sys_id=${id}`; @@ -57,7 +77,7 @@ export const createExternalService = ( }; const checkInstance = (res: AxiosResponse) => { - if (res.status === 200 && res.data.result == null) { + if (res.status >= 200 && res.status < 400 && res.data.result == null) { throw new Error( `There is an issue with your Service Now Instance. Please check ${ res.request?.connection?.servername ?? '' @@ -66,34 +86,70 @@ export const createExternalService = ( } }; - const createErrorMessage = (errorResponse: ResponseError): string => { - if (errorResponse == null) { - return ''; + const isImportSetApiResponseAnError = ( + data: ImportSetApiResponse['result'][0] + ): data is ImportSetApiResponseError['result'][0] => data.status === 'error'; + + const throwIfImportSetApiResponseIsAnError = (res: ImportSetApiResponse) => { + if (res.result.length === 0) { + throw new Error('Unexpected result'); } - const { error } = errorResponse; - return error != null ? `${error?.message}: ${error?.detail}` : ''; + const data = res.result[0]; + + // Create ResponseError message? + if (isImportSetApiResponseAnError(data)) { + throw new Error(data.error_message); + } }; - const getIncident = async (id: string) => { + /** + * Gets the Elastic SN Application information including the current version. + * It should not be used on legacy connectors. + */ + const getApplicationInformation = async (): Promise => { try { const res = await request({ axios: axiosInstance, - url: `${incidentUrl}/${id}`, + url: getVersionUrl(), logger, configurationUtilities, + method: 'get', }); + checkInstance(res); + return { ...res.data.result }; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to get incident with id ${id}. Error: ${ - error.message - } Reason: ${createErrorMessage(error.response?.data)}` - ) - ); + throw createServiceError(error, 'Unable to get application version'); + } + }; + + const logApplicationInfo = (scope: string, version: string) => + logger.debug(`Create incident: Application scope: ${scope}: Application version${version}`); + + const checkIfApplicationIsInstalled = async () => { + if (!useOldApi) { + const { version, scope } = await getApplicationInformation(); + logApplicationInfo(scope, version); + } + }; + + const getIncident = async (id: string): Promise => { + try { + const res = await request({ + axios: axiosInstance, + url: `${tableApiIncidentUrl}/${id}`, + logger, + configurationUtilities, + method: 'get', + }); + + checkInstance(res); + + return { ...res.data.result }; + } catch (error) { + throw createServiceError(error, `Unable to get incident with id ${id}`); } }; @@ -101,7 +157,7 @@ export const createExternalService = ( try { const res = await request({ axios: axiosInstance, - url: incidentUrl, + url: tableApiIncidentUrl, logger, params, configurationUtilities, @@ -109,71 +165,80 @@ export const createExternalService = ( checkInstance(res); return res.data.result.length > 0 ? { ...res.data.result } : undefined; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to find incidents by query. Error: ${error.message} Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); + throw createServiceError(error, 'Unable to find incidents by query'); } }; - const createIncident = async ({ incident }: ExternalServiceParams) => { + const getUrl = () => urlWithoutTrailingSlash; + + const createIncident = async ({ incident }: ExternalServiceParamsCreate) => { try { + await checkIfApplicationIsInstalled(); + const res = await request({ axios: axiosInstance, - url: `${incidentUrl}`, + url: getCreateIncidentUrl(), logger, method: 'post', - data: { ...(incident as Record) }, + data: prepareIncident(useOldApi, incident), configurationUtilities, }); + checkInstance(res); + + if (!useOldApi) { + throwIfImportSetApiResponseIsAnError(res.data); + } + + const incidentId = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const insertedIncident = await getIncident(incidentId); + return { - title: res.data.result.number, - id: res.data.result.sys_id, - pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_created_on)).toISOString(), - url: getIncidentViewURL(res.data.result.sys_id), + title: insertedIncident.number, + id: insertedIncident.sys_id, + pushedDate: getPushedDate(insertedIncident.sys_created_on), + url: getIncidentViewURL(insertedIncident.sys_id), }; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to create incident. Error: ${error.message} Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); + throw createServiceError(error, 'Unable to create incident'); } }; - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + const updateIncident = async ({ incidentId, incident }: ExternalServiceParamsUpdate) => { try { - const res = await patch({ + await checkIfApplicationIsInstalled(); + + const res = await request({ axios: axiosInstance, - url: `${incidentUrl}/${incidentId}`, + url: getUpdateIncidentUrl(incidentId), + // Import Set API supports only POST. + method: useOldApi ? 'patch' : 'post', logger, - data: { ...(incident as Record) }, + data: { + ...prepareIncident(useOldApi, incident), + // elastic_incident_id is used to update the incident when using the Import Set API. + ...(useOldApi ? {} : { elastic_incident_id: incidentId }), + }, configurationUtilities, }); + checkInstance(res); + + if (!useOldApi) { + throwIfImportSetApiResponseIsAnError(res.data); + } + + const id = useOldApi ? res.data.result.sys_id : res.data.result[0].sys_id; + const updatedIncident = await getIncident(id); + return { - title: res.data.result.number, - id: res.data.result.sys_id, - pushedDate: new Date(addTimeZoneToDate(res.data.result.sys_updated_on)).toISOString(), - url: getIncidentViewURL(res.data.result.sys_id), + title: updatedIncident.number, + id: updatedIncident.sys_id, + pushedDate: getPushedDate(updatedIncident.sys_updated_on), + url: getIncidentViewURL(updatedIncident.sys_id), }; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to update incident with id ${incidentId}. Error: ${ - error.message - } Reason: ${createErrorMessage(error.response?.data)}` - ) - ); + throw createServiceError(error, `Unable to update incident with id ${incidentId}`); } }; @@ -185,17 +250,12 @@ export const createExternalService = ( logger, configurationUtilities, }); + checkInstance(res); + return res.data.result.length > 0 ? res.data.result : []; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to get fields. Error: ${error.message} Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); + throw createServiceError(error, 'Unable to get fields'); } }; @@ -210,14 +270,7 @@ export const createExternalService = ( checkInstance(res); return res.data.result; } catch (error) { - throw new Error( - getErrorMessage( - i18n.SERVICENOW, - `Unable to get choices. Error: ${error.message} Reason: ${createErrorMessage( - error.response?.data - )}` - ) - ); + throw createServiceError(error, 'Unable to get choices'); } }; @@ -228,5 +281,9 @@ export const createExternalService = ( getIncident, updateIncident, getChoices, + getUrl, + checkInstance, + getApplicationInformation, + checkIfApplicationIsInstalled, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts new file mode 100644 index 0000000000000..0fc94b6287abd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.test.ts @@ -0,0 +1,129 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import axios from 'axios'; + +import { createExternalServiceSIR } from './service_sir'; +import * as utils from '../lib/axios_utils'; +import { ExternalServiceSIR } from './types'; +import { Logger } from '../../../../../../src/core/server'; +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { actionsConfigMock } from '../../actions_config.mock'; +import { observables } from './mocks'; +import { snExternalServiceConfig } from './config'; + +const logger = loggingSystemMock.create().get() as jest.Mocked; + +jest.mock('axios'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + patch: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); + +const mockApplicationVersion = () => + requestMock.mockImplementationOnce(() => ({ + data: { + result: { name: 'Elastic', scope: 'x_elas2_sir_int', version: '1.0.0' }, + }, + })); + +const getAddObservablesResponse = () => [ + { + value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + observable_sys_id: '1', + }, + { + value: '127.0.0.1', + observable_sys_id: '2', + }, + { + value: 'https://example.com', + observable_sys_id: '3', + }, +]; + +const mockAddObservablesResponse = (single: boolean) => { + const res = getAddObservablesResponse(); + requestMock.mockImplementation(() => ({ + data: { + result: single ? res[0] : res, + }, + })); +}; + +const expectAddObservables = (single: boolean) => { + expect(requestMock).toHaveBeenNthCalledWith(1, { + axios, + logger, + configurationUtilities, + url: 'https://example.com/api/x_elas2_sir_int/elastic_api/health', + method: 'get', + }); + + const url = single + ? 'https://example.com/api/x_elas2_sir_int/elastic_api/incident/incident-1/observables' + : 'https://example.com/api/x_elas2_sir_int/elastic_api/incident/incident-1/observables/bulk'; + + const data = single ? observables[0] : observables; + + expect(requestMock).toHaveBeenNthCalledWith(2, { + axios, + logger, + configurationUtilities, + url, + method: 'post', + data, + }); +}; + +describe('ServiceNow SIR service', () => { + let service: ExternalServiceSIR; + + beforeEach(() => { + service = createExternalServiceSIR( + { + config: { apiUrl: 'https://example.com/' }, + secrets: { username: 'admin', password: 'admin' }, + }, + logger, + configurationUtilities, + snExternalServiceConfig['.servicenow-sir'] + ) as ExternalServiceSIR; + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('bulkAddObservableToIncident', () => { + test('it adds multiple observables correctly', async () => { + mockApplicationVersion(); + mockAddObservablesResponse(false); + + const res = await service.bulkAddObservableToIncident(observables, 'incident-1'); + expect(res).toEqual(getAddObservablesResponse()); + expectAddObservables(false); + }); + + test('it adds a single observable correctly', async () => { + mockApplicationVersion(); + mockAddObservablesResponse(true); + + const res = await service.addObservableToIncident(observables[0], 'incident-1'); + expect(res).toEqual(getAddObservablesResponse()[0]); + expectAddObservables(true); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts new file mode 100644 index 0000000000000..fc8d8cc555bc8 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/service_sir.ts @@ -0,0 +1,104 @@ +/* + * 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 axios from 'axios'; + +import { + ExternalServiceCredentials, + SNProductsConfigValue, + Observable, + ExternalServiceSIR, + ObservableResponse, + ServiceFactory, +} from './types'; + +import { Logger } from '../../../../../../src/core/server'; +import { ServiceNowSecretConfigurationType } from './types'; +import { request } from '../lib/axios_utils'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { createExternalService } from './service'; +import { createServiceError } from './utils'; + +const getAddObservableToIncidentURL = (url: string, incidentID: string) => + `${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables`; + +const getBulkAddObservableToIncidentURL = (url: string, incidentID: string) => + `${url}/api/x_elas2_sir_int/elastic_api/incident/${incidentID}/observables/bulk`; + +export const createExternalServiceSIR: ServiceFactory = ( + credentials: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities, + serviceConfig: SNProductsConfigValue +): ExternalServiceSIR => { + const snService = createExternalService( + credentials, + logger, + configurationUtilities, + serviceConfig + ); + + const { username, password } = credentials.secrets as ServiceNowSecretConfigurationType; + const axiosInstance = axios.create({ + auth: { username, password }, + }); + + const _addObservable = async (data: Observable | Observable[], url: string) => { + snService.checkIfApplicationIsInstalled(); + + const res = await request({ + axios: axiosInstance, + url, + logger, + method: 'post', + data, + configurationUtilities, + }); + + snService.checkInstance(res); + return res.data.result; + }; + + const addObservableToIncident = async ( + observable: Observable, + incidentID: string + ): Promise => { + try { + return await _addObservable( + observable, + getAddObservableToIncidentURL(snService.getUrl(), incidentID) + ); + } catch (error) { + throw createServiceError( + error, + `Unable to add observable to security incident with id ${incidentID}` + ); + } + }; + + const bulkAddObservableToIncident = async ( + observables: Observable[], + incidentID: string + ): Promise => { + try { + return await _addObservable( + observables, + getBulkAddObservableToIncidentURL(snService.getUrl(), incidentID) + ); + } catch (error) { + throw createServiceError( + error, + `Unable to add observables to security incident with id ${incidentID}` + ); + } + }; + return { + ...snService, + addObservableToIncident, + bulkAddObservableToIncident, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 50631cf289a73..ecca1e55e0fec 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -7,6 +7,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import { AxiosError, AxiosResponse } from 'axios'; import { TypeOf } from '@kbn/config-schema'; import { ExecutorParamsSchemaITSM, @@ -78,15 +79,29 @@ export interface PushToServiceResponse extends ExternalServiceIncidentResponse { comments?: ExternalServiceCommentResponse[]; } -export type ExternalServiceParams = Record; +export type Incident = ServiceNowITSMIncident | ServiceNowSIRIncident; +export type PartialIncident = Partial; + +export interface ExternalServiceParamsCreate { + incident: Incident & Record; +} + +export interface ExternalServiceParamsUpdate { + incidentId: string; + incident: PartialIncident & Record; +} export interface ExternalService { getChoices: (fields: string[]) => Promise; - getIncident: (id: string) => Promise; + getIncident: (id: string) => Promise; getFields: () => Promise; - createIncident: (params: ExternalServiceParams) => Promise; - updateIncident: (params: ExternalServiceParams) => Promise; - findIncidents: (params?: Record) => Promise; + createIncident: (params: ExternalServiceParamsCreate) => Promise; + updateIncident: (params: ExternalServiceParamsUpdate) => Promise; + findIncidents: (params?: Record) => Promise; + getUrl: () => string; + checkInstance: (res: AxiosResponse) => void; + getApplicationInformation: () => Promise; + checkIfApplicationIsInstalled: () => Promise; } export type PushToServiceApiParams = ExecutorSubActionPushParams; @@ -115,10 +130,9 @@ export type ServiceNowSIRIncident = Omit< 'externalId' >; -export type Incident = ServiceNowITSMIncident | ServiceNowSIRIncident; - export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; + config: Record; secrets: Record; logger: Logger; commentFieldKey: string; @@ -158,12 +172,20 @@ export interface GetChoicesHandlerArgs { params: ExecutorSubActionGetChoicesParams; } -export interface ExternalServiceApi { +export interface ServiceNowIncident { + sys_id: string; + number: string; + sys_created_on: string; + sys_updated_on: string; + [x: string]: unknown; +} + +export interface ExternalServiceAPI { getChoices: (args: GetChoicesHandlerArgs) => Promise; getFields: (args: GetCommonFieldsHandlerArgs) => Promise; handshake: (args: HandshakeApiHandlerArgs) => Promise; pushToService: (args: PushToServiceApiHandlerArgs) => Promise; - getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; } export interface ExternalServiceCommentResponse { @@ -173,10 +195,90 @@ export interface ExternalServiceCommentResponse { } type TypeNullOrUndefined = T | null | undefined; -export interface ResponseError { + +export interface ServiceNowError { error: TypeNullOrUndefined<{ message: TypeNullOrUndefined; detail: TypeNullOrUndefined; }>; status: TypeNullOrUndefined; } + +export type ResponseError = AxiosError; + +export interface ImportSetApiResponseSuccess { + import_set: string; + staging_table: string; + result: Array<{ + display_name: string; + display_value: string; + record_link: string; + status: string; + sys_id: string; + table: string; + transform_map: string; + }>; +} + +export interface ImportSetApiResponseError { + import_set: string; + staging_table: string; + result: Array<{ + error_message: string; + status_message: string; + status: string; + transform_map: string; + }>; +} + +export type ImportSetApiResponse = ImportSetApiResponseSuccess | ImportSetApiResponseError; +export interface GetApplicationInfoResponse { + id: string; + name: string; + scope: string; + version: string; +} + +export interface SNProductsConfigValue { + table: string; + appScope: string; + useImportAPI: boolean; + importSetTable: string; + commentFieldKey: string; +} + +export type SNProductsConfig = Record; + +export enum ObservableTypes { + ip4 = 'ipv4-addr', + url = 'URL', + sha256 = 'SHA256', +} + +export interface Observable { + value: string; + type: ObservableTypes; +} + +export interface ObservableResponse { + value: string; + observable_sys_id: ObservableTypes; +} + +export interface ExternalServiceSIR extends ExternalService { + addObservableToIncident: ( + observable: Observable, + incidentID: string + ) => Promise; + bulkAddObservableToIncident: ( + observables: Observable[], + incidentID: string + ) => Promise; +} + +export type ServiceFactory = ( + credentials: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities, + serviceConfig: SNProductsConfigValue +) => ExternalServiceSIR | ExternalService; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts new file mode 100644 index 0000000000000..87f27da6d213f --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.test.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AxiosError } from 'axios'; +import { prepareIncident, createServiceError, getPushedDate } from './utils'; + +/** + * The purpose of this test is to + * prevent developers from accidentally + * change important configuration values + * such as the scope or the import set table + * of our ServiceNow application + */ + +describe('utils', () => { + describe('prepareIncident', () => { + test('it prepares the incident correctly when useOldApi=false', async () => { + const incident = { short_description: 'title', description: 'desc' }; + const newIncident = prepareIncident(false, incident); + expect(newIncident).toEqual({ u_short_description: 'title', u_description: 'desc' }); + }); + + test('it prepares the incident correctly when useOldApi=true', async () => { + const incident = { short_description: 'title', description: 'desc' }; + const newIncident = prepareIncident(true, incident); + expect(newIncident).toEqual(incident); + }); + }); + + describe('createServiceError', () => { + test('it creates an error when the response is null', async () => { + const error = new Error('An error occurred'); + // @ts-expect-error + expect(createServiceError(error, 'Unable to do action').message).toBe( + '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: unknown: errorResponse was null' + ); + }); + + test('it creates an error with response correctly', async () => { + const axiosError = { + message: 'An error occurred', + response: { data: { error: { message: 'Denied', detail: 'no access' } } }, + } as AxiosError; + + expect(createServiceError(axiosError, 'Unable to do action').message).toBe( + '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: Denied: no access' + ); + }); + + test('it creates an error correctly when the ServiceNow error is null', async () => { + const axiosError = { + message: 'An error occurred', + response: { data: { error: null } }, + } as AxiosError; + + expect(createServiceError(axiosError, 'Unable to do action').message).toBe( + '[Action][ServiceNow]: Unable to do action. Error: An error occurred Reason: unknown: no error in error response' + ); + }); + }); + + describe('getPushedDate', () => { + beforeAll(() => { + jest.useFakeTimers('modern'); + jest.setSystemTime(new Date('2021-10-04 11:15:06 GMT')); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + test('it formats the date correctly if timestamp is provided', async () => { + expect(getPushedDate('2021-10-04 11:15:06')).toBe('2021-10-04T11:15:06.000Z'); + }); + + test('it formats the date correctly if timestamp is not provided', async () => { + expect(getPushedDate()).toBe('2021-10-04T11:15:06.000Z'); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts new file mode 100644 index 0000000000000..5b7ca99ffc709 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/utils.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Incident, PartialIncident, ResponseError, ServiceNowError } from './types'; +import { FIELD_PREFIX } from './config'; +import { addTimeZoneToDate, getErrorMessage } from '../lib/axios_utils'; +import * as i18n from './translations'; + +export const prepareIncident = (useOldApi: boolean, incident: PartialIncident): PartialIncident => + useOldApi + ? incident + : Object.entries(incident).reduce( + (acc, [key, value]) => ({ ...acc, [`${FIELD_PREFIX}${key}`]: value }), + {} as Incident + ); + +const createErrorMessage = (errorResponse?: ServiceNowError): string => { + if (errorResponse == null) { + return 'unknown: errorResponse was null'; + } + + const { error } = errorResponse; + return error != null + ? `${error?.message}: ${error?.detail}` + : 'unknown: no error in error response'; +}; + +export const createServiceError = (error: ResponseError, message: string) => + new Error( + getErrorMessage( + i18n.SERVICENOW, + `${message}. Error: ${error.message} Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + +export const getPushedDate = (timestamp?: string) => { + if (timestamp != null) { + return new Date(addTimeZoneToDate(timestamp)).toISOString(); + } + + return new Date().toISOString(); +}; diff --git a/x-pack/plugins/actions/server/constants/connectors.ts b/x-pack/plugins/actions/server/constants/connectors.ts new file mode 100644 index 0000000000000..f20d499716cf0 --- /dev/null +++ b/x-pack/plugins/actions/server/constants/connectors.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +// TODO: Remove when Elastic for ITSM is published. +export const ENABLE_NEW_SN_ITSM_CONNECTOR = true; + +// TODO: Remove when Elastic for Security Operations is published. +export const ENABLE_NEW_SN_SIR_CONNECTOR = true; diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index c094109a43d97..9f8e62c77e3a7 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -165,6 +165,47 @@ describe('successful migrations', () => { }); expect(migratedAction).toEqual(action); }); + + test('set isLegacy config property for .servicenow', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForServiceNow(); + const migratedAction = migration716(action, context); + + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + apiUrl: 'https://example.com', + isLegacy: true, + }, + }, + }); + }); + + test('set isLegacy config property for .servicenow-sir', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockDataForServiceNow({ actionTypeId: '.servicenow-sir' }); + const migratedAction = migration716(action, context); + + expect(migratedAction).toEqual({ + ...action, + attributes: { + ...action.attributes, + config: { + apiUrl: 'https://example.com', + isLegacy: true, + }, + }, + }); + }); + + test('it does not set isLegacy config for other connectors', () => { + const migration716 = getActionsMigrations(encryptedSavedObjectsSetup)['7.16.0']; + const action = getMockData(); + const migratedAction = migration716(action, context); + expect(migratedAction).toEqual(action); + }); }); describe('8.0.0', () => { @@ -306,3 +347,19 @@ function getMockData( type: 'action', }; } + +function getMockDataForServiceNow( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc> { + return { + attributes: { + name: 'abc', + actionTypeId: '.servicenow', + config: { apiUrl: 'https://example.com' }, + secrets: { user: 'test', password: '123' }, + ...overwrites, + }, + id: uuid.v4(), + type: 'action', + }; +} diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index e75f3eb41f2df..688839eb89858 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -59,13 +59,16 @@ export function getActionsMigrations( const migrationActionsFourteen = createEsoMigration( encryptedSavedObjects, (doc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(addisMissingSecretsField) + pipeMigrations(addIsMissingSecretsField) ); - const migrationEmailActionsSixteen = createEsoMigration( + const migrationActionsSixteen = createEsoMigration( encryptedSavedObjects, - (doc): doc is SavedObjectUnsanitizedDoc => doc.attributes.actionTypeId === '.email', - pipeMigrations(setServiceConfigIfNotSet) + (doc): doc is SavedObjectUnsanitizedDoc => + doc.attributes.actionTypeId === '.servicenow' || + doc.attributes.actionTypeId === '.servicenow-sir' || + doc.attributes.actionTypeId === '.email', + pipeMigrations(markOldServiceNowITSMConnectorAsLegacy, setServiceConfigIfNotSet) ); const migrationActions800 = createEsoMigration( @@ -79,7 +82,7 @@ export function getActionsMigrations( '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), - '7.16.0': executeMigrationWithErrorHandling(migrationEmailActionsSixteen, '7.16.0'), + '7.16.0': executeMigrationWithErrorHandling(migrationActionsSixteen, '7.16.0'), '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } @@ -182,7 +185,7 @@ const setServiceConfigIfNotSet = ( }; }; -const addisMissingSecretsField = ( +const addIsMissingSecretsField = ( doc: SavedObjectUnsanitizedDoc ): SavedObjectUnsanitizedDoc => { return { @@ -194,6 +197,28 @@ const addisMissingSecretsField = ( }; }; +const markOldServiceNowITSMConnectorAsLegacy = ( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc => { + if ( + doc.attributes.actionTypeId !== '.servicenow' && + doc.attributes.actionTypeId !== '.servicenow-sir' + ) { + return doc; + } + + return { + ...doc, + attributes: { + ...doc.attributes, + config: { + ...doc.attributes.config, + isLegacy: true, + }, + }, + }; +}; + function pipeMigrations(...migrations: ActionMigration[]): ActionMigration { return (doc: SavedObjectUnsanitizedDoc) => migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index f894ca23dfbf0..f28926eb52052 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -1,9 +1,9 @@ -Case management in Kibana +# Case management in Kibana [![Issues][issues-shield]][issues-url] -[![Pull Requests][pr-shield]][pr-url] +[![Pull Requests][pr-shield]][pr-url] -# Cases Plugin Docs +# Docs ![Cases Logo][cases-logo] @@ -288,9 +288,9 @@ Connectors of type (`.none`) should have the `fields` attribute set to `null`. -[pr-shield]: https://img.shields.io/github/issues-pr/elangosundar/awesome-README-templates?style=for-the-badge -[pr-url]: https://github.com/elastic/kibana/pulls?q=is%3Apr+label%3AFeature%3ACases+-is%3Adraft+is%3Aopen+ -[issues-shield]: https://img.shields.io/github/issues/othneildrew/Best-README-Template.svg?style=for-the-badge +[pr-shield]: https://img.shields.io/github/issues-pr/elastic/kibana/Team:Threat%20Hunting:Cases?label=pull%20requests&style=for-the-badge +[pr-url]: https://github.com/elastic/kibana/pulls?q=is%3Apr+is%3Aopen+sort%3Aupdated-desc+label%3A%22Team%3AThreat+Hunting%3ACases%22 +[issues-shield]: https://img.shields.io/github/issues-search?label=issue&query=repo%3Aelastic%2Fkibana%20is%3Aissue%20is%3Aopen%20label%3A%22Team%3AThreat%20Hunting%3ACases%22&style=for-the-badge [issues-url]: https://github.com/elastic/kibana/issues?q=is%3Aopen+is%3Aissue+label%3AFeature%3ACases [cases-logo]: images/logo.png [configure-img]: images/configure.png diff --git a/x-pack/plugins/cases/common/ui/types.ts b/x-pack/plugins/cases/common/ui/types.ts index 948b203af14a8..b4ed4f7db177e 100644 --- a/x-pack/plugins/cases/common/ui/types.ts +++ b/x-pack/plugins/cases/common/ui/types.ts @@ -16,6 +16,7 @@ import { User, UserAction, UserActionField, + ActionConnector, } from '../api'; export interface CasesUiConfigType { @@ -259,3 +260,5 @@ export interface Ecs { _index?: string; signal?: SignalEcs; } + +export type CaseActionConnector = ActionConnector; diff --git a/x-pack/plugins/cases/public/common/mock/register_connectors.ts b/x-pack/plugins/cases/public/common/mock/register_connectors.ts new file mode 100644 index 0000000000000..42e7cd4a85e40 --- /dev/null +++ b/x-pack/plugins/cases/public/common/mock/register_connectors.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; +import { CaseActionConnector } from '../../../common'; + +const getUniqueActionTypeIds = (connectors: CaseActionConnector[]) => + new Set(connectors.map((connector) => connector.actionTypeId)); + +export const registerConnectorsToMockActionRegistry = ( + actionTypeRegistry: TriggersAndActionsUIPublicPluginStart['actionTypeRegistry'], + connectors: CaseActionConnector[] +) => { + const { createMockActionTypeModel } = actionTypeRegistryMock; + const uniqueActionTypeIds = getUniqueActionTypeIds(connectors); + uniqueActionTypeIds.forEach((actionTypeId) => + actionTypeRegistry.register( + createMockActionTypeModel({ id: actionTypeId, iconClass: 'logoSecurity' }) + ) + ); +}; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx index 0e548fd53c89d..fed23564a3955 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_generic.test.tsx @@ -19,8 +19,8 @@ import { useKibana } from '../../common/lib/kibana'; import { StatusAll } from '../../containers/types'; import { CaseStatuses, SECURITY_SOLUTION_OWNER } from '../../../common'; import { connectorsMock } from '../../containers/mock'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; jest.mock('../../containers/use_get_reporters'); jest.mock('../../containers/use_get_tags'); @@ -59,14 +59,10 @@ jest.mock('../../common/lib/kibana', () => { }); describe('AllCasesGeneric ', () => { - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectorsMock.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock); }); beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx index 015ba877a2749..090ac0d31ed06 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.test.tsx @@ -12,21 +12,17 @@ import '../../common/mock/match_media'; import { ExternalServiceColumn } from './columns'; import { useGetCasesMockState } from '../../containers/mock'; import { useKibana } from '../../common/lib/kibana'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { connectors } from '../configure_cases/__mock__'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; describe('ExternalServiceColumn ', () => { - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectors.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectors); }); it('Not pushed render', () => { diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 3fff43108772d..a387c5eae3834 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -32,8 +32,8 @@ import { useKibana } from '../../common/lib/kibana'; import { AllCasesGeneric as AllCases } from './all_cases_generic'; import { AllCasesProps } from '.'; import { CasesColumns, GetCasesColumn, useCasesColumns } from './columns'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; jest.mock('../../containers/use_bulk_update_case'); jest.mock('../../containers/use_delete_cases'); @@ -148,14 +148,10 @@ describe('AllCasesGeneric', () => { userCanCrud: true, }; - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectorsMock.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock); }); beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 0bda6fe185093..38923784d862c 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -7,6 +7,7 @@ import React from 'react'; import { mount, ReactWrapper } from 'enzyme'; +import { render, screen } from '@testing-library/react'; import { Connectors, Props } from './connectors'; import { TestProviders } from '../../common/mock'; @@ -14,6 +15,7 @@ import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; import { ConnectorTypes } from '../../../common'; import { useKibana } from '../../common/lib/kibana'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; @@ -35,11 +37,10 @@ describe('Connectors', () => { updateConnectorDisabled: false, }; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; + beforeAll(() => { - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.get = jest.fn().mockReturnValue({ - actionTypeTitle: 'test', - iconClass: 'logoSecurity', - }); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectors); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -121,4 +122,33 @@ describe('Connectors', () => { .text() ).toBe('Update My Connector'); }); + + test('it shows the deprecated callout when the connector is legacy', async () => { + render( + , + { + // wrapper: TestProviders produces a TS error + wrapper: ({ children }) => {children}, + } + ); + + expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); + expect( + screen.getByText( + 'This connector type is deprecated. Create a new connector or update this connector' + ) + ).toBeInTheDocument(); + }); + + test('it does not shows the deprecated callout when the connector is none', async () => { + render(, { + // wrapper: TestProviders produces a TS error + wrapper: ({ children }) => {children}, + }); + + expect(screen.queryByText('Deprecated connector type')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index 40f314a653882..1b575e3ba9334 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -22,6 +22,8 @@ import * as i18n from './translations'; import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types'; import { Mapping } from './mapping'; import { ActionTypeConnector, ConnectorTypes } from '../../../common'; +import { DeprecatedCallout } from '../connectors/deprecated_callout'; +import { isLegacyConnector } from '../utils'; const EuiFormRowExtended = styled(EuiFormRow)` .euiFormRow__labelWrapper { @@ -53,11 +55,13 @@ const ConnectorsComponent: React.FC = ({ selectedConnector, updateConnectorDisabled, }) => { - const connectorsName = useMemo( - () => connectors.find((c) => c.id === selectedConnector.id)?.name ?? 'none', + const connector = useMemo( + () => connectors.find((c) => c.id === selectedConnector.id), [connectors, selectedConnector.id] ); + const connectorsName = connector?.name ?? 'none'; + const actionTypeName = useMemo( () => actionTypes.find((c) => c.id === selectedConnector.type)?.name ?? 'Unknown', [actionTypes, selectedConnector.type] @@ -107,6 +111,11 @@ const ConnectorsComponent: React.FC = ({ appendAddConnectorButton={true} /> + {selectedConnector.type !== ConnectorTypes.none && isLegacyConnector(connector) && ( + + + + )} {selectedConnector.type !== ConnectorTypes.none ? ( ; @@ -28,14 +29,10 @@ describe('ConnectorsDropdown', () => { selectedConnector: 'none', }; - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectors.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectors); wrapper = mount(, { wrappingComponent: TestProviders }); }); @@ -77,7 +74,7 @@ describe('ConnectorsDropdown', () => { "data-test-subj": "dropdown-connector-servicenow-1", "inputDisplay": { type="logoSecurity" /> - + My Connector @@ -100,7 +99,7 @@ describe('ConnectorsDropdown', () => { "data-test-subj": "dropdown-connector-resilient-2", "inputDisplay": { type="logoSecurity" /> - + My Connector 2 @@ -123,7 +124,7 @@ describe('ConnectorsDropdown', () => { "data-test-subj": "dropdown-connector-jira-1", "inputDisplay": { type="logoSecurity" /> - + Jira @@ -146,7 +149,7 @@ describe('ConnectorsDropdown', () => { "data-test-subj": "dropdown-connector-servicenow-sir", "inputDisplay": { type="logoSecurity" /> - + My Connector SIR @@ -165,6 +170,43 @@ describe('ConnectorsDropdown', () => { , "value": "servicenow-sir", }, + Object { + "data-test-subj": "dropdown-connector-servicenow-legacy", + "inputDisplay": + + + + + + My Connector + + + + + + , + "value": "servicenow-legacy", + }, ] `); }); @@ -245,4 +287,13 @@ describe('ConnectorsDropdown', () => { ) ).not.toThrowError(); }); + + test('it shows the deprecated tooltip when the connector is legacy', () => { + render(, { + wrapper: ({ children }) => {children}, + }); + + const tooltips = screen.getAllByLabelText('Deprecated connector'); + expect(tooltips[0]).toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index 3cab2afd41f41..f21b3ab3d544f 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -6,14 +6,14 @@ */ import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiSuperSelect } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; import { ConnectorTypes } from '../../../common'; import { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; import { useKibana } from '../../common/lib/kibana'; -import { getConnectorIcon } from '../utils'; +import { getConnectorIcon, isLegacyConnector } from '../utils'; export interface Props { connectors: ActionConnector[]; @@ -79,16 +79,28 @@ const ConnectorsDropdownComponent: React.FC = ({ { value: connector.id, inputDisplay: ( - + - + {connector.name} + {isLegacyConnector(connector) && ( + + + + )} ), 'data-test-subj': `dropdown-connector-${connector.id}`, diff --git a/x-pack/plugins/cases/public/components/configure_cases/translations.ts b/x-pack/plugins/cases/public/components/configure_cases/translations.ts index 878d261369340..4a775c78d4ab8 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/translations.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/translations.ts @@ -162,3 +162,17 @@ export const UPDATE_SELECTED_CONNECTOR = (connectorName: string): string => values: { connectorName }, defaultMessage: 'Update { connectorName }', }); + +export const DEPRECATED_TOOLTIP_TITLE = i18n.translate( + 'xpack.cases.configureCases.deprecatedTooltipTitle', + { + defaultMessage: 'Deprecated connector', + } +); + +export const DEPRECATED_TOOLTIP_CONTENT = i18n.translate( + 'xpack.cases.configureCases.deprecatedTooltipContent', + { + defaultMessage: 'Please update your connector', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/card.test.tsx b/x-pack/plugins/cases/public/components/connectors/card.test.tsx index b5d70a6781916..384442814ffef 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.test.tsx @@ -10,22 +10,18 @@ import { mount } from 'enzyme'; import { ConnectorTypes } from '../../../common'; import { useKibana } from '../../common/lib/kibana'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { connectors } from '../configure_cases/__mock__'; import { ConnectorCard } from './card'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; jest.mock('../../common/lib/kibana'); const useKibanaMock = useKibana as jest.Mocked; describe('ConnectorCard ', () => { - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectors.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectors); }); it('it does not throw when accessing the icon if the connector type is not registered', () => { diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx new file mode 100644 index 0000000000000..6b1475e3c4bd0 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { DeprecatedCallout } from './deprecated_callout'; + +describe('DeprecatedCallout', () => { + test('it renders correctly', () => { + render(); + expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); + expect( + screen.getByText( + 'This connector type is deprecated. Create a new connector or update this connector' + ) + ).toBeInTheDocument(); + expect(screen.getByTestId('legacy-connector-warning-callout')).toHaveClass( + 'euiCallOut euiCallOut--warning' + ); + }); + + test('it renders a danger flyout correctly', () => { + render(); + expect(screen.getByTestId('legacy-connector-warning-callout')).toHaveClass( + 'euiCallOut euiCallOut--danger' + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx new file mode 100644 index 0000000000000..937f8406e218a --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/deprecated_callout.tsx @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { EuiCallOut, EuiCallOutProps } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +const LEGACY_CONNECTOR_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.serviceNow.legacyConnectorWarningTitle', + { + defaultMessage: 'Deprecated connector type', + } +); + +const LEGACY_CONNECTOR_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.serviceNow.legacyConnectorWarningDesc', + { + defaultMessage: + 'This connector type is deprecated. Create a new connector or update this connector', + } +); + +interface Props { + type?: EuiCallOutProps['color']; +} + +const DeprecatedCalloutComponent: React.FC = ({ type = 'warning' }) => ( + + {LEGACY_CONNECTOR_WARNING_DESC} + +); + +export const DeprecatedCallout = React.memo(DeprecatedCalloutComponent); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx index b14842bbf1bbf..008340b6b7e97 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { waitFor, act } from '@testing-library/react'; +import { waitFor, act, render, screen } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; import { mount } from 'enzyme'; @@ -127,6 +127,17 @@ describe('ServiceNowITSM Fields', () => { ); }); + test('it shows the deprecated callout when the connector is legacy', async () => { + const legacyConnector = { ...connector, config: { isLegacy: true } }; + render(); + expect(screen.getByTestId('legacy-connector-warning-callout')).toBeInTheDocument(); + }); + + test('it does not show the deprecated callout when the connector is not legacy', async () => { + render(); + expect(screen.queryByTestId('legacy-connector-warning-callout')).not.toBeInTheDocument(); + }); + describe('onChange calls', () => { const wrapper = mount(); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index 53c0d32dea1a5..096e450c736c1 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -16,6 +16,8 @@ import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; import { Fields, Choice } from './types'; import { choicesToEuiOptions } from './helpers'; +import { connectorValidator } from './validator'; +import { DeprecatedCallout } from '../deprecated_callout'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; const defaultFields: Fields = { @@ -39,6 +41,7 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< } = fields ?? {}; const { http, notifications } = useKibana().services; const [choices, setChoices] = useState(defaultFields); + const showConnectorWarning = useMemo(() => connectorValidator(connector) != null, [connector]); const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); @@ -149,90 +152,111 @@ const ServiceNowITSMFieldsComponent: React.FunctionComponent< } }, [category, impact, onChange, severity, subcategory, urgency]); - return isEdit ? ( -
- - onChangeCb('urgency', e.target.value)} - /> - - - - - - onChangeCb('severity', e.target.value)} + return ( + <> + {showConnectorWarning && ( + + + + + + )} + {isEdit ? ( +
+ + + + onChangeCb('urgency', e.target.value)} + /> + + + + + + + + onChangeCb('severity', e.target.value)} + /> + + + + + onChangeCb('impact', e.target.value)} + /> + + + + + + + + onChange({ ...fields, category: e.target.value, subcategory: null }) + } + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + + -
-
- - - onChangeCb('impact', e.target.value)} - /> - - -
- - - - onChange({ ...fields, category: e.target.value, subcategory: null })} - /> - - - - - onChangeCb('subcategory', e.target.value)} - /> - - - -
- ) : ( - +
+
+ )} + ); }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx index 7d42c90a436f7..aac78b8266fb5 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { waitFor, act } from '@testing-library/react'; +import { waitFor, act, render, screen } from '@testing-library/react'; import { EuiSelect } from '@elastic/eui'; import { useKibana } from '../../../common/lib/kibana'; @@ -68,16 +68,16 @@ describe('ServiceNowSIR Fields', () => { wrapper.update(); expect(wrapper.find('[data-test-subj="card-list-item"]').at(0).text()).toEqual( - 'Destination IP: Yes' + 'Destination IPs: Yes' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(1).text()).toEqual( - 'Source IP: Yes' + 'Source IPs: Yes' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(2).text()).toEqual( - 'Malware URL: Yes' + 'Malware URLs: Yes' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(3).text()).toEqual( - 'Malware Hash: Yes' + 'Malware Hashes: Yes' ); expect(wrapper.find('[data-test-subj="card-list-item"]').at(4).text()).toEqual( 'Priority: 1 - Critical' @@ -161,6 +161,17 @@ describe('ServiceNowSIR Fields', () => { ]); }); + test('it shows the deprecated callout when the connector is legacy', async () => { + const legacyConnector = { ...connector, config: { isLegacy: true } }; + render(); + expect(screen.getByTestId('legacy-connector-warning-callout')).toBeInTheDocument(); + }); + + test('it does not show the deprecated callout when the connector is not legacy', async () => { + render(); + expect(screen.queryByTestId('legacy-connector-warning-callout')).not.toBeInTheDocument(); + }); + describe('onChange calls', () => { const wrapper = mount(); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx index 1f9a7cf7acd64..a7b8aa7b27df5 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -17,6 +17,8 @@ import { Choice, Fields } from './types'; import { choicesToEuiOptions } from './helpers'; import * as i18n from './translations'; +import { connectorValidator } from './validator'; +import { DeprecatedCallout } from '../deprecated_callout'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -40,8 +42,8 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< } = fields ?? {}; const { http, notifications } = useKibana().services; - const [choices, setChoices] = useState(defaultFields); + const showConnectorWarning = useMemo(() => connectorValidator(connector) != null, [connector]); const onChangeCb = useCallback( ( @@ -166,115 +168,132 @@ const ServiceNowSIRFieldsComponent: React.FunctionComponent< } }, [category, destIp, malwareHash, malwareUrl, onChange, priority, sourceIp, subcategory]); - return isEdit ? ( -
- - - - <> - - - onChangeCb('destIp', e.target.checked)} - /> - - - onChangeCb('sourceIp', e.target.checked)} - /> - - - - - onChangeCb('malwareUrl', e.target.checked)} - /> - - - onChangeCb('malwareHash', e.target.checked)} - /> - - - - - - - - - - onChangeCb('priority', e.target.value)} - /> - - - - - - - onChange({ ...fields, category: e.target.value, subcategory: null })} - /> - - - - - onChangeCb('subcategory', e.target.value)} + return ( + <> + {showConnectorWarning && ( + + + + + + )} + {isEdit ? ( +
+ + + + <> + + + onChangeCb('destIp', e.target.checked)} + /> + + + onChangeCb('sourceIp', e.target.checked)} + /> + + + + + onChangeCb('malwareUrl', e.target.checked)} + /> + + + onChangeCb('malwareHash', e.target.checked)} + /> + + + + + + + + + + onChangeCb('priority', e.target.value)} + /> + + + + + + + + onChange({ ...fields, category: e.target.value, subcategory: null }) + } + /> + + + + + onChangeCb('subcategory', e.target.value)} + /> + + + +
+ ) : ( + + + -
-
-
-
- ) : ( - +
+
+ )} + ); }; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts index fc48ecf17f2c6..d9ed86b594ecc 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/translations.ts @@ -30,11 +30,11 @@ export const CHOICES_API_ERROR = i18n.translate( ); export const MALWARE_URL = i18n.translate('xpack.cases.connectors.serviceNow.malwareURLTitle', { - defaultMessage: 'Malware URL', + defaultMessage: 'Malware URLs', }); export const MALWARE_HASH = i18n.translate('xpack.cases.connectors.serviceNow.malwareHashTitle', { - defaultMessage: 'Malware Hash', + defaultMessage: 'Malware Hashes', }); export const CATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.categoryTitle', { @@ -46,11 +46,11 @@ export const SUBCATEGORY = i18n.translate('xpack.cases.connectors.serviceNow.sub }); export const SOURCE_IP = i18n.translate('xpack.cases.connectors.serviceNow.sourceIPTitle', { - defaultMessage: 'Source IP', + defaultMessage: 'Source IPs', }); export const DEST_IP = i18n.translate('xpack.cases.connectors.serviceNow.destinationIPTitle', { - defaultMessage: 'Destination IP', + defaultMessage: 'Destination IPs', }); export const PRIORITY = i18n.translate( diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts new file mode 100644 index 0000000000000..c098d803276bc --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.test.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { connector } from '../mock'; +import { connectorValidator } from './validator'; + +describe('ServiceNow validator', () => { + describe('connectorValidator', () => { + test('it returns an error message if the connector is legacy', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + isLegacy: true, + }, + }; + + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Deprecated connector' }); + }); + + test('it does not returns an error message if the connector is not legacy', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + isLegacy: false, + }, + }; + + expect(connectorValidator(invalidConnector)).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.ts new file mode 100644 index 0000000000000..3f67f25549343 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/validator.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ValidationConfig } from '../../../common/shared_imports'; +import { CaseActionConnector } from '../../types'; + +/** + * The user can not use a legacy connector + */ + +export const connectorValidator = ( + connector: CaseActionConnector +): ReturnType => { + const { + config: { isLegacy }, + } = connector; + if (isLegacy) { + return { + message: 'Deprecated connector', + }; + } +}; diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index a2ffd42f2660b..ea7435c2cba45 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -22,8 +22,8 @@ import { TestProviders } from '../../common/mock'; import { useCaseConfigure } from '../../containers/configure/use_configure'; import { useCaseConfigureResponse } from '../configure_cases/__mock__'; import { triggersActionsUiMock } from '../../../../triggers_actions_ui/public/mocks'; -import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; import { useKibana } from '../../common/lib/kibana'; +import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; const mockTriggersActionsUiService = triggersActionsUiMock.createStart(); @@ -86,14 +86,10 @@ describe('Connector', () => { return
{children}
; }; - const { createMockActionTypeModel } = actionTypeRegistryMock; + const actionTypeRegistry = useKibanaMock().services.triggersActionsUi.actionTypeRegistry; beforeAll(() => { - connectorsMock.forEach((connector) => - useKibanaMock().services.triggersActionsUi.actionTypeRegistry.register( - createMockActionTypeModel({ id: connector.actionTypeId, iconClass: 'logoSecurity' }) - ) - ); + registerConnectorsToMockActionRegistry(actionTypeRegistry, connectorsMock); }); beforeEach(() => { diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts index 014afc371e761..07ab5814b082b 100644 --- a/x-pack/plugins/cases/public/components/types.ts +++ b/x-pack/plugins/cases/public/components/types.ts @@ -5,6 +5,4 @@ * 2.0. */ -import { ActionConnector } from '../../common'; - -export type CaseActionConnector = ActionConnector; +export { CaseActionConnector } from '../../common'; diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 5f7480cb84f7c..ac5f4dbdd298e 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -10,7 +10,13 @@ import { ConnectorTypes } from '../../common'; import { FieldConfig, ValidationConfig } from '../common/shared_imports'; import { StartPlugins } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; +import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator'; import { CaseActionConnector } from './types'; +import { + ENABLE_NEW_SN_ITSM_CONNECTOR, + ENABLE_NEW_SN_SIR_CONNECTOR, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../actions/server/constants/connectors'; export const getConnectorById = ( id: string, @@ -22,6 +28,8 @@ const validators: Record< (connector: CaseActionConnector) => ReturnType > = { [ConnectorTypes.swimlane]: swimlaneConnectorValidator, + [ConnectorTypes.serviceNowITSM]: servicenowConnectorValidator, + [ConnectorTypes.serviceNowSIR]: servicenowConnectorValidator, }; export const getConnectorsFormValidators = ({ @@ -68,3 +76,20 @@ export const getConnectorIcon = ( return emptyResponse; }; + +// TODO: Remove when the applications are certified +export const isLegacyConnector = (connector?: CaseActionConnector) => { + if (connector == null) { + return true; + } + + if (!ENABLE_NEW_SN_ITSM_CONNECTOR && connector.actionTypeId === '.servicenow') { + return true; + } + + if (!ENABLE_NEW_SN_SIR_CONNECTOR && connector.actionTypeId === '.servicenow-sir') { + return true; + } + + return connector.config.isLegacy; +}; diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts index 833c2cfb3aa7c..d1ae7f310a719 100644 --- a/x-pack/plugins/cases/public/containers/configure/mock.ts +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -71,6 +71,16 @@ export const connectorsMock: ActionConnector[] = [ }, isPreconfigured: false, }, + { + id: 'servicenow-legacy', + actionTypeId: '.servicenow', + name: 'My Connector', + config: { + apiUrl: 'https://instance1.service-now.com', + isLegacy: true, + }, + isPreconfigured: false, + }, ]; export const actionTypesMock: ActionTypeConnector[] = [ diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts index 2cc1816e7fa67..ac9dc8839bfb8 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.test.ts @@ -10,6 +10,7 @@ import { format } from './itsm_format'; describe('ITSM formatter', () => { const theCase = { + id: 'case-id', connector: { fields: { severity: '2', urgency: '2', impact: '2', category: 'software', subcategory: 'os' }, }, @@ -17,7 +18,11 @@ describe('ITSM formatter', () => { it('it formats correctly', async () => { const res = await format(theCase, []); - expect(res).toEqual(theCase.connector.fields); + expect(res).toEqual({ + ...theCase.connector.fields, + correlation_display: 'Elastic Case', + correlation_id: 'case-id', + }); }); it('it formats correctly when fields do not exist ', async () => { @@ -29,6 +34,8 @@ describe('ITSM formatter', () => { impact: null, category: null, subcategory: null, + correlation_display: 'Elastic Case', + correlation_id: null, }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts index bc9d50026d1f8..1859ea1246f21 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts @@ -16,5 +16,13 @@ export const format: ServiceNowITSMFormat = (theCase, alerts) => { category = null, subcategory = null, } = (theCase.connector.fields as ConnectorServiceNowITSMTypeFields['fields']) ?? {}; - return { severity, urgency, impact, category, subcategory }; + return { + severity, + urgency, + impact, + category, + subcategory, + correlation_id: theCase.id ?? null, + correlation_display: 'Elastic Case', + }; }; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index fa103d4c1142d..b09272d0a5505 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -10,6 +10,7 @@ import { format } from './sir_format'; describe('ITSM formatter', () => { const theCase = { + id: 'case-id', connector: { fields: { destIp: true, @@ -26,13 +27,15 @@ describe('ITSM formatter', () => { it('it formats correctly without alerts', async () => { const res = await format(theCase, []); expect(res).toEqual({ - dest_ip: null, - source_ip: null, + dest_ip: [], + source_ip: [], category: 'Denial of Service', subcategory: 'Inbound DDos', - malware_hash: null, - malware_url: null, + malware_hash: [], + malware_url: [], priority: '2 - High', + correlation_display: 'Elastic Case', + correlation_id: 'case-id', }); }); @@ -40,13 +43,15 @@ describe('ITSM formatter', () => { const invalidFields = { connector: { fields: null } } as CaseResponse; const res = await format(invalidFields, []); expect(res).toEqual({ - dest_ip: null, - source_ip: null, + dest_ip: [], + source_ip: [], category: null, subcategory: null, - malware_hash: null, - malware_url: null, + malware_hash: [], + malware_url: [], priority: null, + correlation_display: 'Elastic Case', + correlation_id: null, }); }); @@ -75,14 +80,18 @@ describe('ITSM formatter', () => { ]; const res = await format(theCase, alerts); expect(res).toEqual({ - dest_ip: '192.168.1.1,192.168.1.4', - source_ip: '192.168.1.2,192.168.1.3', + dest_ip: ['192.168.1.1', '192.168.1.4'], + source_ip: ['192.168.1.2', '192.168.1.3'], category: 'Denial of Service', subcategory: 'Inbound DDos', - malware_hash: - '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08,60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com,https://attack.com/api', + malware_hash: [ + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', + ], + malware_url: ['https://attack.com', 'https://attack.com/api'], priority: '2 - High', + correlation_display: 'Elastic Case', + correlation_id: 'case-id', }); }); @@ -111,13 +120,15 @@ describe('ITSM formatter', () => { ]; const res = await format(theCase, alerts); expect(res).toEqual({ - dest_ip: '192.168.1.1', - source_ip: '192.168.1.2,192.168.1.3', + dest_ip: ['192.168.1.1'], + source_ip: ['192.168.1.2', '192.168.1.3'], category: 'Denial of Service', subcategory: 'Inbound DDos', - malware_hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', - malware_url: 'https://attack.com,https://attack.com/api', + malware_hash: ['9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'], + malware_url: ['https://attack.com', 'https://attack.com/api'], priority: '2 - High', + correlation_display: 'Elastic Case', + correlation_id: 'case-id', }); }); @@ -152,13 +163,15 @@ describe('ITSM formatter', () => { const res = await format(newCase, alerts); expect(res).toEqual({ - dest_ip: null, - source_ip: '192.168.1.2,192.168.1.3', + dest_ip: [], + source_ip: ['192.168.1.2', '192.168.1.3'], category: 'Denial of Service', subcategory: 'Inbound DDos', - malware_hash: null, - malware_url: 'https://attack.com,https://attack.com/api', + malware_hash: [], + malware_url: ['https://attack.com', 'https://attack.com/api'], priority: '2 - High', + correlation_display: 'Elastic Case', + correlation_id: 'case-id', }); }); }); diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index b48a1b7f734c8..9108408c4d089 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -32,11 +32,11 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { malware_url: new Set(), }; - let sirFields: Record = { - dest_ip: null, - source_ip: null, - malware_hash: null, - malware_url: null, + let sirFields: Record = { + dest_ip: [], + source_ip: [], + malware_hash: [], + malware_url: [], }; const fieldsToAdd = (Object.keys(alertFieldMapping) as SirFieldKey[]).filter( @@ -44,18 +44,17 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { ); if (fieldsToAdd.length > 0) { - sirFields = alerts.reduce>((acc, alert) => { + sirFields = alerts.reduce>((acc, alert) => { fieldsToAdd.forEach((alertField) => { const field = get(alertFieldMapping[alertField].alertPath, alert); if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); acc = { ...acc, - [alertFieldMapping[alertField].sirFieldKey]: `${ - acc[alertFieldMapping[alertField].sirFieldKey] != null - ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` - : field - }`, + [alertFieldMapping[alertField].sirFieldKey]: [ + ...acc[alertFieldMapping[alertField].sirFieldKey], + field, + ], }; } }); @@ -68,5 +67,7 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { category, subcategory, priority, + correlation_id: theCase.id ?? null, + correlation_display: 'Elastic Case', }; }; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/types.ts b/x-pack/plugins/cases/server/connectors/servicenow/types.ts index 2caebc3dab316..b0e71cbe5e743 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/types.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/types.ts @@ -8,13 +8,18 @@ import { ServiceNowITSMFieldsType } from '../../../common'; import { ICasesConnector } from '../types'; -export interface ServiceNowSIRFieldsType { - dest_ip: string | null; - source_ip: string | null; +interface CorrelationValues { + correlation_id: string | null; + correlation_display: string | null; +} + +export interface ServiceNowSIRFieldsType extends CorrelationValues { + dest_ip: string[] | null; + source_ip: string[] | null; category: string | null; subcategory: string | null; - malware_hash: string | null; - malware_url: string | null; + malware_hash: string[] | null; + malware_url: string[] | null; priority: string | null; } @@ -26,7 +31,9 @@ export type AlertFieldMappingAndValues = Record< // ServiceNow ITSM export type ServiceNowITSMCasesConnector = ICasesConnector; -export type ServiceNowITSMFormat = ICasesConnector['format']; +export type ServiceNowITSMFormat = ICasesConnector< + ServiceNowITSMFieldsType & CorrelationValues +>['format']; export type ServiceNowITSMGetMapping = ICasesConnector['getMapping']; // ServiceNow SIR diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index d2120faf09dfb..51511fad90b30 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -301,6 +301,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.swimlane', '.webhook', '.servicenow', + '.servicenow-sir', '.jira', '.resilient', '.teams', diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts index aa1bd7a5db5cc..a53e37f363d05 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/connectors.spec.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { getServiceNowConnector } from '../../objects/case'; +import { getServiceNowConnector, getServiceNowITSMHealthResponse } from '../../objects/case'; import { SERVICE_NOW_MAPPING, TOASTER } from '../../screens/configure_cases'; @@ -43,8 +43,16 @@ describe('Cases connectors', () => { id: '123', owner: 'securitySolution', }; + + const snConnector = getServiceNowConnector(); + beforeEach(() => { cleanKibana(); + cy.intercept('GET', `${snConnector.URL}/api/x_elas2_inc_int/elastic_api/health*`, { + statusCode: 200, + body: getServiceNowITSMHealthResponse(), + }); + cy.intercept('POST', '/api/actions/connector').as('createConnector'); cy.intercept('POST', '/api/cases/configure', (req) => { const connector = req.body.connector; @@ -52,6 +60,7 @@ describe('Cases connectors', () => { res.send(200, { ...configureResult, connector }); }); }).as('saveConnector'); + cy.intercept('GET', '/api/cases/configure', (req) => { req.reply((res) => { const resBody = @@ -77,7 +86,7 @@ describe('Cases connectors', () => { loginAndWaitForPageWithoutDateRange(CASES_URL); goToEditExternalConnection(); openAddNewConnectorOption(); - addServiceNowConnector(getServiceNowConnector()); + addServiceNowConnector(snConnector); cy.wait('@createConnector').then(({ response }) => { cy.wrap(response!.statusCode).should('eql', 200); diff --git a/x-pack/plugins/security_solution/cypress/objects/case.ts b/x-pack/plugins/security_solution/cypress/objects/case.ts index af9b34f542046..b0bfdbf16c705 100644 --- a/x-pack/plugins/security_solution/cypress/objects/case.ts +++ b/x-pack/plugins/security_solution/cypress/objects/case.ts @@ -44,6 +44,14 @@ export interface IbmResilientConnectorOptions { incidentTypes: string[]; } +interface ServiceNowHealthResponse { + result: { + name: string; + scope: string; + version: string; + }; +} + export const getCase1 = (): TestCase => ({ name: 'This is the title of the case', tags: ['Tag1', 'Tag2'], @@ -60,6 +68,14 @@ export const getServiceNowConnector = (): Connector => ({ password: 'password', }); +export const getServiceNowITSMHealthResponse = (): ServiceNowHealthResponse => ({ + result: { + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', + }, +}); + export const getJiraConnectorOptions = (): JiraConnectorOptions => ({ issueType: '10006', priority: 'High', diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 642668bcecf34..54ffd60d0dc16 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -25052,7 +25052,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.unableToGetChoicesMessage": "選択肢を取得できません", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.urgencySelectFieldLabel": "緊急", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "ユーザー名", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "Personal Developer Instance の構成", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle": "ServiceNow ITSM", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText": "ServiceNow ITSMでインシデントを作成します。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle": "ServiceNow SecOps", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4d86994c0fb84..55e4a913c0e09 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -25480,7 +25480,6 @@ "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.unableToGetChoicesMessage": "无法获取选项", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.urgencySelectFieldLabel": "紧急性", "xpack.triggersActionsUI.components.builtinActionTypes.servicenow.usernameTextFieldLabel": "用户名", - "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowAction.apiUrlHelpLabel": "配置个人开发者实例", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.actionTypeTitle": "ServiceNow ITSM", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowITSM.selectMessageText": "在 ServiceNow ITSM 中创建事件。", "xpack.triggersActionsUI.components.builtinActionTypes.serviceNowSIR.actionTypeTitle": "ServiceNow SecOps", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx index 0b446b99c93dc..a96e1fc3dcb5d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_connector.test.tsx @@ -35,6 +35,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); @@ -66,6 +68,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailFromInput"]').length > 0).toBeTruthy(); @@ -99,6 +103,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailFromInput"]').first().prop('value')).toBe( @@ -132,6 +138,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailServiceSelectInput"]').length > 0).toBeTruthy(); @@ -165,6 +173,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(true); @@ -199,6 +209,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="emailHostInput"]').first().prop('disabled')).toBe(false); @@ -223,6 +235,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -245,6 +259,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); @@ -268,6 +284,8 @@ describe('EmailActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx index e804ce2a9f54d..9ef498334ad3d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_connector.test.tsx @@ -71,6 +71,8 @@ describe('IndexActionConnectorFields renders', () => { editActionSecrets: () => {}, errors: { index: [] }, readOnly: false, + setCallbacks: () => {}, + isEdit: false, }; const wrapper = mountWithIntl(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index be5250ccf8b29..4859c25adcc06 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -34,6 +34,8 @@ describe('JiraActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -74,6 +76,8 @@ describe('JiraActionConnectorFields renders', () => { editActionSecrets={() => {}} readOnly={false} consumer={'case'} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); @@ -104,6 +108,8 @@ describe('JiraActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -125,6 +131,8 @@ describe('JiraActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); @@ -152,6 +160,8 @@ describe('JiraActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx index 86347de528a01..8be15ddaa6bca 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_connectors.test.tsx @@ -33,6 +33,8 @@ describe('PagerDutyActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -61,6 +63,8 @@ describe('PagerDutyActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -86,6 +90,8 @@ describe('PagerDutyActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); @@ -112,6 +118,8 @@ describe('PagerDutyActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index bbd237a7cec89..35891f513be6b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -34,6 +34,8 @@ describe('ResilientActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -74,6 +76,8 @@ describe('ResilientActionConnectorFields renders', () => { editActionSecrets={() => {}} readOnly={false} consumer={'case'} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -105,6 +109,8 @@ describe('ResilientActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -126,6 +132,8 @@ describe('ResilientActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); @@ -153,6 +161,8 @@ describe('ResilientActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts index ba820efc8111f..4b67d256d99bc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.test.ts @@ -6,7 +6,7 @@ */ import { httpServiceMock } from '../../../../../../../../src/core/public/mocks'; -import { getChoices } from './api'; +import { getChoices, getAppInfo } from './api'; const choicesResponse = { status: 'ok', @@ -44,10 +44,27 @@ const choicesResponse = { ], }; +const applicationInfoData = { + result: { name: 'Elastic', scope: 'x_elas2_inc_int', version: '1.0.0' }, +}; + +const applicationInfoResponse = { + ok: true, + status: 200, + json: async () => applicationInfoData, +}; + describe('ServiceNow API', () => { const http = httpServiceMock.createStartContract(); + let fetchMock: jest.SpyInstance>; - beforeEach(() => jest.resetAllMocks()); + beforeAll(() => { + fetchMock = jest.spyOn(window, 'fetch'); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); describe('getChoices', () => { test('should call get choices API', async () => { @@ -67,4 +84,96 @@ describe('ServiceNow API', () => { }); }); }); + + describe('getAppInfo', () => { + test('should call getAppInfo API for ITSM', async () => { + const abortCtrl = new AbortController(); + fetchMock.mockResolvedValueOnce(applicationInfoResponse); + + const res = await getAppInfo({ + signal: abortCtrl.signal, + apiUrl: 'https://example.com', + username: 'test', + password: 'test', + actionTypeId: '.servicenow', + }); + + expect(res).toEqual(applicationInfoData.result); + expect(fetchMock).toHaveBeenCalledWith( + 'https://example.com/api/x_elas2_inc_int/elastic_api/health', + { + signal: abortCtrl.signal, + method: 'GET', + headers: { Authorization: 'Basic dGVzdDp0ZXN0' }, + } + ); + }); + + test('should call getAppInfo API correctly for SIR', async () => { + const abortCtrl = new AbortController(); + fetchMock.mockResolvedValueOnce(applicationInfoResponse); + + const res = await getAppInfo({ + signal: abortCtrl.signal, + apiUrl: 'https://example.com', + username: 'test', + password: 'test', + actionTypeId: '.servicenow-sir', + }); + + expect(res).toEqual(applicationInfoData.result); + expect(fetchMock).toHaveBeenCalledWith( + 'https://example.com/api/x_elas2_sir_int/elastic_api/health', + { + signal: abortCtrl.signal, + method: 'GET', + headers: { Authorization: 'Basic dGVzdDp0ZXN0' }, + } + ); + }); + + it('returns an error when the response fails', async () => { + expect.assertions(1); + + const abortCtrl = new AbortController(); + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 401, + json: async () => applicationInfoResponse.json, + }); + + await expect(() => + getAppInfo({ + signal: abortCtrl.signal, + apiUrl: 'https://example.com', + username: 'test', + password: 'test', + actionTypeId: '.servicenow', + }) + ).rejects.toThrow('Received status:'); + }); + + it('returns an error when parsing the json fails', async () => { + expect.assertions(1); + + const abortCtrl = new AbortController(); + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => { + throw new Error('bad'); + }, + }); + + await expect(() => + getAppInfo({ + signal: abortCtrl.signal, + apiUrl: 'https://example.com', + username: 'test', + password: 'test', + actionTypeId: '.servicenow', + }) + ).rejects.toThrow('bad'); + }); + }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts index 62347580e75ca..32a2d0296d4c9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/api.ts @@ -6,7 +6,11 @@ */ import { HttpSetup } from 'kibana/public'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { snExternalServiceConfig } from '../../../../../../actions/server/builtin_action_types/servicenow/config'; import { BASE_ACTION_API_PATH } from '../../../constants'; +import { API_INFO_ERROR } from './translations'; +import { AppInfo, RESTApiError } from './types'; export async function getChoices({ http, @@ -29,3 +33,43 @@ export async function getChoices({ } ); } + +/** + * The app info url should be the same as at: + * x-pack/plugins/actions/server/builtin_action_types/servicenow/service.ts + */ +const getAppInfoUrl = (url: string, scope: string) => `${url}/api/${scope}/elastic_api/health`; + +export async function getAppInfo({ + signal, + apiUrl, + username, + password, + actionTypeId, +}: { + signal: AbortSignal; + apiUrl: string; + username: string; + password: string; + actionTypeId: string; +}): Promise { + const urlWithoutTrailingSlash = apiUrl.endsWith('/') ? apiUrl.slice(0, -1) : apiUrl; + const config = snExternalServiceConfig[actionTypeId]; + const response = await fetch(getAppInfoUrl(urlWithoutTrailingSlash, config.appScope ?? ''), { + method: 'GET', + signal, + headers: { + Authorization: 'Basic ' + btoa(username + ':' + password), + }, + }); + + if (!response.ok) { + throw new Error(API_INFO_ERROR(response.status)); + } + + const data = await response.json(); + + return { + ...data.result, + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.test.tsx new file mode 100644 index 0000000000000..67c3238b04774 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.test.tsx @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { ApplicationRequiredCallout } from './application_required_callout'; + +describe('ApplicationRequiredCallout', () => { + test('it renders the callout', () => { + render(); + expect(screen.getByText('Elastic ServiceNow App not installed')).toBeInTheDocument(); + expect( + screen.getByText('Please go to the ServiceNow app store and install the application') + ).toBeInTheDocument(); + }); + + test('it renders the ServiceNow store button', () => { + render(); + expect(screen.getByText('Visit ServiceNow app store')).toBeInTheDocument(); + }); + + test('it renders an error message if provided', () => { + render(); + expect(screen.getByText('Error message: Denied')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx new file mode 100644 index 0000000000000..561dae95fe1b7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/application_required_callout.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { SNStoreButton } from './sn_store_button'; + +const content = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout.content', + { + defaultMessage: 'Please go to the ServiceNow app store and install the application', + } +); + +const ERROR_MESSAGE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.applicationRequiredCallout.errorMessage', + { + defaultMessage: 'Error message', + } +); + +interface Props { + message?: string | null; +} + +const ApplicationRequiredCalloutComponent: React.FC = ({ message }) => { + return ( + <> + + +

{content}

+ {message && ( +

+ {ERROR_MESSAGE}: {message} +

+ )} + +
+ + + ); +}; + +export const ApplicationRequiredCallout = memo(ApplicationRequiredCalloutComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts new file mode 100644 index 0000000000000..9d5fafbf5a0ea --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/config.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const UPDATE_INCIDENT_VARIABLE = '{{rule.id}}'; +export const NOT_UPDATE_INCIDENT_VARIABLE = '{{rule.id}}:{{alert.id}}'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx new file mode 100644 index 0000000000000..caee946524265 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/credentials.tsx @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo, useCallback } from 'react'; +import { + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiLink, + EuiFieldText, + EuiSpacer, + EuiTitle, + EuiFieldPassword, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { ActionConnectorFieldsProps } from '../../../../../public/types'; +import { useKibana } from '../../../../common/lib/kibana'; +import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import * as i18n from './translations'; +import { ServiceNowActionConnector } from './types'; +import { isFieldInvalid } from './helpers'; + +interface Props { + action: ActionConnectorFieldsProps['action']; + errors: ActionConnectorFieldsProps['errors']; + readOnly: boolean; + isLoading: boolean; + editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; + editActionConfig: ActionConnectorFieldsProps['editActionConfig']; +} + +const CredentialsComponent: React.FC = ({ + action, + errors, + readOnly, + isLoading, + editActionSecrets, + editActionConfig, +}) => { + const { docLinks } = useKibana().services; + const { apiUrl } = action.config; + const { username, password } = action.secrets; + + const isApiUrlInvalid = isFieldInvalid(apiUrl, errors.apiUrl); + const isUsernameInvalid = isFieldInvalid(username, errors.username); + const isPasswordInvalid = isFieldInvalid(password, errors.password); + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + [editActionConfig] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + [editActionSecrets] + ); + + return ( + <> + + + +

{i18n.SN_INSTANCE_LABEL}

+
+

+ + {i18n.SETUP_DEV_INSTANCE} + + ), + }} + /> +

+
+ + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + disabled={isLoading} + /> + + +
+ + + + +

{i18n.AUTHENTICATION_LABEL}

+
+
+
+ + + + + {getEncryptedFieldNotifyLabel( + !action.id, + 2, + action.isMissingSecrets ?? false, + i18n.REENTER_VALUES_LABEL + )} + + + + + + + + handleOnChangeSecretConfig('username', evt.target.value)} + onBlur={() => { + if (!username) { + editActionSecrets('username', ''); + } + }} + disabled={isLoading} + /> + + + + + + + + handleOnChangeSecretConfig('password', evt.target.value)} + onBlur={() => { + if (!password) { + editActionSecrets('password', ''); + } + }} + disabled={isLoading} + /> + + + + + ); +}; + +export const Credentials = memo(CredentialsComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx new file mode 100644 index 0000000000000..767b38ebcf6ad --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { I18nProvider } from '@kbn/i18n/react'; + +import { DeprecatedCallout } from './deprecated_callout'; + +describe('DeprecatedCallout', () => { + const onMigrate = jest.fn(); + + test('it renders correctly', () => { + render(, { + wrapper: ({ children }) => {children}, + }); + + expect(screen.getByText('Deprecated connector type')).toBeInTheDocument(); + }); + + test('it calls onMigrate when pressing the button', () => { + render(, { + wrapper: ({ children }) => {children}, + }); + + const button = screen.getByRole('button'); + fireEvent.click(button); + + expect(onMigrate).toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx new file mode 100644 index 0000000000000..101d1572a67ad --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/deprecated_callout.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiSpacer, EuiCallOut, EuiButtonEmpty } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; + +interface Props { + onMigrate: () => void; +} + +const DeprecatedCalloutComponent: React.FC = ({ onMigrate }) => { + return ( + <> + + + + {i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.deprecatedCalloutMigrate', + { + defaultMessage: 'update this connector.', + } + )} + + ), + }} + /> + + + + ); +}; + +export const DeprecatedCallout = memo(DeprecatedCalloutComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts new file mode 100644 index 0000000000000..e37d8dd3b4147 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.test.ts @@ -0,0 +1,47 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { isRESTApiError, isFieldInvalid } from './helpers'; + +describe('helpers', () => { + describe('isRESTApiError', () => { + const resError = { error: { message: 'error', detail: 'access denied' }, status: '401' }; + + test('should return true if the error is RESTApiError', async () => { + expect(isRESTApiError(resError)).toBeTruthy(); + }); + + test('should return true if there is failure status', async () => { + // @ts-expect-error + expect(isRESTApiError({ status: 'failure' })).toBeTruthy(); + }); + + test('should return false if there is no error', async () => { + // @ts-expect-error + expect(isRESTApiError({ whatever: 'test' })).toBeFalsy(); + }); + }); + + describe('isFieldInvalid', () => { + test('should return true if the field is invalid', async () => { + expect(isFieldInvalid('description', ['required'])).toBeTruthy(); + }); + + test('should return if false the field is not defined', async () => { + expect(isFieldInvalid(undefined, ['required'])).toBeFalsy(); + }); + + test('should return if false the error is not defined', async () => { + // @ts-expect-error + expect(isFieldInvalid('description', undefined)).toBeFalsy(); + }); + + test('should return if false the error is empty', async () => { + expect(isFieldInvalid('description', [])).toBeFalsy(); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts index 314d224491128..ca557b31c4f4f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/helpers.ts @@ -6,7 +6,38 @@ */ import { EuiSelectOption } from '@elastic/eui'; -import { Choice } from './types'; +import { + ENABLE_NEW_SN_ITSM_CONNECTOR, + ENABLE_NEW_SN_SIR_CONNECTOR, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../actions/server/constants/connectors'; +import { IErrorObject } from '../../../../../public/types'; +import { AppInfo, Choice, RESTApiError, ServiceNowActionConnector } from './types'; export const choicesToEuiOptions = (choices: Choice[]): EuiSelectOption[] => choices.map((choice) => ({ value: choice.value, text: choice.label })); + +export const isRESTApiError = (res: AppInfo | RESTApiError): res is RESTApiError => + (res as RESTApiError).error != null || (res as RESTApiError).status === 'failure'; + +export const isFieldInvalid = ( + field: string | undefined, + error: string | IErrorObject | string[] +): boolean => error !== undefined && error.length > 0 && field !== undefined; + +// TODO: Remove when the applications are certified +export const isLegacyConnector = (connector: ServiceNowActionConnector) => { + if (connector == null) { + return true; + } + + if (!ENABLE_NEW_SN_ITSM_CONNECTOR && connector.actionTypeId === '.servicenow') { + return true; + } + + if (!ENABLE_NEW_SN_SIR_CONNECTOR && connector.actionTypeId === '.servicenow-sir') { + return true; + } + + return connector.config.isLegacy; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx new file mode 100644 index 0000000000000..8e1c1820920c5 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { InstallationCallout } from './installation_callout'; + +describe('DeprecatedCallout', () => { + test('it renders correctly', () => { + render(); + expect( + screen.getByText( + 'To use this connector, you must first install the Elastic App from the ServiceNow App Store' + ) + ).toBeInTheDocument(); + }); + + test('it renders the button', () => { + render(); + expect(screen.getByRole('link')).toBeInTheDocument(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.tsx new file mode 100644 index 0000000000000..064207910568f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/installation_callout.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiSpacer, EuiCallOut } from '@elastic/eui'; + +import * as i18n from './translations'; +import { SNStoreButton } from './sn_store_button'; + +const InstallationCalloutComponent: React.FC = () => { + return ( + <> + + + + + + + ); +}; + +export const InstallationCallout = memo(InstallationCalloutComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx index f1516f880dce4..b40db9c2dabda 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.test.tsx @@ -43,6 +43,7 @@ describe('servicenow connector validation', () => { isPreconfigured: false, config: { apiUrl: 'https://dev94428.service-now.com/', + isLegacy: false, }, } as ServiceNowActionConnector; @@ -50,6 +51,7 @@ describe('servicenow connector validation', () => { config: { errors: { apiUrl: [], + isLegacy: [], }, }, secrets: { @@ -77,6 +79,7 @@ describe('servicenow connector validation', () => { config: { errors: { apiUrl: ['URL is required.'], + isLegacy: [], }, }, secrets: { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx index 24e2a87d42357..bb4a645f10bbc 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow.tsx @@ -27,6 +27,7 @@ const validateConnector = async ( const translations = await import('./translations'); const configErrors = { apiUrl: new Array(), + isLegacy: new Array(), }; const secretsErrors = { username: new Array(), diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 4993c51f350ad..02f3ae47728ab 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -33,6 +33,8 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect( @@ -57,8 +59,7 @@ describe('ServiceNowActionConnectorFields renders', () => { name: 'servicenow', config: { apiUrl: 'https://test/', - incidentConfiguration: { mapping: [] }, - isCaseOwned: true, + isLegacy: false, }, } as ServiceNowActionConnector; const wrapper = mountWithIntl( @@ -69,6 +70,8 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionSecrets={() => {}} readOnly={false} consumer={'case'} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); @@ -91,6 +94,8 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -112,6 +117,8 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); @@ -138,6 +145,8 @@ describe('ServiceNowActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index 29a6bca4b16ab..2cf738c5e0c13 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -5,162 +5,142 @@ * 2.0. */ -import React, { useCallback } from 'react'; - -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, - EuiLink, - EuiTitle, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useCallback, useEffect, useState } from 'react'; + import { ActionConnectorFieldsProps } from '../../../../types'; import * as i18n from './translations'; import { ServiceNowActionConnector } from './types'; import { useKibana } from '../../../../common/lib/kibana'; -import { getEncryptedFieldNotifyLabel } from '../../get_encrypted_field_notify_label'; +import { DeprecatedCallout } from './deprecated_callout'; +import { useGetAppInfo } from './use_get_app_info'; +import { ApplicationRequiredCallout } from './application_required_callout'; +import { isRESTApiError, isLegacyConnector } from './helpers'; +import { InstallationCallout } from './installation_callout'; +import { UpdateConnectorModal } from './update_connector_modal'; +import { updateActionConnector } from '../../../lib/action_connector_api'; +import { Credentials } from './credentials'; const ServiceNowConnectorFields: React.FC> = - ({ action, editActionSecrets, editActionConfig, errors, consumer, readOnly }) => { - const { docLinks } = useKibana().services; + ({ + action, + editActionSecrets, + editActionConfig, + errors, + consumer, + readOnly, + setCallbacks, + isEdit, + }) => { + const { + http, + notifications: { toasts }, + } = useKibana().services; const { apiUrl } = action.config; + const { username, password } = action.secrets; + const isOldConnector = isLegacyConnector(action); - const isApiUrlInvalid: boolean = - errors.apiUrl !== undefined && errors.apiUrl.length > 0 && apiUrl !== undefined; + const [showModal, setShowModal] = useState(false); - const { username, password } = action.secrets; + const { fetchAppInfo, isLoading } = useGetAppInfo({ + actionTypeId: action.actionTypeId, + }); - const isUsernameInvalid: boolean = - errors.username !== undefined && errors.username.length > 0 && username !== undefined; - const isPasswordInvalid: boolean = - errors.password !== undefined && errors.password.length > 0 && password !== undefined; + const [applicationRequired, setApplicationRequired] = useState(false); + const [applicationInfoErrorMsg, setApplicationInfoErrorMsg] = useState(null); - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - [editActionConfig] - ); + const getApplicationInfo = useCallback(async () => { + setApplicationRequired(false); + setApplicationInfoErrorMsg(null); + + try { + const res = await fetchAppInfo(action); + if (isRESTApiError(res)) { + throw new Error(res.error?.message ?? i18n.UNKNOWN); + } + + return res; + } catch (e) { + setApplicationRequired(true); + setApplicationInfoErrorMsg(e.message); + // We need to throw here so the connector will be not be saved. + throw e; + } + }, [action, fetchAppInfo]); - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - [editActionSecrets] + const beforeActionConnectorSave = useCallback(async () => { + if (!isOldConnector) { + await getApplicationInfo(); + } + }, [getApplicationInfo, isOldConnector]); + + useEffect( + () => setCallbacks({ beforeActionConnectorSave }), + [beforeActionConnectorSave, setCallbacks] ); + + const onMigrateClick = useCallback(() => setShowModal(true), []); + const onModalCancel = useCallback(() => setShowModal(false), []); + + const onModalConfirm = useCallback(async () => { + await getApplicationInfo(); + await updateActionConnector({ + http, + connector: { + name: action.name, + config: { apiUrl, isLegacy: false }, + secrets: { username, password }, + }, + id: action.id, + }); + + editActionConfig('isLegacy', false); + setShowModal(false); + + toasts.addSuccess({ + title: i18n.MIGRATION_SUCCESS_TOAST_TITLE(action.name), + text: i18n.MIGRATION_SUCCESS_TOAST_TEXT, + }); + }, [ + getApplicationInfo, + http, + action.name, + action.id, + apiUrl, + username, + password, + editActionConfig, + toasts, + ]); + return ( <> - - - - - - } - > - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={() => { - if (!apiUrl) { - editActionConfig('apiUrl', ''); - } - }} - /> - - - - - - - -

{i18n.AUTHENTICATION_LABEL}

-
-
-
- - - - - {getEncryptedFieldNotifyLabel( - !action.id, - 2, - action.isMissingSecrets ?? false, - i18n.REENTER_VALUES_LABEL - )} - - - - - - - - handleOnChangeSecretConfig('username', evt.target.value)} - onBlur={() => { - if (!username) { - editActionSecrets('username', ''); - } - }} - /> - - - - - - - - handleOnChangeSecretConfig('password', evt.target.value)} - onBlur={() => { - if (!password) { - editActionSecrets('password', ''); - } - }} - /> - - - + {showModal && ( + + )} + {!isOldConnector && } + {isOldConnector && } + + {applicationRequired && !isOldConnector && ( + + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx index e864a8d3fd114..30e09356e95dd 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.test.tsx @@ -31,6 +31,8 @@ const actionParams = { category: 'software', subcategory: 'os', externalId: null, + correlation_id: 'alertID', + correlation_display: 'Alerting', }, comments: [], }, @@ -144,7 +146,10 @@ describe('ServiceNowITSMParamsFields renders', () => { }; mount(); expect(editAction.mock.calls[0][1]).toEqual({ - incident: {}, + incident: { + correlation_display: 'Alerting', + correlation_id: '{{rule.id}}:{{alert.id}}', + }, comments: [], }); }); @@ -166,7 +171,10 @@ describe('ServiceNowITSMParamsFields renders', () => { wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); expect(editAction.mock.calls.length).toEqual(1); expect(editAction.mock.calls[0][1]).toEqual({ - incident: {}, + incident: { + correlation_display: 'Alerting', + correlation_id: '{{rule.id}}:{{alert.id}}', + }, comments: [], }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx index b243afb375e6d..81428cd7f0a73 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_itsm_params.tsx @@ -13,16 +13,18 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, + EuiSwitch, } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; -import { ServiceNowITSMActionParams, Choice, Fields } from './types'; +import { ServiceNowITSMActionParams, Choice, Fields, ServiceNowActionConnector } from './types'; import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; import { useGetChoices } from './use_get_choices'; -import { choicesToEuiOptions } from './helpers'; +import { choicesToEuiOptions, isLegacyConnector } from './helpers'; import * as i18n from './translations'; +import { UPDATE_INCIDENT_VARIABLE, NOT_UPDATE_INCIDENT_VARIABLE } from './config'; const useGetChoicesFields = ['urgency', 'severity', 'impact', 'category', 'subcategory']; const defaultFields: Fields = { @@ -42,6 +44,8 @@ const ServiceNowParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const isOldConnector = isLegacyConnector(actionConnector as unknown as ServiceNowActionConnector); + const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -53,8 +57,13 @@ const ServiceNowParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); + const hasUpdateIncident = + incident.correlation_id != null && incident.correlation_id === UPDATE_INCIDENT_VARIABLE; + const [updateIncident, setUpdateIncident] = useState(hasUpdateIncident); const [choices, setChoices] = useState(defaultFields); + const correlationID = updateIncident ? UPDATE_INCIDENT_VARIABLE : NOT_UPDATE_INCIDENT_VARIABLE; + const editSubActionProperty = useCallback( (key: string, value: any) => { const newProps = @@ -90,6 +99,14 @@ const ServiceNowParamsFields: React.FunctionComponent< ); }, []); + const onUpdateIncidentSwitchChange = useCallback(() => { + const newCorrelationID = !updateIncident + ? UPDATE_INCIDENT_VARIABLE + : NOT_UPDATE_INCIDENT_VARIABLE; + editSubActionProperty('correlation_id', newCorrelationID); + setUpdateIncident(!updateIncident); + }, [editSubActionProperty, updateIncident]); + const categoryOptions = useMemo(() => choicesToEuiOptions(choices.category), [choices.category]); const urgencyOptions = useMemo(() => choicesToEuiOptions(choices.urgency), [choices.urgency]); const severityOptions = useMemo(() => choicesToEuiOptions(choices.severity), [choices.severity]); @@ -119,7 +136,7 @@ const ServiceNowParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: {}, + incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, comments: [], }, index @@ -136,7 +153,7 @@ const ServiceNowParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: {}, + incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, comments: [], }, index @@ -236,25 +253,43 @@ const ServiceNowParamsFields: React.FunctionComponent<
- 0 && - incident.short_description !== undefined - } - label={i18n.SHORT_DESCRIPTION_LABEL} - > - - + + + 0 && + incident.short_description !== undefined + } + label={i18n.SHORT_DESCRIPTION_LABEL} + > + + + + {!isOldConnector && ( + + + + + + )} + + { }; mount(); expect(editAction.mock.calls[0][1]).toEqual({ - incident: {}, + incident: { + correlation_display: 'Alerting', + correlation_id: '{{rule.id}}:{{alert.id}}', + }, comments: [], }); }); @@ -196,7 +201,10 @@ describe('ServiceNowSIRParamsFields renders', () => { wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); expect(editAction.mock.calls.length).toEqual(1); expect(editAction.mock.calls[0][1]).toEqual({ - incident: {}, + incident: { + correlation_display: 'Alerting', + correlation_id: '{{rule.id}}:{{alert.id}}', + }, comments: [], }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx index 0ba52014fa1f9..7b7cfc67d9971 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_sir_params.tsx @@ -13,6 +13,7 @@ import { EuiFlexItem, EuiSpacer, EuiTitle, + EuiSwitch, } from '@elastic/eui'; import { useKibana } from '../../../../common/lib/kibana'; import { ActionParamsProps } from '../../../../types'; @@ -21,8 +22,9 @@ import { TextFieldWithMessageVariables } from '../../text_field_with_message_var import * as i18n from './translations'; import { useGetChoices } from './use_get_choices'; -import { ServiceNowSIRActionParams, Fields, Choice } from './types'; -import { choicesToEuiOptions } from './helpers'; +import { ServiceNowSIRActionParams, Fields, Choice, ServiceNowActionConnector } from './types'; +import { choicesToEuiOptions, isLegacyConnector } from './helpers'; +import { UPDATE_INCIDENT_VARIABLE, NOT_UPDATE_INCIDENT_VARIABLE } from './config'; const useGetChoicesFields = ['category', 'subcategory', 'priority']; const defaultFields: Fields = { @@ -31,6 +33,14 @@ const defaultFields: Fields = { priority: [], }; +const valuesToString = (value: string | string[] | null): string | undefined => { + if (Array.isArray(value)) { + return value.join(','); + } + + return value ?? undefined; +}; + const ServiceNowSIRParamsFields: React.FunctionComponent< ActionParamsProps > = ({ actionConnector, actionParams, editAction, index, errors, messageVariables }) => { @@ -39,6 +49,8 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< notifications: { toasts }, } = useKibana().services; + const isOldConnector = isLegacyConnector(actionConnector as unknown as ServiceNowActionConnector); + const actionConnectorRef = useRef(actionConnector?.id ?? ''); const { incident, comments } = useMemo( () => @@ -50,8 +62,13 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< [actionParams.subActionParams] ); + const hasUpdateIncident = + incident.correlation_id != null && incident.correlation_id === UPDATE_INCIDENT_VARIABLE; + const [updateIncident, setUpdateIncident] = useState(hasUpdateIncident); const [choices, setChoices] = useState(defaultFields); + const correlationID = updateIncident ? UPDATE_INCIDENT_VARIABLE : NOT_UPDATE_INCIDENT_VARIABLE; + const editSubActionProperty = useCallback( (key: string, value: any) => { const newProps = @@ -87,6 +104,14 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< ); }, []); + const onUpdateIncidentSwitchChange = useCallback(() => { + const newCorrelationID = !updateIncident + ? UPDATE_INCIDENT_VARIABLE + : NOT_UPDATE_INCIDENT_VARIABLE; + editSubActionProperty('correlation_id', newCorrelationID); + setUpdateIncident(!updateIncident); + }, [editSubActionProperty, updateIncident]); + const { isLoading: isLoadingChoices } = useGetChoices({ http, toastNotifications: toasts, @@ -115,7 +140,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: {}, + incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, comments: [], }, index @@ -132,7 +157,7 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< editAction( 'subActionParams', { - incident: {}, + incident: { correlation_id: correlationID, correlation_display: 'Alerting' }, comments: [], }, index @@ -162,48 +187,48 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< editAction={editSubActionProperty} messageVariables={messageVariables} paramsProperty={'short_description'} - inputTargetValue={incident?.short_description ?? undefined} + inputTargetValue={incident?.short_description} errors={errors['subActionParams.incident.short_description'] as string[]} /> - + - + - + - + @@ -277,6 +302,18 @@ const ServiceNowSIRParamsFields: React.FunctionComponent< inputTargetValue={comments && comments.length > 0 ? comments[0].comment : undefined} label={i18n.COMMENTS_LABEL} /> + + {!isOldConnector && ( + + + + )} ); }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx new file mode 100644 index 0000000000000..fe73653234170 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.test.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { SNStoreButton } from './sn_store_button'; + +describe('SNStoreButton', () => { + test('it renders the button', () => { + render(); + expect(screen.getByText('Visit ServiceNow app store')).toBeInTheDocument(); + }); + + test('it renders a danger button', () => { + render(); + expect(screen.getByRole('link')).toHaveClass('euiButton--danger'); + }); + + test('it renders with correct href', () => { + render(); + expect(screen.getByRole('link')).toHaveAttribute('href', 'https://store.servicenow.com/'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx new file mode 100644 index 0000000000000..5921f679d3f50 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/sn_store_button.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { memo } from 'react'; +import { EuiButtonProps, EuiButton } from '@elastic/eui'; + +import * as i18n from './translations'; + +const STORE_URL = 'https://store.servicenow.com/'; + +interface Props { + color: EuiButtonProps['color']; +} + +const SNStoreButtonComponent: React.FC = ({ color }) => { + return ( + + {i18n.VISIT_SN_STORE} + + ); +}; + +export const SNStoreButton = memo(SNStoreButtonComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts index ea646b896f5e9..90292a35a88df 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/translations.ts @@ -10,7 +10,14 @@ import { i18n } from '@kbn/i18n'; export const API_URL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlTextFieldLabel', { - defaultMessage: 'URL', + defaultMessage: 'ServiceNow instance URL', + } +); + +export const API_URL_HELPTEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiUrlHelpText', + { + defaultMessage: 'Include the full URL', } ); @@ -53,7 +60,7 @@ export const REMEMBER_VALUES_LABEL = i18n.translate( export const REENTER_VALUES_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.reenterValuesLabel', { - defaultMessage: 'Username and password are encrypted. Please reenter values for these fields.', + defaultMessage: 'You will need to re-authenticate each time you edit the connector', } ); @@ -95,14 +102,28 @@ export const TITLE_REQUIRED = i18n.translate( export const SOURCE_IP_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPTitle', { - defaultMessage: 'Source IP', + defaultMessage: 'Source IPs', + } +); + +export const SOURCE_IP_HELP_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.sourceIPHelpText', + { + defaultMessage: 'List of source IPs (comma, or pipe delimited)', } ); export const DEST_IP_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destinationIPTitle', { - defaultMessage: 'Destination IP', + defaultMessage: 'Destination IPs', + } +); + +export const DEST_IP_HELP_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.destIPHelpText', + { + defaultMessage: 'List of destination IPs (comma, or pipe delimited)', } ); @@ -137,14 +158,28 @@ export const COMMENTS_LABEL = i18n.translate( export const MALWARE_URL_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLTitle', { - defaultMessage: 'Malware URL', + defaultMessage: 'Malware URLs', + } +); + +export const MALWARE_URL_HELP_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareURLHelpText', + { + defaultMessage: 'List of malware URLs (comma, or pipe delimited)', } ); export const MALWARE_HASH_LABEL = i18n.translate( 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashTitle', { - defaultMessage: 'Malware Hash', + defaultMessage: 'Malware Hashes', + } +); + +export const MALWARE_HASH_HELP_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.malwareHashHelpText', + { + defaultMessage: 'List of malware hashes (comma, or pipe delimited)', } ); @@ -196,3 +231,91 @@ export const PRIORITY_LABEL = i18n.translate( defaultMessage: 'Priority', } ); + +export const API_INFO_ERROR = (status: number) => + i18n.translate('xpack.triggersActionsUI.components.builtinActionTypes.servicenow.apiInfoError', { + values: { status }, + defaultMessage: 'Received status: {status} when attempting to get application information', + }); + +export const INSTALL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.install', + { + defaultMessage: 'install', + } +); + +export const INSTALLATION_CALLOUT_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutTitle', + { + defaultMessage: + 'To use this connector, you must first install the Elastic App from the ServiceNow App Store', + } +); + +export const MIGRATION_SUCCESS_TOAST_TITLE = (connectorName: string) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.migrationSuccessToastTitle', + { + defaultMessage: 'Migrated connector {connectorName}', + values: { + connectorName, + }, + } + ); + +export const MIGRATION_SUCCESS_TOAST_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.installationCalloutText', + { + defaultMessage: 'Connector has been successfully migrated.', + } +); + +export const VISIT_SN_STORE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.visitSNStore', + { + defaultMessage: 'Visit ServiceNow app store', + } +); + +export const SETUP_DEV_INSTANCE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.setupDevInstance', + { + defaultMessage: 'setup a developer instance', + } +); + +export const SN_INSTANCE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.snInstanceLabel', + { + defaultMessage: 'ServiceNow instance', + } +); + +export const UNKNOWN = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.unknown', + { + defaultMessage: 'UNKNOWN', + } +); + +export const UPDATE_INCIDENT_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentCheckboxLabel', + { + defaultMessage: 'Update incident', + } +); + +export const ON = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentOn', + { + defaultMessage: 'On', + } +); + +export const OFF = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.servicenow.updateIncidentOff', + { + defaultMessage: 'Off', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts index f252f4648e670..b24883359dde5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/types.ts @@ -29,6 +29,7 @@ export interface ServiceNowSIRActionParams { export interface ServiceNowConfig { apiUrl: string; + isLegacy: boolean; } export interface ServiceNowSecrets { @@ -44,3 +45,17 @@ export interface Choice { } export type Fields = Record; +export interface AppInfo { + id: string; + name: string; + scope: string; + version: string; +} + +export interface RESTApiError { + error: { + message: string; + detail: string; + }; + status: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx new file mode 100644 index 0000000000000..b9d660f16dff7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/update_connector_modal.tsx @@ -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 React, { memo } from 'react'; +import { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiModal, + EuiModalBody, + EuiModalFooter, + EuiModalHeader, + EuiModalHeaderTitle, + EuiCallOut, + EuiTextColor, + EuiHorizontalRule, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { ActionConnectorFieldsProps } from '../../../../../public/types'; +import { ServiceNowActionConnector } from './types'; +import { Credentials } from './credentials'; +import { isFieldInvalid } from './helpers'; +import { ApplicationRequiredCallout } from './application_required_callout'; + +const title = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.confirmationModalTitle', + { + defaultMessage: 'Update ServiceNow connector', + } +); + +const cancelButtonText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.cancelButtonText', + { + defaultMessage: 'Cancel', + } +); + +const confirmButtonText = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.confirmButtonText', + { + defaultMessage: 'Update', + } +); + +const calloutTitle = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.modalCalloutTitle', + { + defaultMessage: + 'The Elastic App from the ServiceNow App Store must be installed prior to running the update.', + } +); + +const warningMessage = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.modalWarningMessage', + { + defaultMessage: 'This will update all instances of this connector. This can not be reversed.', + } +); + +interface Props { + action: ActionConnectorFieldsProps['action']; + applicationInfoErrorMsg: string | null; + errors: ActionConnectorFieldsProps['errors']; + isLoading: boolean; + readOnly: boolean; + editActionSecrets: ActionConnectorFieldsProps['editActionSecrets']; + editActionConfig: ActionConnectorFieldsProps['editActionConfig']; + onCancel: () => void; + onConfirm: () => void; +} + +const UpdateConnectorModalComponent: React.FC = ({ + action, + applicationInfoErrorMsg, + errors, + isLoading, + readOnly, + editActionSecrets, + editActionConfig, + onCancel, + onConfirm, +}) => { + const { apiUrl } = action.config; + const { username, password } = action.secrets; + + const hasErrorsOrEmptyFields = + apiUrl === undefined || + username === undefined || + password === undefined || + isFieldInvalid(apiUrl, errors.apiUrl) || + isFieldInvalid(username, errors.username) || + isFieldInvalid(password, errors.password); + + return ( + + + +

{title}

+
+
+ + + + + + + + + + + {warningMessage} + + + + + {applicationInfoErrorMsg && ( + + )} + + + + + {cancelButtonText} + + {confirmButtonText} + + +
+ ); +}; + +export const UpdateConnectorModal = memo(UpdateConnectorModalComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx new file mode 100644 index 0000000000000..c6b70443ec8fb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.test.tsx @@ -0,0 +1,95 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useGetAppInfo, UseGetAppInfo, UseGetAppInfoProps } from './use_get_app_info'; +import { getAppInfo } from './api'; +import { ServiceNowActionConnector } from './types'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const getAppInfoMock = getAppInfo as jest.Mock; + +const actionTypeId = '.servicenow'; +const applicationInfoData = { + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', +}; + +const actionConnector = { + secrets: { + username: 'user', + password: 'pass', + }, + id: 'test', + actionTypeId: '.servicenow', + name: 'ServiceNow ITSM', + isPreconfigured: false, + config: { + apiUrl: 'https://test.service-now.com/', + isLegacy: false, + }, +} as ServiceNowActionConnector; + +describe('useGetAppInfo', () => { + getAppInfoMock.mockResolvedValue(applicationInfoData); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('init', async () => { + const { result } = renderHook(() => + useGetAppInfo({ + actionTypeId, + }) + ); + + expect(result.current).toEqual({ + isLoading: false, + fetchAppInfo: result.current.fetchAppInfo, + }); + }); + + it('returns the application information', async () => { + const { result } = renderHook(() => + useGetAppInfo({ + actionTypeId, + }) + ); + + let res; + + await act(async () => { + res = await result.current.fetchAppInfo(actionConnector); + }); + + expect(res).toEqual(applicationInfoData); + }); + + it('it throws an error when api fails', async () => { + expect.assertions(1); + getAppInfoMock.mockImplementation(() => { + throw new Error('An error occurred'); + }); + + const { result } = renderHook(() => + useGetAppInfo({ + actionTypeId, + }) + ); + + await expect(() => + act(async () => { + await result.current.fetchAppInfo(actionConnector); + }) + ).rejects.toThrow('An error occurred'); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx new file mode 100644 index 0000000000000..a211c8dda66b7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/use_get_app_info.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useState, useEffect, useRef, useCallback } from 'react'; +import { getAppInfo } from './api'; +import { AppInfo, RESTApiError, ServiceNowActionConnector } from './types'; + +export interface UseGetAppInfoProps { + actionTypeId: string; +} + +export interface UseGetAppInfo { + fetchAppInfo: (connector: ServiceNowActionConnector) => Promise; + isLoading: boolean; +} + +export const useGetAppInfo = ({ actionTypeId }: UseGetAppInfoProps): UseGetAppInfo => { + const [isLoading, setIsLoading] = useState(false); + const didCancel = useRef(false); + const abortCtrl = useRef(new AbortController()); + + const fetchAppInfo = useCallback( + async (connector) => { + try { + didCancel.current = false; + abortCtrl.current.abort(); + abortCtrl.current = new AbortController(); + setIsLoading(true); + + const res = await getAppInfo({ + signal: abortCtrl.current.signal, + apiUrl: connector.config.apiUrl, + username: connector.secrets.username, + password: connector.secrets.password, + actionTypeId, + }); + + if (!didCancel.current) { + setIsLoading(false); + } + + return res; + } catch (error) { + if (!didCancel.current) { + setIsLoading(false); + } + throw error; + } + }, + [actionTypeId] + ); + + useEffect(() => { + return () => { + didCancel.current = true; + abortCtrl.current.abort(); + setIsLoading(false); + }; + }, []); + + return { + fetchAppInfo, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx index 547346054011b..0a37165bd7f5f 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/slack/slack_connectors.test.tsx @@ -30,6 +30,8 @@ describe('SlackActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -56,6 +58,8 @@ describe('SlackActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -76,6 +80,8 @@ describe('SlackActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); @@ -98,6 +104,8 @@ describe('SlackActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts index 90bab65b83bfd..00262c3265d7a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -39,29 +39,28 @@ describe('Swimlane API', () => { }); it('returns an error when the response fails', async () => { + expect.assertions(1); const abortCtrl = new AbortController(); - fetchMock.mockResolvedValueOnce({ ok: false, status: 401, json: async () => getApplicationResponse, }); - try { - await getApplication({ + await expect(() => + getApplication({ signal: abortCtrl.signal, apiToken: '', appId: '', url: '', - }); - } catch (e) { - expect(e.message).toContain('Received status:'); - } + }) + ).rejects.toThrow('Received status:'); }); it('returns an error when parsing the json fails', async () => { - const abortCtrl = new AbortController(); + expect.assertions(1); + const abortCtrl = new AbortController(); fetchMock.mockResolvedValueOnce({ ok: true, status: 200, @@ -70,16 +69,14 @@ describe('Swimlane API', () => { }, }); - try { - await getApplication({ + await expect(() => + getApplication({ signal: abortCtrl.signal, apiToken: '', appId: '', url: '', - }); - } catch (e) { - expect(e.message).toContain('bad'); - } + }) + ).rejects.toThrow('bad'); }); it('it removes unsafe fields', async () => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx index 6740179d786f2..4829156380e94 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -50,6 +50,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -77,6 +79,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -106,6 +110,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); @@ -139,6 +145,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -184,6 +192,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -229,6 +239,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -285,6 +297,8 @@ describe('SwimlaneActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx index 11c747125595d..5031b32281258 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/teams/teams_connectors.test.tsx @@ -30,6 +30,8 @@ describe('TeamsActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); @@ -56,6 +58,8 @@ describe('TeamsActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -79,6 +83,8 @@ describe('TeamsActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); @@ -103,6 +109,8 @@ describe('TeamsActionFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx index c041b4e3e1e42..ea40c1ddfb139 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/webhook/webhook_connectors.test.tsx @@ -35,6 +35,8 @@ describe('WebhookActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="webhookViewHeadersSwitch"]').length > 0).toBeTruthy(); @@ -62,6 +64,8 @@ describe('WebhookActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); @@ -92,6 +96,8 @@ describe('WebhookActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); @@ -123,6 +129,8 @@ describe('WebhookActionConnectorFields renders', () => { editActionConfig={() => {}} editActionSecrets={() => {}} readOnly={false} + setCallbacks={() => {}} + isEdit={false} /> ); expect(wrapper.find('[data-test-subj="missingSecretsMessage"]').length).toBeGreaterThan(0); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx index 091ea1e305e35..5a4d682ff573b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.test.tsx @@ -49,6 +49,8 @@ describe('action_connector_form', () => { dispatch={() => {}} errors={{ name: [] }} actionTypeRegistry={actionTypeRegistry} + setCallbacks={() => {}} + isEdit={false} /> ); const connectorNameField = wrapper?.find('[data-test-subj="nameInput"]'); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx index f61a0f8f52904..5ee294b6dbd52 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_connector_form.tsx @@ -24,6 +24,7 @@ import { ActionTypeRegistryContract, UserConfiguredActionConnector, ActionTypeModel, + ActionConnectorFieldsSetCallbacks, } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { useKibana } from '../../../common/lib/kibana'; @@ -89,6 +90,8 @@ interface ActionConnectorProps< serverError?: { body: { message: string; error: string }; }; + setCallbacks: ActionConnectorFieldsSetCallbacks; + isEdit: boolean; } export const ActionConnectorForm = ({ @@ -99,6 +102,8 @@ export const ActionConnectorForm = ({ errors, actionTypeRegistry, consumer, + setCallbacks, + isEdit, }: ActionConnectorProps) => { const { docLinks, @@ -237,6 +242,8 @@ export const ActionConnectorForm = ({ editActionConfig={setActionConfigProperty} editActionSecrets={setActionSecretsProperty} consumer={consumer} + setCallbacks={setCallbacks} + isEdit={isEdit} /> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx index 4dcf501fa0023..eda0b99e859a6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/action_form.tsx @@ -34,11 +34,7 @@ import { ActionTypeForm } from './action_type_form'; import { AddConnectorInline } from './connector_add_inline'; import { actionTypeCompare } from '../../lib/action_type_compare'; import { checkActionFormActionTypeEnabled } from '../../lib/check_action_type_enabled'; -import { - VIEW_LICENSE_OPTIONS_LINK, - DEFAULT_HIDDEN_ACTION_TYPES, - DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES, -} from '../../../common/constants'; +import { VIEW_LICENSE_OPTIONS_LINK, DEFAULT_HIDDEN_ACTION_TYPES } from '../../../common/constants'; import { ActionGroup, AlertActionParam } from '../../../../../alerting/common'; import { useKibana } from '../../../common/lib/kibana'; import { DefaultActionParamsGetter } from '../../lib/get_defaults_for_action_params'; @@ -237,15 +233,9 @@ export const ActionForm = ({ .list() /** * TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. - * TODO: Need to decide about ServiceNow SIR connector. * If actionTypes are set, hidden connectors are filtered out. Otherwise, they are not. */ - .filter( - ({ id }) => - actionTypes ?? - (!DEFAULT_HIDDEN_ACTION_TYPES.includes(id) && - !DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES.includes(id)) - ) + .filter(({ id }) => actionTypes ?? !DEFAULT_HIDDEN_ACTION_TYPES.includes(id)) .filter((item) => actionTypesIndex[item.id]) .filter((item) => !!item.actionParamsFields) .sort((a, b) => diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx index 1a3a186d891cc..16466fc9a210d 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_flyout.tsx @@ -33,6 +33,7 @@ import { IErrorObject, ConnectorAddFlyoutProps, ActionTypeModel, + ActionConnectorFieldsCallbacks, } from '../../../types'; import { hasSaveActionsCapability } from '../../lib/capabilities'; import { createActionConnector } from '../../lib/action_connector_api'; @@ -121,6 +122,7 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ }; const [isSaving, setIsSaving] = useState(false); + const [callbacks, setCallbacks] = useState(null); const closeFlyout = useCallback(() => { onClose(); @@ -155,6 +157,8 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ errors={errors.connectorErrors} actionTypeRegistry={actionTypeRegistry} consumer={consumer} + setCallbacks={setCallbacks} + isEdit={false} /> ); @@ -199,10 +203,21 @@ const ConnectorAddFlyout: React.FunctionComponent = ({ ); return; } + setIsSaving(true); + // Do not allow to save the connector if there is an error + try { + await callbacks?.beforeActionConnectorSave?.(); + } catch (e) { + setIsSaving(false); + return; + } + const savedAction = await onActionConnectorSave(); + setIsSaving(false); if (savedAction) { + await callbacks?.afterActionConnectorSave?.(savedAction); closeFlyout(); if (reloadConnectors) { await reloadConnectors(); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx index 1e9669d1995dd..7fd6931c936f5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_add_modal.tsx @@ -33,6 +33,7 @@ import { ActionTypeRegistryContract, UserConfiguredActionConnector, IErrorObject, + ActionConnectorFieldsCallbacks, } from '../../../types'; import { useKibana } from '../../../common/lib/kibana'; import { getConnectorWithInvalidatedFields } from '../../lib/value_validators'; @@ -97,6 +98,7 @@ const ConnectorAddModal = ({ secretsErrors: {}, }); + const [callbacks, setCallbacks] = useState(null); const actionTypeModel = actionTypeRegistry.get(actionType.id); useEffect(() => { @@ -189,6 +191,8 @@ const ConnectorAddModal = ({ errors={errors.connectorErrors} actionTypeRegistry={actionTypeRegistry} consumer={consumer} + setCallbacks={setCallbacks} + isEdit={false} /> {isLoading ? ( <> @@ -230,9 +234,19 @@ const ConnectorAddModal = ({ return; } setIsSaving(true); + // Do not allow to save the connector if there is an error + try { + await callbacks?.beforeActionConnectorSave?.(); + } catch (e) { + setIsSaving(false); + return; + } + const savedAction = await onActionConnectorSave(); + setIsSaving(false); if (savedAction) { + await callbacks?.afterActionConnectorSave?.(savedAction); if (postSaveEventHandler) { postSaveEventHandler(savedAction); } diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx index 25c8103f0c8dc..206ae0bf5018b 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/action_connector_form/connector_edit_flyout.tsx @@ -35,6 +35,7 @@ import { IErrorObject, EditConectorTabs, UserConfiguredActionConnector, + ActionConnectorFieldsCallbacks, } from '../../../types'; import { ConnectorReducer, createConnectorReducer } from './connector_reducer'; import { updateActionConnector, executeAction } from '../../lib/action_connector_api'; @@ -138,6 +139,8 @@ const ConnectorEditFlyout = ({ [testExecutionResult] ); + const [callbacks, setCallbacks] = useState(null); + const closeFlyout = useCallback(() => { setConnector(getConnectorWithoutSecrets()); setHasChanges(false); @@ -236,23 +239,38 @@ const ConnectorEditFlyout = ({ }); }; + const setConnectorWithErrors = () => + setConnector( + getConnectorWithInvalidatedFields( + connector, + errors.configErrors, + errors.secretsErrors, + errors.connectorBaseErrors + ) + ); + const onSaveClicked = async (closeAfterSave: boolean = true) => { if (hasErrors) { - setConnector( - getConnectorWithInvalidatedFields( - connector, - errors.configErrors, - errors.secretsErrors, - errors.connectorBaseErrors - ) - ); + setConnectorWithErrors(); return; } + setIsSaving(true); + + // Do not allow to save the connector if there is an error + try { + await callbacks?.beforeActionConnectorSave?.(); + } catch (e) { + setIsSaving(false); + return; + } + const savedAction = await onActionConnectorSave(); setIsSaving(false); + if (savedAction) { setHasChanges(false); + await callbacks?.afterActionConnectorSave?.(savedAction); if (closeAfterSave) { closeFlyout(); } @@ -313,6 +331,8 @@ const ConnectorEditFlyout = ({ }} actionTypeRegistry={actionTypeRegistry} consumer={consumer} + setCallbacks={setCallbacks} + isEdit={true} /> {isLoading ? ( <> diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx index c237bbda48658..04f2334f8e8fa 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/actions_connectors_list/components/actions_connectors_list.tsx @@ -38,6 +38,7 @@ import { ActionConnectorTableItem, ActionTypeIndex, EditConectorTabs, + UserConfiguredActionConnector, } from '../../../../types'; import { EmptyConnectorsPrompt } from '../../../components/prompts/empty_connectors_prompt'; import { useKibana } from '../../../../common/lib/kibana'; @@ -45,6 +46,11 @@ import { DEFAULT_HIDDEN_ACTION_TYPES } from '../../../../'; import { CenterJustifiedSpinner } from '../../../components/center_justified_spinner'; import ConnectorEditFlyout from '../../action_connector_form/connector_edit_flyout'; import ConnectorAddFlyout from '../../action_connector_form/connector_add_flyout'; +import { + ENABLE_NEW_SN_ITSM_CONNECTOR, + ENABLE_NEW_SN_SIR_CONNECTOR, + // eslint-disable-next-line @kbn/eslint/no-restricted-paths +} from '../../../../../../actions/server/constants/connectors'; const ActionsConnectorsList: React.FunctionComponent = () => { const { @@ -167,6 +173,14 @@ const ActionsConnectorsList: React.FunctionComponent = () => { const checkEnabledResult = checkActionTypeEnabled( actionTypesIndex && actionTypesIndex[item.actionTypeId] ); + const itemConfig = ( + item as UserConfiguredActionConnector, Record> + ).config; + const showLegacyTooltip = + itemConfig?.isLegacy && + // TODO: Remove when applications are certified + ((ENABLE_NEW_SN_ITSM_CONNECTOR && item.actionTypeId === '.servicenow') || + (ENABLE_NEW_SN_SIR_CONNECTOR && item.actionTypeId === '.servicenow-sir')); const link = ( <> @@ -190,6 +204,23 @@ const ActionsConnectorsList: React.FunctionComponent = () => { position="right" /> ) : null} + {showLegacyTooltip && ( + + )} ); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts index c2523dd59821d..9e490945e2261 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/constants/index.ts @@ -12,5 +12,3 @@ export { builtInGroupByTypes } from './group_by_types'; export const VIEW_LICENSE_OPTIONS_LINK = 'https://www.elastic.co/subscriptions'; // TODO: Remove when cases connector is available across Kibana. Issue: https://github.com/elastic/kibana/issues/82502. export const DEFAULT_HIDDEN_ACTION_TYPES = ['.case']; -// Action types included in this array will be hidden only from the alert's action type node list -export const DEFAULT_HIDDEN_ONLY_ON_ALERTS_ACTION_TYPES = ['.servicenow-sir']; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index a78d1d52de0bd..8085f9245f4e9 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -73,6 +73,14 @@ export type ActionTypeRegistryContract< > = PublicMethodsOf>>; export type RuleTypeRegistryContract = PublicMethodsOf>; +export type ActionConnectorFieldsCallbacks = { + beforeActionConnectorSave?: () => Promise; + afterActionConnectorSave?: (connector: ActionConnector) => Promise; +} | null; +export type ActionConnectorFieldsSetCallbacks = React.Dispatch< + React.SetStateAction +>; + export interface ActionConnectorFieldsProps { action: TActionConnector; editActionConfig: (property: string, value: unknown) => void; @@ -80,6 +88,8 @@ export interface ActionConnectorFieldsProps { errors: IErrorObject; readOnly: boolean; consumer?: string; + setCallbacks: ActionConnectorFieldsSetCallbacks; + isEdit: boolean; } export enum AlertFlyoutCloseReason { diff --git a/x-pack/plugins/uptime/public/state/api/alert_actions.ts b/x-pack/plugins/uptime/public/state/api/alert_actions.ts index b0f5f3ea490e5..40a7af18ac906 100644 --- a/x-pack/plugins/uptime/public/state/api/alert_actions.ts +++ b/x-pack/plugins/uptime/public/state/api/alert_actions.ts @@ -189,6 +189,8 @@ function getServiceNowActionParams(): ServiceNowActionParams { category: null, subcategory: null, externalId: null, + correlation_id: null, + correlation_display: null, }, comments: [], }, diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 87eb866b14fa5..0618d379dc77d 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -34,6 +34,7 @@ const enabledActionTypes = [ '.swimlane', '.server-log', '.servicenow', + '.servicenow-sir', '.jira', '.resilient', '.slack', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 997f36020af8c..ecfd8ef3b8e52 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -42,14 +42,6 @@ export function getAllExternalServiceSimulatorPaths(): string[] { const allPaths = Object.values(ExternalServiceSimulator).map((service) => getExternalServiceSimulatorPath(service) ); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); - allPaths.push( - `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident/123` - ); - allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_choice`); - allPaths.push( - `/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/sys_dictionary` - ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); @@ -76,6 +68,10 @@ export async function getSwimlaneServer(): Promise { return await initSwimlane(); } +export async function getServiceNowServer(): Promise { + return await initServiceNow(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; @@ -127,7 +123,6 @@ export class FixturePlugin implements Plugin, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' }, - }); - } - ); +import http from 'http'; - router.patch( - { - path: `${path}/api/now/v2/table/incident/{id}`, - options: { - authRequired: false, - }, - validate: {}, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - result: { sys_id: '123', number: 'INC01', sys_updated_on: '2020-03-10 12:24:20' }, - }); +export const initPlugin = async () => http.createServer(handler); + +const sendResponse = (response: http.ServerResponse, data: any) => { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(data, null, 4)); +}; + +const handler = async (request: http.IncomingMessage, response: http.ServerResponse) => { + const buffers = []; + let data: Record = {}; + + if (request.method === 'POST') { + for await (const chunk of request) { + buffers.push(chunk); } - ); - router.get( - { - path: `${path}/api/now/v2/table/incident/{id}`, - options: { - authRequired: false, + data = JSON.parse(Buffer.concat(buffers).toString()); + } + + const pathName = request.url!; + + if (pathName.includes('elastic_api/health')) { + return sendResponse(response, { + result: { + name: 'Elastic', + scope: 'x_elas2_inc_int', + version: '1.0.0', }, - validate: {}, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - result: { + }); + } + + // Import Set API: Create or update incident + if ( + pathName.includes('x_elas2_inc_int_elastic_incident') || + pathName.includes('x_elas2_sir_int_elastic_si_incident') + ) { + const update = data?.elastic_incident_id != null; + return sendResponse(response, { + import_set: 'ISET01', + staging_table: 'x_elas2_inc_int_elastic_incident', + result: [ + { + transform_map: 'Elastic Incident', + table: 'incident', + display_name: 'number', + display_value: 'INC01', + record_link: '/api/now/table/incident/1', + status: update ? 'updated' : 'inserted', sys_id: '123', - number: 'INC01', - sys_created_on: '2020-03-10 12:24:20', - short_description: 'title', - description: 'description', }, - }); - } - ); + ], + }); + } - router.get( - { - path: `${path}/api/now/v2/table/sys_dictionary`, - options: { - authRequired: false, + // Create incident + if ( + pathName === '/api/now/v2/table/incident' || + pathName === '/api/now/v2/table/sn_si_incident' + ) { + return sendResponse(response, { + result: { sys_id: '123', number: 'INC01', sys_created_on: '2020-03-10 12:24:20' }, + }); + } + + // URLs of type /api/now/v2/table/incident/{id} + // GET incident, PATCH incident + if ( + pathName.includes('/api/now/v2/table/incident') || + pathName.includes('/api/now/v2/table/sn_si_incident') + ) { + return sendResponse(response, { + result: { + sys_id: '123', + number: 'INC01', + sys_created_on: '2020-03-10 12:24:20', + sys_updated_on: '2020-03-10 12:24:20', }, - validate: {}, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - result: [ - { - column_label: 'Close notes', - mandatory: 'false', - max_length: '4000', - element: 'close_notes', - }, - { - column_label: 'Description', - mandatory: 'false', - max_length: '4000', - element: 'description', - }, - { - column_label: 'Short description', - mandatory: 'false', - max_length: '160', - element: 'short_description', - }, - ], - }); - } - ); + }); + } - router.get( - { - path: `${path}/api/now/v2/table/sys_choice`, - options: { - authRequired: false, + // Add multiple observables + if (pathName.includes('/observables/bulk')) { + return sendResponse(response, { + result: [ + { + value: '5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9', + observable_sys_id: '1', + }, + { + value: '127.0.0.1', + observable_sys_id: '2', + }, + { + value: 'https://example.com', + observable_sys_id: '3', + }, + ], + }); + } + + // Add single observables + if (pathName.includes('/observables')) { + return sendResponse(response, { + result: { + value: '127.0.0.1', + observable_sys_id: '2', }, - validate: {}, - }, - async function ( - context: RequestHandlerContext, - req: KibanaRequest, - res: KibanaResponseFactory - ): Promise> { - return jsonResponse(res, 200, { - result: [ - { - dependent_value: '', - label: '1 - Critical', - value: '1', - }, - { - dependent_value: '', - label: '2 - High', - value: '2', - }, - { - dependent_value: '', - label: '3 - Moderate', - value: '3', - }, - { - dependent_value: '', - label: '4 - Low', - value: '4', - }, - { - dependent_value: '', - label: '5 - Planning', - value: '5', - }, - ], - }); - } - ); -} - -function jsonResponse(res: KibanaResponseFactory, code: number, object?: Record) { - if (object == null) { - return res.custom({ - statusCode: code, - body: '', }); } - return res.custom>({ body: object, statusCode: code }); -} + if (pathName.includes('/api/now/table/sys_dictionary')) { + return sendResponse(response, { + result: [ + { + column_label: 'Close notes', + mandatory: 'false', + max_length: '4000', + element: 'close_notes', + }, + { + column_label: 'Description', + mandatory: 'false', + max_length: '4000', + element: 'description', + }, + { + column_label: 'Short description', + mandatory: 'false', + max_length: '160', + element: 'short_description', + }, + ], + }); + } + + if (pathName.includes('/api/now/table/sys_choice')) { + return sendResponse(response, { + result: [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + }, + { + dependent_value: '', + label: '5 - Planning', + value: '5', + }, + ], + }); + } + + // Return an 400 error if endpoint is not supported + response.statusCode = 400; + response.setHeader('Content-Type', 'application/json'); + response.end('Not supported endpoint to request servicenow simulator'); +}; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts index afba550908ddc..97cbcbe7a60a6 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts @@ -35,5 +35,5 @@ const handler = (request: http.IncomingMessage, response: http.ServerResponse) = // Return an 400 error if http method is not supported response.statusCode = 400; response.setHeader('Content-Type', 'application/json'); - response.end('Not supported http method to request slack simulator'); + response.end('Not supported http method to request swimlane simulator'); }; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts similarity index 76% rename from x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts rename to x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts index d6196ee6ce312..fe1ebdf8d28a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -7,24 +7,22 @@ import httpProxy from 'http-proxy'; import expect from '@kbn/expect'; +import getPort from 'get-port'; +import http from 'http'; import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; - -import { - getExternalServiceSimulatorPath, - ExternalServiceSimulator, -} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; +import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export -export default function servicenowTest({ getService }: FtrProviderContext) { +export default function serviceNowITSMTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); const configService = getService('config'); const mockServiceNow = { config: { apiUrl: 'www.servicenowisinkibanaactions.com', + isLegacy: false, }, secrets: { password: 'elastic', @@ -41,7 +39,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { short_description: 'a title', urgency: '1', category: 'software', - subcategory: 'software', + subcategory: 'os', }, comments: [ { @@ -53,16 +51,37 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }, }; - let servicenowSimulatorURL: string = ''; + describe('ServiceNow ITSM', () => { + let simulatedActionId = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; - describe('ServiceNow', () => { - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) + before(async () => { + serviceNowServer = await getServiceNowServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!serviceNowServer.listening) { + serviceNowServer.listen(availablePort); + } + serviceNowSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + serviceNowSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } ); }); - describe('ServiceNow - Action Creation', () => { + after(() => { + serviceNowServer.close(); + if (proxyServer) { + proxyServer.close(); + } + }); + + describe('ServiceNow ITSM - Action Creation', () => { it('should return 200 when creating a servicenow action successfully', async () => { const { body: createdAction } = await supertest .post('/api/actions/connector') @@ -71,7 +90,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { name: 'A servicenow action', connector_type_id: '.servicenow', config: { - apiUrl: servicenowSimulatorURL, + apiUrl: serviceNowSimulatorURL, }, secrets: mockServiceNow.secrets, }) @@ -84,7 +103,8 @@ export default function servicenowTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', is_missing_secrets: false, config: { - apiUrl: servicenowSimulatorURL, + apiUrl: serviceNowSimulatorURL, + isLegacy: false, }, }); @@ -99,11 +119,33 @@ export default function servicenowTest({ getService }: FtrProviderContext) { connector_type_id: '.servicenow', is_missing_secrets: false, config: { - apiUrl: servicenowSimulatorURL, + apiUrl: serviceNowSimulatorURL, + isLegacy: false, }, }); }); + it('should set the isLegacy to false when not provided', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow', + config: { + apiUrl: serviceNowSimulatorURL, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction.config.isLegacy).to.be(false); + }); + it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { await supertest .post('/api/actions/connector') @@ -155,7 +197,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { name: 'A servicenow action', connector_type_id: '.servicenow', config: { - apiUrl: servicenowSimulatorURL, + apiUrl: serviceNowSimulatorURL, }, }) .expect(400) @@ -170,10 +212,7 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); - describe('ServiceNow - Executor', () => { - let simulatedActionId: string; - let proxyServer: httpProxy | undefined; - let proxyHaveBeenCalled = false; + describe('ServiceNow ITSM - Executor', () => { before(async () => { const { body } = await supertest .post('/api/actions/connector') @@ -182,19 +221,12 @@ export default function servicenowTest({ getService }: FtrProviderContext) { name: 'A servicenow simulator', connector_type_id: '.servicenow', config: { - apiUrl: servicenowSimulatorURL, + apiUrl: serviceNowSimulatorURL, + isLegacy: false, }, secrets: mockServiceNow.secrets, }); simulatedActionId = body.id; - - proxyServer = await getHttpProxyServer( - kibanaServer.resolveUrl('/'), - configService.get('kbnTestServer.serverArgs'), - () => { - proxyHaveBeenCalled = true; - } - ); }); describe('Validation', () => { @@ -377,31 +409,81 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); describe('Execution', () => { - it('should handle creating an incident without comments', async () => { - const { body: result } = await supertest - .post(`/api/actions/connector/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockServiceNow.params, - subActionParams: { - incident: mockServiceNow.params.subActionParams.incident, - comments: [], + // New connectors + describe('Import set API', () => { + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: mockServiceNow.params.subActionParams.incident, + comments: [], + }, }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${serviceNowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, }, - }) - .expect(200); - - expect(proxyHaveBeenCalled).to.equal(true); - expect(result).to.eql({ - status: 'ok', - connector_id: simulatedActionId, - data: { - id: '123', - title: 'INC01', - pushedDate: '2020-03-10T12:24:20.000Z', - url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, - }, + }); + }); + }); + + // Legacy connectors + describe('Table API', () => { + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + connector_type_id: '.servicenow', + config: { + apiUrl: serviceNowSimulatorURL, + isLegacy: true, + }, + secrets: mockServiceNow.secrets, + }); + simulatedActionId = body.id; + }); + + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: mockServiceNow.params.subActionParams.incident, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${serviceNowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + }, + }); }); }); @@ -453,12 +535,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) { }); }); }); - - after(() => { - if (proxyServer) { - proxyServer.close(); - } - }); }); }); } diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts new file mode 100644 index 0000000000000..eee3425b6a61f --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts @@ -0,0 +1,544 @@ +/* + * 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 httpProxy from 'http-proxy'; +import expect from '@kbn/expect'; +import getPort from 'get-port'; +import http from 'http'; + +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getServiceNowServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function serviceNowSIRTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const configService = getService('config'); + + const mockServiceNow = { + config: { + apiUrl: 'www.servicenowisinkibanaactions.com', + isLegacy: false, + }, + secrets: { + password: 'elastic', + username: 'changeme', + }, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + externalId: null, + short_description: 'Incident title', + description: 'Incident description', + dest_ip: ['192.168.1.1', '192.168.1.3'], + source_ip: ['192.168.1.2', '192.168.1.4'], + malware_hash: ['5feceb66ffc86f38d952786c6d696c79c2dbc239dd4e91b46729d73a27fb57e9'], + malware_url: ['https://example.com'], + category: 'software', + subcategory: 'os', + correlation_id: 'alertID', + correlation_display: 'Alerting', + priority: '1', + }, + comments: [ + { + comment: 'first comment', + commentId: '456', + }, + ], + }, + }, + }; + + describe('ServiceNow SIR', () => { + let simulatedActionId = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + + before(async () => { + serviceNowServer = await getServiceNowServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!serviceNowServer.listening) { + serviceNowServer.listen(availablePort); + } + serviceNowSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + serviceNowSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + after(() => { + serviceNowServer.close(); + if (proxyServer) { + proxyServer.close(); + } + }); + + describe('ServiceNow SIR - Action Creation', () => { + it('should return 200 when creating a servicenow action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: serviceNowSimulatorURL, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + id: createdAction.id, + is_preconfigured: false, + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + is_missing_secrets: false, + config: { + apiUrl: serviceNowSimulatorURL, + isLegacy: false, + }, + }); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + is_missing_secrets: false, + config: { + apiUrl: serviceNowSimulatorURL, + isLegacy: false, + }, + }); + }); + + it('should set the isLegacy to false when not provided', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: serviceNowSimulatorURL, + }, + secrets: mockServiceNow.secrets, + }) + .expect(200); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction.config.isLegacy).to.be(false); + }); + + it('should respond with a 400 Bad Request when creating a servicenow action with no apiUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + config: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a servicenow action with a not present in allowedHosts apiUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: 'http://servicenow.mynonexistent.com', + }, + secrets: mockServiceNow.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: error configuring connector action: target url "http://servicenow.mynonexistent.com" is not added to the Kibana config xpack.actions.allowedHosts', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a servicenow action without secrets', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow action', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: serviceNowSimulatorURL, + }, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [password]: expected value of type [string] but got [undefined]', + }); + }); + }); + }); + + describe('ServiceNow SIR - Executor', () => { + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: serviceNowSimulatorURL, + isLegacy: false, + }, + secrets: mockServiceNow.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); + expect(resp.body.status).to.eql('error'); + expect(resp.body.retry).to.eql(false); + // Node.js 12 oddity: + // + // The first time after the server is booted, the error message will be: + // + // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) + // + // After this, the error will be: + // + // Cannot destructure property 'value' of 'undefined' as it is undefined. + // + // The error seems to come from the exact same place in the code based on the + // exact same circumstances: + // + // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 + // + // What triggers the error is that the `handleError` function expects its 2nd + // argument to be an object containing a `valids` property of type array. + // + // In this test the object does not contain a `valids` property, so hence the + // error. + // + // Why the error message isn't the same in all scenarios is unknown to me and + // could be a bug in V8. + expect(resp.body.message).to.match( + /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + ); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]\n- [4.subAction]: expected value to equal [getChoices]', + }); + }); + }); + + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]', + }); + }); + }); + + it('should handle failing with a simulated success without title', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + savedObjectId: 'success', + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.incident.short_description]: expected value of type [string] but got [undefined]\n- [4.subAction]: expected value to equal [getChoices]', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: { + ...mockServiceNow.params.subActionParams.incident, + short_description: 'success', + }, + comments: [{ comment: 'boo' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: { + ...mockServiceNow.params.subActionParams.incident, + short_description: 'success', + }, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [4.subAction]: expected value to equal [getChoices]', + }); + }); + }); + + describe('getChoices', () => { + it('should fail when field is not provided', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + subAction: 'getChoices', + subActionParams: {}, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getFields]\n- [1.subAction]: expected value to equal [getIncident]\n- [2.subAction]: expected value to equal [handshake]\n- [3.subAction]: expected value to equal [pushToService]\n- [4.subActionParams.fields]: expected value of type [array] but got [undefined]', + }); + }); + }); + }); + }); + + describe('Execution', () => { + // New connectors + describe('Import set API', () => { + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: mockServiceNow.params.subActionParams.incident, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${serviceNowSimulatorURL}/nav_to.do?uri=sn_si_incident.do?sys_id=123`, + }, + }); + }); + }); + + // Legacy connectors + describe('Table API', () => { + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A servicenow simulator', + connector_type_id: '.servicenow-sir', + config: { + apiUrl: serviceNowSimulatorURL, + isLegacy: true, + }, + secrets: mockServiceNow.secrets, + }); + simulatedActionId = body.id; + }); + + it('should handle creating an incident without comments', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockServiceNow.params, + subActionParams: { + incident: mockServiceNow.params.subActionParams.incident, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: '123', + title: 'INC01', + pushedDate: '2020-03-10T12:24:20.000Z', + url: `${serviceNowSimulatorURL}/nav_to.do?uri=sn_si_incident.do?sys_id=123`, + }, + }); + }); + }); + + describe('getChoices', () => { + it('should get choices', async () => { + const { body: result } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + subAction: 'getChoices', + subActionParams: { fields: ['priority'] }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(result).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: [ + { + dependent_value: '', + label: '1 - Critical', + value: '1', + }, + { + dependent_value: '', + label: '2 - High', + value: '2', + }, + { + dependent_value: '', + label: '3 - Moderate', + value: '3', + }, + { + dependent_value: '', + label: '4 - Low', + value: '4', + }, + { + dependent_value: '', + label: '5 - Planning', + value: '5', + }, + ], + }); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index db57af0ba1a98..61bd1bcad34ad 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -25,7 +25,8 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); - loadTestFile(require.resolve('./builtin_action_types/servicenow')); + loadTestFile(require.resolve('./builtin_action_types/servicenow_itsm')); + loadTestFile(require.resolve('./builtin_action_types/servicenow_sir')); loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/resilient')); loadTestFile(require.resolve('./builtin_action_types/slack')); diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index 7367641d71585..f34d7398db0c2 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -6,6 +6,9 @@ */ import { omit } from 'lodash'; +import getPort from 'get-port'; +import http from 'http'; + import expect from '@kbn/expect'; import type { ApiResponse, estypes } from '@elastic/elasticsearch'; import type { KibanaClient } from '@elastic/elasticsearch/api/kibana'; @@ -58,6 +61,7 @@ import { User } from './authentication/types'; import { superUser } from './authentication/users'; import { ESCasesConfigureAttributes } from '../../../../plugins/cases/server/services/configure/types'; import { ESCaseAttributes } from '../../../../plugins/cases/server/services/cases/types'; +import { getServiceNowServer } from '../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; function toArray(input: T | T[]): T[] { if (Array.isArray(input)) { @@ -652,13 +656,13 @@ export const getCaseSavedObjectsFromES = async ({ es }: { es: KibanaClient }) => export const createCaseWithConnector = async ({ supertest, configureReq = {}, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth = { user: superUser, space: null }, createCaseReq = getPostCaseRequest(), }: { supertest: SuperTest.SuperTest; - servicenowSimulatorURL: string; + serviceNowSimulatorURL: string; actionsRemover: ActionsRemover; configureReq?: Record; auth?: { user: User; space: string | null }; @@ -671,7 +675,7 @@ export const createCaseWithConnector = async ({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth, }); @@ -1220,3 +1224,17 @@ export const getAlertsAttachedToCase = async ({ return theCase; }; + +export const getServiceNowSimulationServer = async (): Promise<{ + server: http.Server; + url: string; +}> => { + const server = await getServiceNowServer(); + const port = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!server.listening) { + server.listen(port); + } + const url = `http://localhost:${port}`; + + return { server, url }; +}; diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts index 0ea66d35b63b8..73e8f2ba851fc 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/push_case.ts @@ -7,6 +7,8 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import http from 'http'; + import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; @@ -32,11 +34,8 @@ import { getServiceNowConnector, getConnectorMappingsFromES, getCase, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { CaseConnector, CaseStatuses, @@ -55,17 +54,17 @@ import { // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); const es = getService('es'); describe('push_case', () => { const actionsRemover = new ActionsRemover(supertest); + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -73,10 +72,14 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should push a case', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); const theCase = await pushCase({ @@ -95,18 +98,13 @@ export default ({ getService }: FtrProviderContext): void => { external_title: 'INC01', }); - // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins - expect( - external_url.includes( - 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' - ) - ).to.equal(true); + expect(external_url.includes('nav_to.do?uri=incident.do?sys_id=123')).to.equal(true); }); it('preserves the connector.id after pushing a case', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); const theCase = await pushCase({ @@ -121,7 +119,7 @@ export default ({ getService }: FtrProviderContext): void => { it('preserves the external_service.connector_id after updating the connector', async () => { const { postedCase, connector: pushConnector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); @@ -135,7 +133,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); @@ -175,7 +173,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); @@ -222,7 +220,7 @@ export default ({ getService }: FtrProviderContext): void => { it('pushes a comment appropriately', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); await createComment({ supertest, caseId: postedCase.id, params: postCommentUserReq }); @@ -241,7 +239,7 @@ export default ({ getService }: FtrProviderContext): void => { closure_type: 'close-by-pushing', }, supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); const theCase = await pushCase({ @@ -256,7 +254,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should create the correct user action', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); const pushedCase = await pushCase({ @@ -289,7 +287,7 @@ export default ({ getService }: FtrProviderContext): void => { connector_name: connector.name, external_id: '123', external_title: 'INC01', - external_url: `${servicenowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, + external_url: `${serviceNowSimulatorURL}/nav_to.do?uri=incident.do?sys_id=123`, }); }); @@ -297,7 +295,7 @@ export default ({ getService }: FtrProviderContext): void => { it.skip('should push a collection case but not close it when closure_type: close-by-pushing', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, configureReq: { closure_type: 'close-by-pushing', @@ -337,7 +335,7 @@ export default ({ getService }: FtrProviderContext): void => { it('unhappy path = 409s when case is closed', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); await updateCase({ @@ -367,7 +365,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: superUserSpace1Auth, }); @@ -383,7 +381,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: superUserSpace1Auth, createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), @@ -404,7 +402,7 @@ export default ({ getService }: FtrProviderContext): void => { } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: superUserSpace1Auth, }); @@ -422,7 +420,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case in a space that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: { user: superUser, space: 'space2' }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts index 255a2a4ce28b5..fda2c8d361042 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/cases/user_actions/get_all_user_actions.ts @@ -5,6 +5,7 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../../common/ftr_provider_context'; @@ -17,13 +18,10 @@ import { deleteConfiguration, getConfigurationRequest, getServiceNowConnector, + getServiceNowSimulationServer, } from '../../../../../common/lib/utils'; import { ObjectRemover as ActionsRemover } from '../../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getCreateConnectorUrl } from '../../../../../../../plugins/cases/common/utils/connectors_api'; // eslint-disable-next-line import/no-default-export @@ -31,15 +29,17 @@ export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); describe('get_all_user_actions', () => { - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; + + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); + afterEach(async () => { await deleteCasesByESQuery(es); await deleteComments(es); @@ -48,13 +48,17 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it(`on new push to service, user action: 'push-to-service' should be called with actionFields: ['pushed']`, async () => { const { body: connector } = await supertest .post(getCreateConnectorUrl()) .set('kbn-xsrf', 'true') .send({ ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }) .expect(200); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts index ff8f1cff884af..404b63376daa4 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_configure.ts @@ -5,14 +5,10 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getServiceNowConnector, @@ -22,6 +18,7 @@ import { getConfigurationRequest, removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; @@ -29,27 +26,31 @@ import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); describe('get_configure', () => { - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should return a configuration with mapping', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); actionsRemover.add('default', connector.id, 'action', 'actions'); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts index fb922f8d10243..c3e737464f19b 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/get_connectors.ts @@ -109,6 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', + isLegacy: false, }, isPreconfigured: false, isMissingSecrets: false, @@ -118,7 +119,10 @@ export default ({ getService }: FtrProviderContext): void => { id: sir.id, actionTypeId: '.servicenow-sir', name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, + config: { + apiUrl: 'http://some.non.existent.com', + isLegacy: false, + }, isPreconfigured: false, isMissingSecrets: false, referencedByCount: 0, diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts index 789b68b19beb6..26eba77dd2576 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/patch_configure.ts @@ -5,13 +5,10 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -22,6 +19,7 @@ import { updateConfiguration, getServiceNowConnector, createConnector, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; @@ -29,16 +27,16 @@ import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -46,12 +44,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should patch a configuration connector and create mappings', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); @@ -107,7 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); diff --git a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts index 96ffcf4bc3f5c..077bfc5861322 100644 --- a/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/security_and_spaces/tests/trial/configure/post_configure.ts @@ -5,14 +5,11 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -22,22 +19,23 @@ import { createConfiguration, createConnector, getServiceNowConnector, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -45,12 +43,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should create a configuration with mapping', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, }); diff --git a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts index 6294400281b92..69d403ea15301 100644 --- a/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/security_only/tests/trial/cases/push_case.ts @@ -5,6 +5,8 @@ * 2.0. */ +import http from 'http'; + import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; @@ -13,11 +15,8 @@ import { pushCase, deleteAllCaseItems, createCaseWithConnector, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { globalRead, noKibanaPrivileges, @@ -31,17 +30,17 @@ import { secOnlyDefaultSpaceAuth } from '../../../utils'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); const es = getService('es'); describe('push_case', () => { const actionsRemover = new ActionsRemover(supertest); + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -49,12 +48,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + const supertestWithoutAuth = getService('supertestWithoutAuth'); it('should push a case that the user has permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); @@ -69,7 +72,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should not push a case that the user does not have permissions for', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, createCaseReq: getPostCaseRequest({ owner: 'observabilityFixture' }), }); @@ -95,7 +98,7 @@ export default ({ getService }: FtrProviderContext): void => { } with role(s) ${user.roles.join()} - should NOT push a case`, async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); @@ -112,7 +115,7 @@ export default ({ getService }: FtrProviderContext): void => { it('should return a 404 when attempting to access a space', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts index 28b7fe6095507..bfb266e6f6c3a 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/cases/push_case.ts @@ -6,7 +6,7 @@ */ /* eslint-disable @typescript-eslint/naming-convention */ - +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; @@ -17,27 +17,24 @@ import { deleteAllCaseItems, createCaseWithConnector, getAuthWithSuperUser, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); - const kibanaServer = getService('kibanaServer'); const es = getService('es'); const authSpace1 = getAuthWithSuperUser(); describe('push_case', () => { const actionsRemover = new ActionsRemover(supertest); + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - let servicenowSimulatorURL: string = ''; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -45,10 +42,14 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should push a case in space1', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: authSpace1, }); @@ -69,18 +70,13 @@ export default ({ getService }: FtrProviderContext): void => { external_title: 'INC01', }); - // external_url is of the form http://elastic:changeme@localhost:5620 which is different between various environments like Jekins - expect( - external_url.includes( - 'api/_actions-FTS-external-service-simulators/servicenow/nav_to.do?uri=incident.do?sys_id=123' - ) - ).to.equal(true); + expect(external_url.includes('nav_to.do?uri=incident.do?sys_id=123')).to.equal(true); }); it('should not push a case in a different space', async () => { const { postedCase, connector } = await createCaseWithConnector({ supertest, - servicenowSimulatorURL, + serviceNowSimulatorURL, actionsRemover, auth: authSpace1, }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts index a142e6470ae93..4da44f08c6236 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_configure.ts @@ -5,14 +5,10 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; - import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; import { getServiceNowConnector, @@ -23,6 +19,7 @@ import { removeServerGeneratedPropertiesFromSavedObject, getConfigurationOutput, getAuthWithSuperUser, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { nullUser } from '../../../../common/lib/mock'; @@ -31,28 +28,32 @@ import { nullUser } from '../../../../common/lib/mock'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const actionsRemover = new ActionsRemover(supertest); - const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); describe('get_configure', () => { - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should return a configuration with a mapping from space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth: authSpace1, }); @@ -107,7 +108,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth: authSpace1, }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts index 0301fa3a930cb..7b6848d1f301e 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/get_connectors.ts @@ -109,6 +109,7 @@ export default ({ getService }: FtrProviderContext): void => { name: 'ServiceNow Connector', config: { apiUrl: 'http://some.non.existent.com', + isLegacy: false, }, isPreconfigured: false, isMissingSecrets: false, @@ -118,7 +119,10 @@ export default ({ getService }: FtrProviderContext): void => { id: sir.id, actionTypeId: '.servicenow-sir', name: 'ServiceNow Connector', - config: { apiUrl: 'http://some.non.existent.com' }, + config: { + apiUrl: 'http://some.non.existent.com', + isLegacy: false, + }, isPreconfigured: false, isMissingSecrets: false, referencedByCount: 0, diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts index 14d0debe2ac17..ca362d13ae459 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/patch_configure.ts @@ -5,13 +5,10 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -24,6 +21,7 @@ import { createConnector, getAuthWithSuperUser, getActionsSpace, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { nullUser } from '../../../../common/lib/mock'; @@ -32,18 +30,18 @@ import { nullUser } from '../../../../common/lib/mock'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); const space = getActionsSpace(authSpace1.space); describe('patch_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -51,12 +49,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should patch a configuration connector and create mappings in space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth: authSpace1, }); @@ -126,7 +128,7 @@ export default ({ getService }: FtrProviderContext): void => { supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth: authSpace1, }); diff --git a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts index 7c5035193d465..b815278db5bd8 100644 --- a/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts +++ b/x-pack/test/case_api_integration/spaces_only/tests/trial/configure/post_configure.ts @@ -5,14 +5,11 @@ * 2.0. */ +import http from 'http'; import expect from '@kbn/expect'; import { ConnectorTypes } from '../../../../../../plugins/cases/common/api'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; import { ObjectRemover as ActionsRemover } from '../../../../../alerting_api_integration/common/lib'; -import { - ExternalServiceSimulator, - getExternalServiceSimulatorPath, -} from '../../../../../alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin'; import { getConfigurationRequest, @@ -24,6 +21,7 @@ import { getServiceNowConnector, getAuthWithSuperUser, getActionsSpace, + getServiceNowSimulationServer, } from '../../../../common/lib/utils'; import { nullUser } from '../../../../common/lib/mock'; @@ -31,18 +29,18 @@ import { nullUser } from '../../../../common/lib/mock'; export default ({ getService }: FtrProviderContext): void => { const supertest = getService('supertest'); const es = getService('es'); - const kibanaServer = getService('kibanaServer'); const authSpace1 = getAuthWithSuperUser(); const space = getActionsSpace(authSpace1.space); describe('post_configure', () => { const actionsRemover = new ActionsRemover(supertest); - let servicenowSimulatorURL: string = ''; + let serviceNowSimulatorURL: string = ''; + let serviceNowServer: http.Server; - before(() => { - servicenowSimulatorURL = kibanaServer.resolveUrl( - getExternalServiceSimulatorPath(ExternalServiceSimulator.SERVICENOW) - ); + before(async () => { + const { server, url } = await getServiceNowSimulationServer(); + serviceNowServer = server; + serviceNowSimulatorURL = url; }); afterEach(async () => { @@ -50,12 +48,16 @@ export default ({ getService }: FtrProviderContext): void => { await actionsRemover.removeAll(); }); + after(async () => { + serviceNowServer.close(); + }); + it('should create a configuration with a mapping in space1', async () => { const connector = await createConnector({ supertest, req: { ...getServiceNowConnector(), - config: { apiUrl: servicenowSimulatorURL }, + config: { apiUrl: serviceNowSimulatorURL }, }, auth: authSpace1, }); From 6a55f87da0df83b299de55b79034648a2ec9fe0d Mon Sep 17 00:00:00 2001 From: Vadim Kibana <82822460+vadimkibana@users.noreply.github.com> Date: Tue, 12 Oct 2021 20:25:08 +0200 Subject: [PATCH 089/287] Connect locator persistable state to Short URLs (#114397) --- .../common/url_service/__tests__/setup.ts | 4 +- .../common/url_service/locators/locator.ts | 6 +- .../url_service/locators/locator_client.ts | 92 ++++++++++- .../common/url_service/locators/types.ts | 28 +++- src/plugins/share/common/url_service/mocks.ts | 4 +- .../common/url_service/short_urls/types.ts | 21 +-- .../share/common/url_service/url_service.ts | 8 +- src/plugins/share/public/mocks.ts | 4 +- src/plugins/share/public/plugin.ts | 4 +- src/plugins/share/server/plugin.ts | 24 +-- .../share/server/saved_objects/index.ts | 9 -- src/plugins/share/server/saved_objects/url.ts | 67 -------- src/plugins/share/server/url_service/index.ts | 2 + ...ster_url_service_saved_object_type.test.ts | 144 ++++++++++++++++++ .../register_url_service_saved_object_type.ts | 97 ++++++++++++ .../short_urls/short_url_client.test.ts | 119 ++++++++++++++- .../short_urls/short_url_client.ts | 86 ++++++++--- .../short_urls/short_url_client_factory.ts | 11 +- .../storage/memory_short_url_storage.test.ts | 44 +++++- .../storage/memory_short_url_storage.ts | 41 +++-- .../storage/saved_object_short_url_storage.ts | 46 ++++-- .../server/url_service/short_urls/types.ts | 24 ++- .../apm_plugin/mock_apm_plugin_context.tsx | 2 +- 23 files changed, 719 insertions(+), 168 deletions(-) delete mode 100644 src/plugins/share/server/saved_objects/index.ts delete mode 100644 src/plugins/share/server/saved_objects/url.ts create mode 100644 src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts create mode 100644 src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts diff --git a/src/plugins/share/common/url_service/__tests__/setup.ts b/src/plugins/share/common/url_service/__tests__/setup.ts index 239b2554e663a..8f339c2060faf 100644 --- a/src/plugins/share/common/url_service/__tests__/setup.ts +++ b/src/plugins/share/common/url_service/__tests__/setup.ts @@ -37,7 +37,7 @@ export const urlServiceTestSetup = (partialDeps: Partial getUrl: async () => { throw new Error('not implemented'); }, - shortUrls: { + shortUrls: () => ({ get: () => ({ create: async () => { throw new Error('Not implemented.'); @@ -52,7 +52,7 @@ export const urlServiceTestSetup = (partialDeps: Partial throw new Error('Not implemented.'); }, }), - }, + }), ...partialDeps, }; const service = new UrlService(deps); diff --git a/src/plugins/share/common/url_service/locators/locator.ts b/src/plugins/share/common/url_service/locators/locator.ts index fc970e2c7a490..2d33f701df595 100644 --- a/src/plugins/share/common/url_service/locators/locator.ts +++ b/src/plugins/share/common/url_service/locators/locator.ts @@ -67,13 +67,15 @@ export class Locator

implements LocatorPublic

{ state: P, references: SavedObjectReference[] ): P => { - return this.definition.inject ? this.definition.inject(state, references) : state; + if (!this.definition.inject) return state; + return this.definition.inject(state, references); }; public readonly extract: PersistableState

['extract'] = ( state: P ): { state: P; references: SavedObjectReference[] } => { - return this.definition.extract ? this.definition.extract(state) : { state, references: [] }; + if (!this.definition.extract) return { state, references: [] }; + return this.definition.extract(state); }; // LocatorPublic

---------------------------------------------------------- diff --git a/src/plugins/share/common/url_service/locators/locator_client.ts b/src/plugins/share/common/url_service/locators/locator_client.ts index 587083551aa6d..7dd69165be5dd 100644 --- a/src/plugins/share/common/url_service/locators/locator_client.ts +++ b/src/plugins/share/common/url_service/locators/locator_client.ts @@ -7,9 +7,12 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; +import { MigrateFunctionsObject } from 'src/plugins/kibana_utils/common'; +import { SavedObjectReference } from 'kibana/server'; import type { LocatorDependencies } from './locator'; -import type { LocatorDefinition, LocatorPublic, ILocatorClient } from './types'; +import type { LocatorDefinition, LocatorPublic, ILocatorClient, LocatorData } from './types'; import { Locator } from './locator'; +import { LocatorMigrationFunction, LocatorsMigrationMap } from '.'; export type LocatorClientDependencies = LocatorDependencies; @@ -44,4 +47,91 @@ export class LocatorClient implements ILocatorClient { public get

(id: string): undefined | LocatorPublic

{ return this.locators.get(id); } + + protected getOrThrow

(id: string): LocatorPublic

{ + const locator = this.locators.get(id); + if (!locator) throw new Error(`Locator [ID = "${id}"] is not registered.`); + return locator; + } + + public migrations(): { [locatorId: string]: MigrateFunctionsObject } { + const migrations: { [locatorId: string]: MigrateFunctionsObject } = {}; + + for (const locator of this.locators.values()) { + migrations[locator.id] = locator.migrations; + } + + return migrations; + } + + // PersistableStateService ---------------------------------------------------------- + + public telemetry( + state: LocatorData, + collector: Record + ): Record { + for (const locator of this.locators.values()) { + collector = locator.telemetry(state.state, collector); + } + + return collector; + } + + public inject(state: LocatorData, references: SavedObjectReference[]): LocatorData { + const locator = this.getOrThrow(state.id); + const filteredReferences = references + .filter((ref) => ref.name.startsWith('params:')) + .map((ref) => ({ + ...ref, + name: ref.name.substr('params:'.length), + })); + return { + ...state, + state: locator.inject(state.state, filteredReferences), + }; + } + + public extract(state: LocatorData): { state: LocatorData; references: SavedObjectReference[] } { + const locator = this.getOrThrow(state.id); + const extracted = locator.extract(state.state); + return { + state: { + ...state, + state: extracted.state, + }, + references: extracted.references.map((ref) => ({ + ...ref, + name: 'params:' + ref.name, + })), + }; + } + + public readonly getAllMigrations = (): LocatorsMigrationMap => { + const locatorParamsMigrations = this.migrations(); + const locatorMigrations: LocatorsMigrationMap = {}; + const versions = new Set(); + + for (const migrationMap of Object.values(locatorParamsMigrations)) + for (const version of Object.keys(migrationMap)) versions.add(version); + + for (const version of versions.values()) { + const migration: LocatorMigrationFunction = (locator) => { + const locatorMigrationsMap = locatorParamsMigrations[locator.id]; + if (!locatorMigrationsMap) return locator; + + const migrationFunction = locatorMigrationsMap[version]; + if (!migrationFunction) return locator; + + return { + ...locator, + version, + state: migrationFunction(locator.state), + }; + }; + + locatorMigrations[version] = migration; + } + + return locatorMigrations; + }; } diff --git a/src/plugins/share/common/url_service/locators/types.ts b/src/plugins/share/common/url_service/locators/types.ts index ab0efa9b2375a..c64dc588aaf22 100644 --- a/src/plugins/share/common/url_service/locators/types.ts +++ b/src/plugins/share/common/url_service/locators/types.ts @@ -8,13 +8,18 @@ import type { SerializableRecord } from '@kbn/utility-types'; import { DependencyList } from 'react'; -import { PersistableState } from 'src/plugins/kibana_utils/common'; +import { + MigrateFunction, + PersistableState, + PersistableStateService, + VersionedState, +} from 'src/plugins/kibana_utils/common'; import type { FormatSearchParamsOptions } from './redirect'; /** * URL locator registry. */ -export interface ILocatorClient { +export interface ILocatorClient extends PersistableStateService { /** * Create and register a new locator. * @@ -141,3 +146,22 @@ export interface KibanaLocation { */ state: S; } + +/** + * Represents a serializable state of a locator. Includes locator ID, version + * and its params. + */ +export interface LocatorData + extends VersionedState, + SerializableRecord { + /** + * Locator ID. + */ + id: string; +} + +export interface LocatorsMigrationMap { + [semver: string]: LocatorMigrationFunction; +} + +export type LocatorMigrationFunction = MigrateFunction; diff --git a/src/plugins/share/common/url_service/mocks.ts b/src/plugins/share/common/url_service/mocks.ts index dd86e2398589e..24ba226818427 100644 --- a/src/plugins/share/common/url_service/mocks.ts +++ b/src/plugins/share/common/url_service/mocks.ts @@ -18,7 +18,7 @@ export class MockUrlService extends UrlService { getUrl: async ({ app, path }, { absolute }) => { return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`; }, - shortUrls: { + shortUrls: () => ({ get: () => ({ create: async () => { throw new Error('Not implemented.'); @@ -33,7 +33,7 @@ export class MockUrlService extends UrlService { throw new Error('Not implemented.'); }, }), - }, + }), }); } } diff --git a/src/plugins/share/common/url_service/short_urls/types.ts b/src/plugins/share/common/url_service/short_urls/types.ts index db744a25f9f79..698ffe7b8421b 100644 --- a/src/plugins/share/common/url_service/short_urls/types.ts +++ b/src/plugins/share/common/url_service/short_urls/types.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { SerializableRecord } from '@kbn/utility-types'; -import { VersionedState } from 'src/plugins/kibana_utils/common'; -import { LocatorPublic } from '../locators'; +import type { SerializableRecord } from '@kbn/utility-types'; +import type { LocatorPublic, ILocatorClient, LocatorData } from '../locators'; /** * A factory for Short URL Service. We need this factory as the dependency @@ -21,6 +20,10 @@ export interface IShortUrlClientFactory { get(dependencies: D): IShortUrlClient; } +export type IShortUrlClientFactoryProvider = (params: { + locators: ILocatorClient; +}) => IShortUrlClientFactory; + /** * CRUD-like API for short URLs. */ @@ -128,14 +131,4 @@ export interface ShortUrlData; } -/** - * Represents a serializable state of a locator. Includes locator ID, version - * and its params. - */ -export interface LocatorData - extends VersionedState { - /** - * Locator ID. - */ - id: string; -} +export type { LocatorData }; diff --git a/src/plugins/share/common/url_service/url_service.ts b/src/plugins/share/common/url_service/url_service.ts index dedb81720865d..24e2ea0b62379 100644 --- a/src/plugins/share/common/url_service/url_service.ts +++ b/src/plugins/share/common/url_service/url_service.ts @@ -7,10 +7,10 @@ */ import { LocatorClient, LocatorClientDependencies } from './locators'; -import { IShortUrlClientFactory } from './short_urls'; +import { IShortUrlClientFactoryProvider, IShortUrlClientFactory } from './short_urls'; export interface UrlServiceDependencies extends LocatorClientDependencies { - shortUrls: IShortUrlClientFactory; + shortUrls: IShortUrlClientFactoryProvider; } /** @@ -26,6 +26,8 @@ export class UrlService { constructor(protected readonly deps: UrlServiceDependencies) { this.locators = new LocatorClient(deps); - this.shortUrls = deps.shortUrls; + this.shortUrls = deps.shortUrls({ + locators: this.locators, + }); } } diff --git a/src/plugins/share/public/mocks.ts b/src/plugins/share/public/mocks.ts index 73df7257290f0..33cdf141de9f3 100644 --- a/src/plugins/share/public/mocks.ts +++ b/src/plugins/share/public/mocks.ts @@ -18,7 +18,7 @@ const url = new UrlService({ getUrl: async ({ app, path }, { absolute }) => { return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`; }, - shortUrls: { + shortUrls: () => ({ get: () => ({ create: async () => { throw new Error('Not implemented'); @@ -33,7 +33,7 @@ const url = new UrlService({ throw new Error('Not implemented.'); }, }), - }, + }), }); const createSetupContract = (): Setup => { diff --git a/src/plugins/share/public/plugin.ts b/src/plugins/share/public/plugin.ts index 103fbb50bb95f..fd8a5fd7541a6 100644 --- a/src/plugins/share/public/plugin.ts +++ b/src/plugins/share/public/plugin.ts @@ -104,7 +104,7 @@ export class SharePlugin implements Plugin { }); return url; }, - shortUrls: { + shortUrls: () => ({ get: () => ({ create: async () => { throw new Error('Not implemented'); @@ -119,7 +119,7 @@ export class SharePlugin implements Plugin { throw new Error('Not implemented.'); }, }), - }, + }), }); this.url.locators.create(new LegacyShortUrlLocatorDefinition()); diff --git a/src/plugins/share/server/plugin.ts b/src/plugins/share/server/plugin.ts index f0e4abf9eb589..d79588420fe87 100644 --- a/src/plugins/share/server/plugin.ts +++ b/src/plugins/share/server/plugin.ts @@ -9,11 +9,14 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { CoreSetup, Plugin, PluginInitializerContext } from 'kibana/server'; -import { url } from './saved_objects'; import { CSV_SEPARATOR_SETTING, CSV_QUOTE_VALUES_SETTING } from '../common/constants'; import { UrlService } from '../common/url_service'; -import { ServerUrlService, ServerShortUrlClientFactory } from './url_service'; -import { registerUrlServiceRoutes } from './url_service/http/register_url_service_routes'; +import { + ServerUrlService, + ServerShortUrlClientFactory, + registerUrlServiceRoutes, + registerUrlServiceSavedObjectType, +} from './url_service'; import { LegacyShortUrlLocatorDefinition } from '../common/url_service/locators/legacy_short_url_locator'; /** @public */ @@ -44,18 +47,17 @@ export class SharePlugin implements Plugin { getUrl: async () => { throw new Error('Locator .getUrl() currently is not supported on the server.'); }, - shortUrls: new ServerShortUrlClientFactory({ - currentVersion: this.version, - }), + shortUrls: ({ locators }) => + new ServerShortUrlClientFactory({ + currentVersion: this.version, + locators, + }), }); - this.url.locators.create(new LegacyShortUrlLocatorDefinition()); - const router = core.http.createRouter(); - - registerUrlServiceRoutes(core, router, this.url); + registerUrlServiceSavedObjectType(core.savedObjects, this.url); + registerUrlServiceRoutes(core, core.http.createRouter(), this.url); - core.savedObjects.registerType(url); core.uiSettings.register({ [CSV_SEPARATOR_SETTING]: { name: i18n.translate('share.advancedSettings.csv.separatorTitle', { diff --git a/src/plugins/share/server/saved_objects/index.ts b/src/plugins/share/server/saved_objects/index.ts deleted file mode 100644 index ff37efb9fec17..0000000000000 --- a/src/plugins/share/server/saved_objects/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -export { url } from './url'; diff --git a/src/plugins/share/server/saved_objects/url.ts b/src/plugins/share/server/saved_objects/url.ts deleted file mode 100644 index 6288e87f629f5..0000000000000 --- a/src/plugins/share/server/saved_objects/url.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { SavedObjectsType } from 'kibana/server'; - -export const url: SavedObjectsType = { - name: 'url', - namespaceType: 'single', - hidden: false, - management: { - icon: 'link', - defaultSearchField: 'url', - importableAndExportable: true, - getTitle(obj) { - return `/goto/${encodeURIComponent(obj.id)}`; - }, - getInAppUrl(obj) { - return { - path: '/goto/' + encodeURIComponent(obj.id), - uiCapabilitiesPath: '', - }; - }, - }, - mappings: { - properties: { - slug: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - }, - }, - }, - accessCount: { - type: 'long', - }, - accessDate: { - type: 'date', - }, - createDate: { - type: 'date', - }, - // Legacy field - contains already pre-formatted final URL. - // This is here to support old saved objects that have this field. - // TODO: Remove this field and execute a migration to the new format. - url: { - type: 'text', - fields: { - keyword: { - type: 'keyword', - ignore_above: 2048, - }, - }, - }, - // Information needed to load and execute a locator. - locatorJSON: { - type: 'text', - index: false, - }, - }, - }, -}; diff --git a/src/plugins/share/server/url_service/index.ts b/src/plugins/share/server/url_service/index.ts index 068a5289d42ed..62d1329371736 100644 --- a/src/plugins/share/server/url_service/index.ts +++ b/src/plugins/share/server/url_service/index.ts @@ -8,3 +8,5 @@ export * from './types'; export * from './short_urls'; +export { registerUrlServiceRoutes } from './http/register_url_service_routes'; +export { registerUrlServiceSavedObjectType } from './saved_objects/register_url_service_saved_object_type'; diff --git a/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts new file mode 100644 index 0000000000000..651169f6101a9 --- /dev/null +++ b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.test.ts @@ -0,0 +1,144 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { SerializableRecord } from '@kbn/utility-types'; +import type { + SavedObjectMigrationMap, + SavedObjectsType, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { ServerShortUrlClientFactory } from '..'; +import { UrlService, LocatorDefinition } from '../../../common/url_service'; +import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/locators/legacy_short_url_locator'; +import { MemoryShortUrlStorage } from '../short_urls/storage/memory_short_url_storage'; +import { ShortUrlSavedObjectAttributes } from '../short_urls/storage/saved_object_short_url_storage'; +import { registerUrlServiceSavedObjectType } from './register_url_service_saved_object_type'; + +const setup = () => { + const currentVersion = '7.7.7'; + const service = new UrlService({ + getUrl: () => { + throw new Error('Not implemented.'); + }, + navigate: () => { + throw new Error('Not implemented.'); + }, + shortUrls: ({ locators }) => + new ServerShortUrlClientFactory({ + currentVersion, + locators, + }), + }); + const definition = new LegacyShortUrlLocatorDefinition(); + const locator = service.locators.create(definition); + const storage = new MemoryShortUrlStorage(); + const client = service.shortUrls.get({ storage }); + + let type: SavedObjectsType; + registerUrlServiceSavedObjectType( + { + registerType: (urlSavedObjectType) => { + type = urlSavedObjectType; + }, + }, + service + ); + + return { + type: type!, + client, + service, + storage, + locator, + definition, + currentVersion, + }; +}; + +describe('migrations', () => { + test('returns empty migrations object if there are no migrations', () => { + const { type } = setup(); + + expect((type.migrations as () => SavedObjectMigrationMap)()).toEqual({}); + }); + + test('migrates locator to the latest version', () => { + interface FooLocatorParamsOld extends SerializableRecord { + color: string; + indexPattern: string; + } + + interface FooLocatorParams extends SerializableRecord { + color: string; + indexPatterns: string[]; + } + + class FooLocatorDefinition implements LocatorDefinition { + public readonly id = 'FOO_LOCATOR'; + + public async getLocation() { + return { + app: 'foo', + path: '', + state: {}, + }; + } + + migrations = { + '8.0.0': ({ indexPattern, ...rest }: FooLocatorParamsOld): FooLocatorParams => ({ + ...rest, + indexPatterns: [indexPattern], + }), + }; + } + + const { type, service } = setup(); + + service.locators.create(new FooLocatorDefinition()); + + const migrationFunction = (type.migrations as () => SavedObjectMigrationMap)()['8.0.0']; + + expect(typeof migrationFunction).toBe('function'); + + const doc1: SavedObjectUnsanitizedDoc = { + id: 'foo', + attributes: { + accessCount: 0, + accessDate: 0, + createDate: 0, + locatorJSON: JSON.stringify({ + id: 'FOO_LOCATOR', + version: '7.7.7', + state: { + color: 'red', + indexPattern: 'myIndex', + }, + }), + url: '', + }, + type: 'url', + }; + + const doc2 = migrationFunction(doc1, {} as any); + + expect(doc2.id).toBe('foo'); + expect(doc2.type).toBe('url'); + expect(doc2.attributes.accessCount).toBe(0); + expect(doc2.attributes.accessDate).toBe(0); + expect(doc2.attributes.createDate).toBe(0); + expect(doc2.attributes.url).toBe(''); + expect(JSON.parse(doc2.attributes.locatorJSON)).toEqual({ + id: 'FOO_LOCATOR', + version: '8.0.0', + state: { + color: 'red', + indexPatterns: ['myIndex'], + }, + }); + }); +}); diff --git a/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.ts new file mode 100644 index 0000000000000..b2fcefcc767cf --- /dev/null +++ b/src/plugins/share/server/url_service/saved_objects/register_url_service_saved_object_type.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 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { + SavedObjectMigrationMap, + SavedObjectsServiceSetup, + SavedObjectsType, +} from 'kibana/server'; +import type { LocatorData } from 'src/plugins/share/common/url_service'; +import type { ServerUrlService } from '..'; + +export const registerUrlServiceSavedObjectType = ( + so: Pick, + service: ServerUrlService +) => { + const urlSavedObjectType: SavedObjectsType = { + name: 'url', + namespaceType: 'single', + hidden: false, + management: { + icon: 'link', + defaultSearchField: 'url', + importableAndExportable: true, + getTitle(obj) { + return `/goto/${encodeURIComponent(obj.id)}`; + }, + getInAppUrl(obj) { + return { + path: '/goto/' + encodeURIComponent(obj.id), + uiCapabilitiesPath: '', + }; + }, + }, + mappings: { + properties: { + slug: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + }, + }, + }, + accessCount: { + type: 'long', + }, + accessDate: { + type: 'date', + }, + createDate: { + type: 'date', + }, + // Legacy field - contains already pre-formatted final URL. + // This is here to support old saved objects that have this field. + // TODO: Remove this field and execute a migration to the new format. + url: { + type: 'text', + fields: { + keyword: { + type: 'keyword', + ignore_above: 2048, + }, + }, + }, + // Information needed to load and execute a locator. + locatorJSON: { + type: 'text', + index: false, + }, + }, + }, + migrations: () => { + const locatorMigrations = service.locators.getAllMigrations(); + const savedObjectLocatorMigrations: SavedObjectMigrationMap = {}; + + for (const [version, locatorMigration] of Object.entries(locatorMigrations)) { + savedObjectLocatorMigrations[version] = (doc) => { + const locator = JSON.parse(doc.attributes.locatorJSON) as LocatorData; + doc.attributes = { + ...doc.attributes, + locatorJSON: JSON.stringify(locatorMigration(locator)), + }; + return doc; + }; + } + + return savedObjectLocatorMigrations; + }, + }; + + so.registerType(urlSavedObjectType); +}; diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts index ac684eb03a9d5..503748a2b1cad 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.test.ts @@ -7,9 +7,11 @@ */ import { ServerShortUrlClientFactory } from './short_url_client_factory'; -import { UrlService } from '../../../common/url_service'; +import { UrlService, LocatorDefinition } from '../../../common/url_service'; import { LegacyShortUrlLocatorDefinition } from '../../../common/url_service/locators/legacy_short_url_locator'; import { MemoryShortUrlStorage } from './storage/memory_short_url_storage'; +import { SerializableRecord } from '@kbn/utility-types'; +import { SavedObjectReference } from 'kibana/server'; const setup = () => { const currentVersion = '1.2.3'; @@ -20,9 +22,11 @@ const setup = () => { navigate: () => { throw new Error('Not implemented.'); }, - shortUrls: new ServerShortUrlClientFactory({ - currentVersion, - }), + shortUrls: ({ locators }) => + new ServerShortUrlClientFactory({ + currentVersion, + locators, + }), }); const definition = new LegacyShortUrlLocatorDefinition(); const locator = service.locators.create(definition); @@ -177,4 +181,111 @@ describe('ServerShortUrlClient', () => { ); }); }); + + describe('Persistable State', () => { + interface FooLocatorParams extends SerializableRecord { + dashboardId: string; + indexPatternId: string; + } + + class FooLocatorDefinition implements LocatorDefinition { + public readonly id = 'FOO_LOCATOR'; + + public readonly getLocation = async () => ({ + app: 'foo_app', + path: '/foo/path', + state: {}, + }); + + public readonly extract = ( + state: FooLocatorParams + ): { state: FooLocatorParams; references: SavedObjectReference[] } => ({ + state, + references: [ + { + id: state.dashboardId, + type: 'dashboard', + name: 'dashboardId', + }, + { + id: state.indexPatternId, + type: 'index_pattern', + name: 'indexPatternId', + }, + ], + }); + + public readonly inject = ( + state: FooLocatorParams, + references: SavedObjectReference[] + ): FooLocatorParams => { + const dashboard = references.find( + (ref) => ref.type === 'dashboard' && ref.name === 'dashboardId' + ); + const indexPattern = references.find( + (ref) => ref.type === 'index_pattern' && ref.name === 'indexPatternId' + ); + + return { + ...state, + dashboardId: dashboard ? dashboard.id : '', + indexPatternId: indexPattern ? indexPattern.id : '', + }; + }; + } + + test('extracts and persists references', async () => { + const { service, client, storage } = setup(); + const locator = service.locators.create(new FooLocatorDefinition()); + const shortUrl = await client.create({ + locator, + params: { + dashboardId: '123', + indexPatternId: '456', + }, + }); + const record = await storage.getById(shortUrl.data.id); + + expect(record.references).toEqual([ + { + id: '123', + type: 'dashboard', + name: 'locator:params:dashboardId', + }, + { + id: '456', + type: 'index_pattern', + name: 'locator:params:indexPatternId', + }, + ]); + }); + + test('injects references', async () => { + const { service, client, storage } = setup(); + const locator = service.locators.create(new FooLocatorDefinition()); + const shortUrl1 = await client.create({ + locator, + params: { + dashboardId: '3', + indexPatternId: '5', + }, + }); + const record1 = await storage.getById(shortUrl1.data.id); + + record1.data.locator.state = {}; + + await storage.update(record1.data.id, record1.data); + + const record2 = await storage.getById(shortUrl1.data.id); + + expect(record2.data.locator.state).toEqual({}); + + const shortUrl2 = await client.get(shortUrl1.data.id); + + expect(shortUrl2.data.locator.state).toEqual({ + dashboardId: '3', + indexPatternId: '5', + }); + }); + }); }); diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client.ts b/src/plugins/share/server/url_service/short_urls/short_url_client.ts index caaa76bef172d..1efece073d955 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client.ts @@ -7,8 +7,17 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; +import { SavedObjectReference } from 'kibana/server'; import { generateSlug } from 'random-word-slugs'; -import type { IShortUrlClient, ShortUrl, ShortUrlCreateParams } from '../../../common/url_service'; +import { ShortUrlRecord } from '.'; +import type { + IShortUrlClient, + ShortUrl, + ShortUrlCreateParams, + ILocatorClient, + ShortUrlData, + LocatorData, +} from '../../../common/url_service'; import type { ShortUrlStorage } from './types'; import { validateSlug } from './util'; @@ -36,6 +45,11 @@ export interface ServerShortUrlClientDependencies { * Storage provider for short URLs. */ storage: ShortUrlStorage; + + /** + * The locators service. + */ + locators: ILocatorClient; } export class ServerShortUrlClient implements IShortUrlClient { @@ -64,44 +78,80 @@ export class ServerShortUrlClient implements IShortUrlClient { } } + const extracted = this.extractReferences({ + id: locator.id, + version: currentVersion, + state: params, + }); const now = Date.now(); - const data = await storage.create({ - accessCount: 0, - accessDate: now, - createDate: now, - slug, - locator: { - id: locator.id, - version: currentVersion, - state: params, + + const data = await storage.create

( + { + accessCount: 0, + accessDate: now, + createDate: now, + slug, + locator: extracted.state as LocatorData

, }, - }); + { references: extracted.references } + ); return { data, }; } - public async get(id: string): Promise { - const { storage } = this.dependencies; - const data = await storage.getById(id); + private extractReferences(locatorData: LocatorData): { + state: LocatorData; + references: SavedObjectReference[]; + } { + const { locators } = this.dependencies; + const { state, references } = locators.extract(locatorData); + return { + state, + references: references.map((ref) => ({ + ...ref, + name: 'locator:' + ref.name, + })), + }; + } + private injectReferences({ data, references }: ShortUrlRecord): ShortUrlData { + const { locators } = this.dependencies; + const locatorReferences = references + .filter((ref) => ref.name.startsWith('locator:')) + .map((ref) => ({ + ...ref, + name: ref.name.substr('locator:'.length), + })); return { - data, + ...data, + locator: locators.inject(data.locator, locatorReferences), }; } - public async delete(id: string): Promise { + public async get(id: string): Promise { const { storage } = this.dependencies; - await storage.delete(id); + const record = await storage.getById(id); + const data = this.injectReferences(record); + + return { + data, + }; } public async resolve(slug: string): Promise { const { storage } = this.dependencies; - const data = await storage.getBySlug(slug); + const record = await storage.getBySlug(slug); + const data = this.injectReferences(record); return { data, }; } + + public async delete(id: string): Promise { + const { storage } = this.dependencies; + await storage.delete(id); + } } diff --git a/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts b/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts index 696233b7a1ca5..63456c36daa68 100644 --- a/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts +++ b/src/plugins/share/server/url_service/short_urls/short_url_client_factory.ts @@ -8,7 +8,7 @@ import { SavedObjectsClientContract } from 'kibana/server'; import { ShortUrlStorage } from './types'; -import type { IShortUrlClientFactory } from '../../../common/url_service'; +import type { IShortUrlClientFactory, ILocatorClient } from '../../../common/url_service'; import { ServerShortUrlClient } from './short_url_client'; import { SavedObjectShortUrlStorage } from './storage/saved_object_short_url_storage'; @@ -20,6 +20,11 @@ export interface ServerShortUrlClientFactoryDependencies { * Current version of Kibana, e.g. 7.15.0. */ currentVersion: string; + + /** + * Locators service. + */ + locators: ILocatorClient; } export interface ServerShortUrlClientFactoryCreateParams { @@ -39,9 +44,11 @@ export class ServerShortUrlClientFactory savedObjects: params.savedObjects!, savedObjectType: 'url', }); + const { currentVersion, locators } = this.dependencies; const client = new ServerShortUrlClient({ storage, - currentVersion: this.dependencies.currentVersion, + currentVersion, + locators, }); return client; diff --git a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts index d178e0b81786c..5d1b0bfa0bf55 100644 --- a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts +++ b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.test.ts @@ -41,6 +41,46 @@ describe('.create()', () => { }); }); +describe('.update()', () => { + test('can update an existing short URL', async () => { + const storage = new MemoryShortUrlStorage(); + const now = Date.now(); + const url1 = await storage.create({ + accessCount: 0, + createDate: now, + accessDate: now, + locator: { + id: 'TEST_LOCATOR', + version: '7.11', + state: { + foo: 'bar', + }, + }, + slug: 'test-slug', + }); + + await storage.update(url1.id, { + accessCount: 1, + }); + + const url2 = await storage.getById(url1.id); + + expect(url1.accessCount).toBe(0); + expect(url2.data.accessCount).toBe(1); + }); + + test('throws when URL does not exist', async () => { + const storage = new MemoryShortUrlStorage(); + const [, error] = await of( + storage.update('DOES_NOT_EXIST', { + accessCount: 1, + }) + ); + + expect(error).toBeInstanceOf(Error); + }); +}); + describe('.getById()', () => { test('can fetch by ID a newly created short URL', async () => { const storage = new MemoryShortUrlStorage(); @@ -58,7 +98,7 @@ describe('.getById()', () => { }, slug: 'test-slug', }); - const url2 = await storage.getById(url1.id); + const url2 = (await storage.getById(url1.id)).data; expect(url2.accessCount).toBe(0); expect(url1.createDate).toBe(now); @@ -112,7 +152,7 @@ describe('.getBySlug()', () => { }, slug: 'test-slug', }); - const url2 = await storage.getBySlug('test-slug'); + const url2 = (await storage.getBySlug('test-slug')).data; expect(url2.accessCount).toBe(0); expect(url1.createDate).toBe(now); diff --git a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts index 40d76a91154ba..fafd00344eecd 100644 --- a/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts +++ b/src/plugins/share/server/url_service/short_urls/storage/memory_short_url_storage.ts @@ -9,35 +9,54 @@ import { v4 as uuidv4 } from 'uuid'; import type { SerializableRecord } from '@kbn/utility-types'; import { ShortUrlData } from 'src/plugins/share/common/url_service/short_urls/types'; -import { ShortUrlStorage } from '../types'; +import { SavedObjectReference } from 'kibana/server'; +import { ShortUrlStorage, ShortUrlRecord } from '../types'; + +const clone =

(obj: P): P => JSON.parse(JSON.stringify(obj)) as P; export class MemoryShortUrlStorage implements ShortUrlStorage { - private urls = new Map(); + private urls = new Map(); public async create

( - data: Omit, 'id'> + data: Omit, 'id'>, + { references = [] }: { references?: SavedObjectReference[] } = {} ): Promise> { const id = uuidv4(); - const url: ShortUrlData

= { ...data, id }; + const url: ShortUrlRecord

= { + data: { ...data, id }, + references, + }; this.urls.set(id, url); - return url; + + return clone(url.data); + } + + public async update

( + id: string, + data: Partial, 'id'>>, + { references }: { references?: SavedObjectReference[] } = {} + ): Promise { + const so = await this.getById(id); + Object.assign(so.data, data); + if (references) so.references = references; + this.urls.set(id, so); } public async getById

( id: string - ): Promise> { + ): Promise> { if (!this.urls.has(id)) { throw new Error(`No short url with id "${id}"`); } - return this.urls.get(id)! as ShortUrlData

; + return clone(this.urls.get(id)! as ShortUrlRecord

); } public async getBySlug

( slug: string - ): Promise> { + ): Promise> { for (const url of this.urls.values()) { - if (url.slug === slug) { - return url as ShortUrlData

; + if (url.data.slug === slug) { + return clone(url as ShortUrlRecord

); } } throw new Error(`No short url with slug "${slug}".`); @@ -45,7 +64,7 @@ export class MemoryShortUrlStorage implements ShortUrlStorage { public async exists(slug: string): Promise { for (const url of this.urls.values()) { - if (url.slug === slug) { + if (url.data.slug === slug) { return true; } } diff --git a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts index c66db6d82cdbd..792dfabde3cab 100644 --- a/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts +++ b/src/plugins/share/server/url_service/short_urls/storage/saved_object_short_url_storage.ts @@ -7,7 +7,8 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; -import { SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { SavedObject, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; +import { ShortUrlRecord } from '..'; import { LEGACY_SHORT_URL_LOCATOR_ID } from '../../../../common/url_service/locators/legacy_short_url_locator'; import { ShortUrlData } from '../../../../common/url_service/short_urls/types'; import { ShortUrlStorage } from '../types'; @@ -85,12 +86,15 @@ const createShortUrlData =

( }; const createAttributes =

( - data: Omit, 'id'> + data: Partial, 'id'>> ): ShortUrlSavedObjectAttributes => { - const { locator, ...rest } = data; + const { accessCount = 0, accessDate = 0, createDate = 0, slug = '', locator } = data; const attributes: ShortUrlSavedObjectAttributes = { - ...rest, - locatorJSON: JSON.stringify(locator), + accessCount, + accessDate, + createDate, + slug, + locatorJSON: locator ? JSON.stringify(locator) : '', url: '', }; @@ -106,30 +110,49 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage { constructor(private readonly dependencies: SavedObjectShortUrlStorageDependencies) {} public async create

( - data: Omit, 'id'> + data: Omit, 'id'>, + { references }: { references?: SavedObjectReference[] } = {} ): Promise> { const { savedObjects, savedObjectType } = this.dependencies; const attributes = createAttributes(data); const savedObject = await savedObjects.create(savedObjectType, attributes, { refresh: true, + references, }); return createShortUrlData

(savedObject); } + public async update

( + id: string, + data: Partial, 'id'>>, + { references }: { references?: SavedObjectReference[] } = {} + ): Promise { + const { savedObjects, savedObjectType } = this.dependencies; + const attributes = createAttributes(data); + + await savedObjects.update(savedObjectType, id, attributes, { + refresh: true, + references, + }); + } + public async getById

( id: string - ): Promise> { + ): Promise> { const { savedObjects, savedObjectType } = this.dependencies; const savedObject = await savedObjects.get(savedObjectType, id); - return createShortUrlData

(savedObject); + return { + data: createShortUrlData

(savedObject), + references: savedObject.references, + }; } public async getBySlug

( slug: string - ): Promise> { + ): Promise> { const { savedObjects } = this.dependencies; const search = `(attributes.slug:"${escapeSearchReservedChars(slug)}")`; const result = await savedObjects.find({ @@ -143,7 +166,10 @@ export class SavedObjectShortUrlStorage implements ShortUrlStorage { const savedObject = result.saved_objects[0] as ShortUrlSavedObject; - return createShortUrlData

(savedObject); + return { + data: createShortUrlData

(savedObject), + references: savedObject.references, + }; } public async exists(slug: string): Promise { diff --git a/src/plugins/share/server/url_service/short_urls/types.ts b/src/plugins/share/server/url_service/short_urls/types.ts index 7aab70ca49519..9a9d9006eb371 100644 --- a/src/plugins/share/server/url_service/short_urls/types.ts +++ b/src/plugins/share/server/url_service/short_urls/types.ts @@ -7,6 +7,7 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; +import { SavedObjectReference } from 'kibana/server'; import { ShortUrlData } from '../../../common/url_service/short_urls/types'; /** @@ -17,20 +18,32 @@ export interface ShortUrlStorage { * Create and store a new short URL entry. */ create

( - data: Omit, 'id'> + data: Omit, 'id'>, + options?: { references?: SavedObjectReference[] } ): Promise>; + /** + * Update an existing short URL entry. + */ + update

( + id: string, + data: Partial, 'id'>>, + options?: { references?: SavedObjectReference[] } + ): Promise; + /** * Fetch a short URL entry by ID. */ - getById

(id: string): Promise>; + getById

( + id: string + ): Promise>; /** * Fetch a short URL entry by slug. */ getBySlug

( slug: string - ): Promise>; + ): Promise>; /** * Checks if a short URL exists by slug. @@ -42,3 +55,8 @@ export interface ShortUrlStorage { */ delete(id: string): Promise; } + +export interface ShortUrlRecord { + data: ShortUrlData; + references: SavedObjectReference[]; +} diff --git a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx index 617af6dae484d..abdab939f4a0a 100644 --- a/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_plugin/mock_apm_plugin_context.tsx @@ -100,7 +100,7 @@ const urlService = new UrlService({ getUrl: async ({ app, path }, { absolute }) => { return `${absolute ? 'http://localhost:8888' : ''}/app/${app}${path}`; }, - shortUrls: {} as any, + shortUrls: () => ({ get: () => {} } as any), }); const locator = urlService.locators.create(new MlLocatorDefinition()); From 9e908f6caa3b1030e931ccf38b03c40f0f9deaef Mon Sep 17 00:00:00 2001 From: Marshall Main <55718608+marshallmain@users.noreply.github.com> Date: Tue, 12 Oct 2021 14:43:50 -0400 Subject: [PATCH 090/287] [RAC][Security Solution] Refactor persistence and security rule generic types (#114022) * Refactor persistence and security rule generic types * Remove unused import, fix unit tests Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/rule_registry/server/index.ts | 2 +- ...> create_persistence_rule_type_wrapper.ts} | 4 +- .../server/utils/persistence_types.ts | 54 +++++++++----- ...s => create_security_rule_type_wrapper.ts} | 33 +++++++-- .../eql/create_eql_alert_type.test.ts | 12 ++- .../rule_types/eql/create_eql_alert_type.ts | 24 ++---- .../factories/bulk_create_factory.ts | 18 ++++- .../create_indicator_match_alert_type.test.ts | 60 +++++++-------- .../create_indicator_match_alert_type.ts | 24 ++---- .../lib/detection_engine/rule_types/ml.ts | 71 ------------------ .../ml/create_ml_alert_type.test.ts | 14 +++- .../rule_types/ml/create_ml_alert_type.ts | 23 ++---- .../query/create_query_alert_type.test.ts | 43 ++++++----- .../query/create_query_alert_type.ts | 24 ++---- .../create_threshold_alert_type.test.ts | 14 +++- .../threshold/create_threshold_alert_type.ts | 25 ++----- .../lib/detection_engine/rule_types/types.ts | 74 ++++++++----------- .../security_solution/server/plugin.ts | 31 ++++++-- 18 files changed, 254 insertions(+), 296 deletions(-) rename x-pack/plugins/rule_registry/server/utils/{create_persistence_rule_type_factory.ts => create_persistence_rule_type_wrapper.ts} (91%) rename x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/{create_security_rule_type_factory.ts => create_security_rule_type_wrapper.ts} (92%) delete mode 100644 x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml.ts diff --git a/x-pack/plugins/rule_registry/server/index.ts b/x-pack/plugins/rule_registry/server/index.ts index b287e6a3e4688..5331ab86be982 100644 --- a/x-pack/plugins/rule_registry/server/index.ts +++ b/x-pack/plugins/rule_registry/server/index.ts @@ -32,7 +32,7 @@ export { LifecycleAlertServices, createLifecycleExecutor, } from './utils/create_lifecycle_executor'; -export { createPersistenceRuleTypeFactory } from './utils/create_persistence_rule_type_factory'; +export { createPersistenceRuleTypeWrapper } from './utils/create_persistence_rule_type_wrapper'; export * from './utils/persistence_types'; export type { AlertsClient } from './alert_data_client/alerts_client'; diff --git a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts similarity index 91% rename from x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts rename to x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts index 837d0378703f7..86b6cf72ed1f1 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_factory.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_persistence_rule_type_wrapper.ts @@ -7,9 +7,9 @@ import { ALERT_INSTANCE_ID, VERSION } from '@kbn/rule-data-utils'; import { getCommonAlertFields } from './get_common_alert_fields'; -import { CreatePersistenceRuleTypeFactory } from './persistence_types'; +import { CreatePersistenceRuleTypeWrapper } from './persistence_types'; -export const createPersistenceRuleTypeFactory: CreatePersistenceRuleTypeFactory = +export const createPersistenceRuleTypeWrapper: CreatePersistenceRuleTypeWrapper = ({ logger, ruleDataClient }) => (type) => { return { diff --git a/x-pack/plugins/rule_registry/server/utils/persistence_types.ts b/x-pack/plugins/rule_registry/server/utils/persistence_types.ts index 11607909a2e0f..5da05d9956d7f 100644 --- a/x-pack/plugins/rule_registry/server/utils/persistence_types.ts +++ b/x-pack/plugins/rule_registry/server/utils/persistence_types.ts @@ -8,43 +8,59 @@ import { ApiResponse } from '@elastic/elasticsearch'; import { BulkResponse } from '@elastic/elasticsearch/api/types'; import { Logger } from '@kbn/logging'; -import { ESSearchRequest } from 'src/core/types/elasticsearch'; import { + AlertExecutorOptions, AlertInstanceContext, AlertInstanceState, + AlertType, AlertTypeParams, AlertTypeState, } from '../../../alerting/server'; +import { WithoutReservedActionGroups } from '../../../alerting/common'; import { IRuleDataClient } from '../rule_data_client'; -import { AlertTypeWithExecutor } from '../types'; -export type PersistenceAlertService< - TState extends AlertInstanceState = never, - TContext extends AlertInstanceContext = never, - TActionGroupIds extends string = never -> = ( +export type PersistenceAlertService = ( alerts: Array<{ id: string; fields: Record; }>, refresh: boolean | 'wait_for' -) => Promise>; +) => Promise | undefined>; -export type PersistenceAlertQueryService = ( - query: ESSearchRequest -) => Promise>>; -export interface PersistenceServices { - alertWithPersistence: PersistenceAlertService; +export interface PersistenceServices { + alertWithPersistence: PersistenceAlertService; } -export type CreatePersistenceRuleTypeFactory = (options: { +export type PersistenceAlertType< + TParams extends AlertTypeParams, + TState extends AlertTypeState, + TInstanceContext extends AlertInstanceContext = {}, + TActionGroupIds extends string = never +> = Omit< + AlertType, + 'executor' +> & { + executor: ( + options: AlertExecutorOptions< + TParams, + TState, + AlertInstanceState, + TInstanceContext, + WithoutReservedActionGroups + > & { + services: PersistenceServices; + } + ) => Promise; +}; + +export type CreatePersistenceRuleTypeWrapper = (options: { ruleDataClient: IRuleDataClient; logger: Logger; }) => < - TState extends AlertTypeState, TParams extends AlertTypeParams, - TServices extends PersistenceServices, - TAlertInstanceContext extends AlertInstanceContext = {} + TState extends AlertTypeState, + TInstanceContext extends AlertInstanceContext = {}, + TActionGroupIds extends string = never >( - type: AlertTypeWithExecutor -) => AlertTypeWithExecutor; + type: PersistenceAlertType +) => AlertType; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts similarity index 92% rename from x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts rename to x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 9ea36abe997c3..b037e572f21b7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -14,7 +14,7 @@ import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { ListArray } from '@kbn/securitysolution-io-ts-list-types'; import { toError } from '@kbn/securitysolution-list-api'; -import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server'; +import { createPersistenceRuleTypeWrapper } from '../../../../../rule_registry/server'; import { buildRuleMessageFactory } from './factories/build_rule_message_factory'; import { checkPrivilegesFromEsClient, @@ -22,10 +22,14 @@ import { getRuleRangeTuples, hasReadIndexPrivileges, hasTimestampFields, - isMachineLearningParams, + isEqlParams, + isQueryParams, + isSavedQueryParams, + isThreatParams, + isThresholdParams, } from '../signals/utils'; import { DEFAULT_MAX_SIGNALS, DEFAULT_SEARCH_AFTER_PAGE_SIZE } from '../../../../common/constants'; -import { CreateSecurityRuleTypeFactory } from './types'; +import { CreateSecurityRuleTypeWrapper } from './types'; import { getListClient } from './utils/get_list_client'; import { NotificationRuleTypeParams, @@ -37,13 +41,14 @@ import { bulkCreateFactory, wrapHitsFactory, wrapSequencesFactory } from './fact import { RuleExecutionLogClient } from '../rule_execution_log/rule_execution_log_client'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; +import { AlertAttributes } from '../signals/types'; /* eslint-disable complexity */ -export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = +export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ({ lists, logger, config, ruleDataClient, eventLogService }) => (type) => { const { alertIgnoreFields: ignoreFields, alertMergeStrategy: mergeStrategy } = config; - const persistenceRuleType = createPersistenceRuleTypeFactory({ ruleDataClient, logger }); + const persistenceRuleType = createPersistenceRuleTypeWrapper({ ruleDataClient, logger }); return persistenceRuleType({ ...type, async executor(options) { @@ -69,7 +74,10 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = eventLogService, underlyingClient: config.ruleExecutionLog.underlyingClient, }); - const ruleSO = await savedObjectsClient.get('alert', alertId); + const ruleSO = await savedObjectsClient.get>( + 'alert', + alertId + ); const { actions, @@ -107,7 +115,16 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = // move this collection of lines into a function in utils // so that we can use it in create rules route, bulk, etc. try { - if (!isMachineLearningParams(params)) { + // Typescript 4.1.3 can't figure out that `!isMachineLearningParams(params)` also excludes the only rule type + // of rule params that doesn't include `params.index`, but Typescript 4.3.5 does compute the stricter type correctly. + // When we update Typescript to >= 4.3.5, we can replace this logic with `!isMachineLearningParams(params)` again. + if ( + isEqlParams(params) || + isThresholdParams(params) || + isQueryParams(params) || + isSavedQueryParams(params) || + isThreatParams(params) + ) { const index = params.index; const hasTimestampOverride = !!timestampOverride; @@ -254,7 +271,7 @@ export const createSecurityRuleTypeFactory: CreateSecurityRuleTypeFactory = createdSignals, createdSignalsCount: createdSignals.length, errors: result.errors.concat(runResult.errors), - lastLookbackDate: runResult.lastLookbackDate, + lastLookbackDate: runResult.lastLookBackDate, searchAfterTimes: result.searchAfterTimes.concat(runResult.searchAfterTimes), state: runState, success: result.success && runResult.success, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts index 43860d396ac5d..486a692ba29f4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.test.ts @@ -12,6 +12,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { createEqlAlertType } from './create_eql_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { getEqlRuleParams } from '../../schemas/rule_schemas.mock'; +import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../rule_execution_log/rule_execution_log_client'); @@ -23,15 +24,20 @@ describe('Event correlation alerts', () => { query: 'any where false', }; const { services, dependencies, executor } = createRuleTypeMocks('eql', params); - const eqlAlertType = createEqlAlertType({ - experimentalFeatures: allowedExperimentalValues, + const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ lists: dependencies.lists, logger: dependencies.logger, config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, eventLogService: dependencies.eventLogService, - version: '1.0.0', }); + const eqlAlertType = securityRuleTypeWrapper( + createEqlAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(eqlAlertType); services.scopedClusterClient.asCurrentUser.search.mockReturnValue( elasticsearchClientMock.createSuccessTransportRequestPromise({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts index 9324b469bf644..f09f013301dea 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/eql/create_eql_alert_type.ts @@ -6,24 +6,16 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; -import { PersistenceServices } from '../../../../../../rule_registry/server'; import { EQL_RULE_TYPE_ID } from '../../../../../common/constants'; -import { eqlRuleParams, EqlRuleParams } from '../../schemas/rule_schemas'; +import { EqlRuleParams, eqlRuleParams } from '../../schemas/rule_schemas'; import { eqlExecutor } from '../../signals/executors/eql'; -import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; -import { CreateRuleOptions } from '../types'; +import { CreateRuleOptions, SecurityAlertType } from '../types'; -export const createEqlAlertType = (createOptions: CreateRuleOptions) => { - const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = - createOptions; - const createSecurityRuleType = createSecurityRuleTypeFactory({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - }); - return createSecurityRuleType({ +export const createEqlAlertType = ( + createOptions: CreateRuleOptions +): SecurityAlertType => { + const { experimentalFeatures, logger, version } = createOptions; + return { id: EQL_RULE_TYPE_ID, name: 'Event Correlation Rule', validate: { @@ -83,5 +75,5 @@ export const createEqlAlertType = (createOptions: CreateRuleOptions) => { }); return { ...result, state }; }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts index af0a8a27f2b25..3c12adbca3e44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/factories/bulk_create_factory.ts @@ -16,7 +16,6 @@ import { BuildRuleMessage } from '../../signals/rule_messages'; import { errorAggregator, makeFloatString } from '../../signals/utils'; import { RefreshTypes } from '../../types'; import { PersistenceAlertService } from '../../../../../../rule_registry/server'; -import { AlertInstanceContext } from '../../../../../../alerting/common'; export interface GenericBulkCreateResponse { success: boolean; @@ -27,9 +26,9 @@ export interface GenericBulkCreateResponse { } export const bulkCreateFactory = - ( + ( logger: Logger, - alertWithPersistence: PersistenceAlertService, + alertWithPersistence: PersistenceAlertService, buildRuleMessage: BuildRuleMessage, refreshForBulkCreate: RefreshTypes ) => @@ -61,6 +60,19 @@ export const bulkCreateFactory = `individual bulk process time took: ${makeFloatString(end - start)} milliseconds` ) ); + + if (response == null) { + return { + errors: [ + 'alertWithPersistence returned undefined response. Alerts as Data write flag may be disabled.', + ], + success: false, + bulkCreateDuration: makeFloatString(end - start), + createdItemsCount: 0, + createdItems: [], + }; + } + logger.debug( buildRuleMessage(`took property says bulk took: ${response.body.took} milliseconds`) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts index 3db4f5686abdc..576e409378213 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.test.ts @@ -16,6 +16,7 @@ import { createIndicatorMatchAlertType } from './create_indicator_match_alert_ty import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { CountResponse } from 'kibana/server'; import { RuleParams } from '../../schemas/rule_schemas'; +import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../utils/get_list_client', () => ({ @@ -50,18 +51,23 @@ describe('Indicator Match Alerts', () => { to: 'now', type: 'threat_match', }; + const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params); + const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ + lists: dependencies.lists, + logger: dependencies.logger, + config: createMockConfig(), + ruleDataClient: dependencies.ruleDataClient, + eventLogService: dependencies.eventLogService, + }); it('does not send an alert when no events found', async () => { - const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params); - const indicatorMatchAlertType = createIndicatorMatchAlertType({ - experimentalFeatures: allowedExperimentalValues, - lists: dependencies.lists, - logger: dependencies.logger, - config: createMockConfig(), - ruleDataClient: dependencies.ruleDataClient, - eventLogService: dependencies.eventLogService, - version: '1.0.0', - }); + const indicatorMatchAlertType = securityRuleTypeWrapper( + createIndicatorMatchAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(indicatorMatchAlertType); @@ -92,16 +98,13 @@ describe('Indicator Match Alerts', () => { }); it('does not send an alert when no enrichments are found', async () => { - const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params); - const indicatorMatchAlertType = createIndicatorMatchAlertType({ - experimentalFeatures: allowedExperimentalValues, - lists: dependencies.lists, - logger: dependencies.logger, - config: createMockConfig(), - ruleDataClient: dependencies.ruleDataClient, - eventLogService: dependencies.eventLogService, - version: '1.0.0', - }); + const indicatorMatchAlertType = securityRuleTypeWrapper( + createIndicatorMatchAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(indicatorMatchAlertType); @@ -130,16 +133,13 @@ describe('Indicator Match Alerts', () => { }); it('sends an alert when enrichments are found', async () => { - const { services, dependencies, executor } = createRuleTypeMocks('threat_match', params); - const indicatorMatchAlertType = createIndicatorMatchAlertType({ - experimentalFeatures: allowedExperimentalValues, - lists: dependencies.lists, - logger: dependencies.logger, - config: createMockConfig(), - ruleDataClient: dependencies.ruleDataClient, - eventLogService: dependencies.eventLogService, - version: '1.0.0', - }); + const indicatorMatchAlertType = securityRuleTypeWrapper( + createIndicatorMatchAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(indicatorMatchAlertType); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts index c30fdd7d99c2a..ee0688840811a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/indicator_match/create_indicator_match_alert_type.ts @@ -6,24 +6,16 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; -import { PersistenceServices } from '../../../../../../rule_registry/server'; import { INDICATOR_RULE_TYPE_ID } from '../../../../../common/constants'; -import { threatRuleParams, ThreatRuleParams } from '../../schemas/rule_schemas'; +import { ThreatRuleParams, threatRuleParams } from '../../schemas/rule_schemas'; import { threatMatchExecutor } from '../../signals/executors/threat_match'; -import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; -import { CreateRuleOptions } from '../types'; +import { CreateRuleOptions, SecurityAlertType } from '../types'; -export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) => { - const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = - createOptions; - const createSecurityRuleType = createSecurityRuleTypeFactory({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - }); - return createSecurityRuleType({ +export const createIndicatorMatchAlertType = ( + createOptions: CreateRuleOptions +): SecurityAlertType => { + const { experimentalFeatures, logger, version } = createOptions; + return { id: INDICATOR_RULE_TYPE_ID, name: 'Indicator Match Rule', validate: { @@ -86,5 +78,5 @@ export const createIndicatorMatchAlertType = (createOptions: CreateRuleOptions) }); return { ...result, state }; }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml.ts deleted file mode 100644 index e0ad333b76a24..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* - -import { schema } from '@kbn/config-schema'; -import { KibanaRequest, Logger } from 'src/core/server'; -import { SavedObject } from 'src/core/types'; - -import { buildEsQuery, IIndexPattern } from '../../../../../../../src/plugins/data/common'; - -import { createPersistenceRuleTypeFactory } from '../../../../../rule_registry/server'; -import { ML_RULE_TYPE_ID } from '../../../../common/constants'; -import { SecurityRuleRegistry } from '../../../plugin'; - -const createSecurityMlRuleType = createPersistenceRuleTypeFactory(); - -import { - AlertInstanceContext, - AlertInstanceState, - AlertServices, -} from '../../../../../alerting/server'; -import { ListClient } from '../../../../../lists/server'; -import { isJobStarted } from '../../../../common/machine_learning/helpers'; -import { ExceptionListItemSchema } from '../../../../common/shared_imports'; -import { SetupPlugins } from '../../../plugin'; -import { RefreshTypes } from '../types'; -import { bulkCreateMlSignals } from '../signals/bulk_create_ml_signals'; -import { filterEventsAgainstList } from '../signals/filters/filter_events_against_list'; -import { findMlSignals } from '../signals/find_ml_signals'; -import { BuildRuleMessage } from '../signals/rule_messages'; -import { RuleStatusService } from '../signals/rule_status_service'; -import { MachineLearningRuleAttributes } from '../signals/types'; -import { createErrorsFromShard, createSearchAfterReturnType, mergeReturns } from '../signals/utils'; - -export const mlAlertType = createSecurityMlRuleType({ - id: ML_RULE_TYPE_ID, - name: 'Machine Learning Rule', - validate: { - params: schema.object({ - indexPatterns: schema.arrayOf(schema.string()), - customQuery: schema.string(), - }), - }, - actionGroups: [ - { - id: 'default', - name: 'Default', - }, - ], - defaultActionGroupId: 'default', - actionVariables: { - context: [{ name: 'server', description: 'the server' }], - }, - minimumLicenseRequired: 'basic', - isExportable: false, - producer: 'security-solution', - async executor({ - services: { alertWithPersistence, findAlerts }, - params: { indexPatterns, customQuery }, - }) { - return { - lastChecked: new Date(), - }; - }, -}); -*/ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts index bffc20c3df1e3..b7a099b10e275 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts @@ -14,6 +14,7 @@ import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { createMlAlertType } from './create_ml_alert_type'; import { RuleParams } from '../../schemas/rule_schemas'; +import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../signals/bulk_create_ml_signals'); @@ -94,16 +95,21 @@ describe('Machine Learning Alerts', () => { }, ]); const { dependencies, executor } = createRuleTypeMocks('machine_learning', params); - const mlAlertType = createMlAlertType({ - experimentalFeatures: allowedExperimentalValues, + const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ lists: dependencies.lists, logger: dependencies.logger, config: createMockConfig(), - ml: mlMock, ruleDataClient: dependencies.ruleDataClient, eventLogService: dependencies.eventLogService, - version: '1.0.0', }); + const mlAlertType = securityRuleTypeWrapper( + createMlAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + ml: mlMock, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(mlAlertType); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts index ac2d3f14831a4..756757c7c9956 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -6,23 +6,16 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; -import { PersistenceServices } from '../../../../../../rule_registry/server'; import { ML_RULE_TYPE_ID } from '../../../../../common/constants'; -import { machineLearningRuleParams, MachineLearningRuleParams } from '../../schemas/rule_schemas'; +import { MachineLearningRuleParams, machineLearningRuleParams } from '../../schemas/rule_schemas'; import { mlExecutor } from '../../signals/executors/ml'; -import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; -import { CreateRuleOptions } from '../types'; +import { CreateRuleOptions, SecurityAlertType } from '../types'; -export const createMlAlertType = (createOptions: CreateRuleOptions) => { - const { lists, logger, config, ml, ruleDataClient, eventLogService } = createOptions; - const createSecurityRuleType = createSecurityRuleTypeFactory({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - }); - return createSecurityRuleType({ +export const createMlAlertType = ( + createOptions: CreateRuleOptions +): SecurityAlertType => { + const { logger, ml } = createOptions; + return { id: ML_RULE_TYPE_ID, name: 'Machine Learning Rule', validate: { @@ -81,5 +74,5 @@ export const createMlAlertType = (createOptions: CreateRuleOptions) => { }); return { ...result, state }; }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts index 4fdeac8047b1d..638c40c13cfe2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.test.ts @@ -14,6 +14,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { sampleDocNoSortId } from '../../signals/__mocks__/es_results'; import { createQueryAlertType } from './create_query_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../utils/get_list_client', () => ({ @@ -26,17 +27,22 @@ jest.mock('../utils/get_list_client', () => ({ jest.mock('../../rule_execution_log/rule_execution_log_client'); describe('Custom Query Alerts', () => { + const { services, dependencies, executor } = createRuleTypeMocks(); + const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ + lists: dependencies.lists, + logger: dependencies.logger, + config: createMockConfig(), + ruleDataClient: dependencies.ruleDataClient, + eventLogService: dependencies.eventLogService, + }); it('does not send an alert when no events found', async () => { - const { services, dependencies, executor } = createRuleTypeMocks(); - const queryAlertType = createQueryAlertType({ - experimentalFeatures: allowedExperimentalValues, - lists: dependencies.lists, - logger: dependencies.logger, - config: createMockConfig(), - ruleDataClient: dependencies.ruleDataClient, - eventLogService: dependencies.eventLogService, - version: '1.0.0', - }); + const queryAlertType = securityRuleTypeWrapper( + createQueryAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(queryAlertType); @@ -74,16 +80,13 @@ describe('Custom Query Alerts', () => { }); it('sends a properly formatted alert when events are found', async () => { - const { services, dependencies, executor } = createRuleTypeMocks(); - const queryAlertType = createQueryAlertType({ - experimentalFeatures: allowedExperimentalValues, - lists: dependencies.lists, - logger: dependencies.logger, - config: createMockConfig(), - ruleDataClient: dependencies.ruleDataClient, - eventLogService: dependencies.eventLogService, - version: '1.0.0', - }); + const queryAlertType = securityRuleTypeWrapper( + createQueryAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); dependencies.alerting.registerType(queryAlertType); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts index 469c237112dcb..aa2b25c422221 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/query/create_query_alert_type.ts @@ -6,24 +6,16 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; -import { PersistenceServices } from '../../../../../../rule_registry/server'; import { QUERY_RULE_TYPE_ID } from '../../../../../common/constants'; -import { queryRuleParams, QueryRuleParams } from '../../schemas/rule_schemas'; +import { QueryRuleParams, queryRuleParams } from '../../schemas/rule_schemas'; import { queryExecutor } from '../../signals/executors/query'; -import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; -import { CreateRuleOptions } from '../types'; +import { CreateRuleOptions, SecurityAlertType } from '../types'; -export const createQueryAlertType = (createOptions: CreateRuleOptions) => { - const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = - createOptions; - const createSecurityRuleType = createSecurityRuleTypeFactory({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - }); - return createSecurityRuleType({ +export const createQueryAlertType = ( + createOptions: CreateRuleOptions +): SecurityAlertType => { + const { experimentalFeatures, logger, version } = createOptions; + return { id: QUERY_RULE_TYPE_ID, name: 'Custom Query Rule', validate: { @@ -86,5 +78,5 @@ export const createQueryAlertType = (createOptions: CreateRuleOptions) => { }); return { ...result, state }; }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts index aff57dbdf3cd4..093ec0af78f59 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.test.ts @@ -9,6 +9,7 @@ import { allowedExperimentalValues } from '../../../../../common/experimental_fe import { createThresholdAlertType } from './create_threshold_alert_type'; import { createRuleTypeMocks } from '../__mocks__/rule_type'; import { getThresholdRuleParams } from '../../schemas/rule_schemas.mock'; +import { createSecurityRuleTypeWrapper } from '../create_security_rule_type_wrapper'; import { createMockConfig } from '../../routes/__mocks__'; jest.mock('../../rule_execution_log/rule_execution_log_client'); @@ -17,16 +18,21 @@ describe('Threshold Alerts', () => { it('does not send an alert when no events found', async () => { const params = getThresholdRuleParams(); const { dependencies, executor } = createRuleTypeMocks('threshold', params); - const thresholdAlertTpe = createThresholdAlertType({ - experimentalFeatures: allowedExperimentalValues, + const securityRuleTypeWrapper = createSecurityRuleTypeWrapper({ lists: dependencies.lists, logger: dependencies.logger, config: createMockConfig(), ruleDataClient: dependencies.ruleDataClient, eventLogService: dependencies.eventLogService, - version: '1.0.0', }); - dependencies.alerting.registerType(thresholdAlertTpe); + const thresholdAlertType = securityRuleTypeWrapper( + createThresholdAlertType({ + experimentalFeatures: allowedExperimentalValues, + logger: dependencies.logger, + version: '1.0.0', + }) + ); + dependencies.alerting.registerType(thresholdAlertType); await executor({ params }); expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts index 789e4525c58ab..2b3c1c0a8965b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/threshold/create_threshold_alert_type.ts @@ -6,26 +6,17 @@ */ import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; - -import { PersistenceServices } from '../../../../../../rule_registry/server'; import { THRESHOLD_RULE_TYPE_ID } from '../../../../../common/constants'; import { thresholdRuleParams, ThresholdRuleParams } from '../../schemas/rule_schemas'; import { thresholdExecutor } from '../../signals/executors/threshold'; import { ThresholdAlertState } from '../../signals/types'; -import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; -import { CreateRuleOptions } from '../types'; +import { CreateRuleOptions, SecurityAlertType } from '../types'; -export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { - const { experimentalFeatures, lists, logger, config, ruleDataClient, version, eventLogService } = - createOptions; - const createSecurityRuleType = createSecurityRuleTypeFactory({ - lists, - logger, - config, - ruleDataClient, - eventLogService, - }); - return createSecurityRuleType({ +export const createThresholdAlertType = ( + createOptions: CreateRuleOptions +): SecurityAlertType => { + const { experimentalFeatures, logger, version } = createOptions; + return { id: THRESHOLD_RULE_TYPE_ID, name: 'Threshold Rule', validate: { @@ -63,8 +54,6 @@ export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { state, } = execOptions; - // console.log(JSON.stringify(state)); - const result = await thresholdExecutor({ buildRuleMessage, bulkCreate, @@ -82,5 +71,5 @@ export const createThresholdAlertType = (createOptions: CreateRuleOptions) => { return result; }, - }); + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index c94339da03b93..393cb00939b24 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -11,28 +11,30 @@ import { SearchHit } from '@elastic/elasticsearch/api/types'; import { Logger } from '@kbn/logging'; import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; +import { AlertExecutorOptions, AlertType } from '../../../../../alerting/server'; import { SavedObject } from '../../../../../../../src/core/server'; import { AlertInstanceContext, AlertInstanceState, - AlertTypeParams, AlertTypeState, + WithoutReservedActionGroups, } from '../../../../../alerting/common'; -import { AlertType } from '../../../../../alerting/server'; import { ListClient } from '../../../../../lists/server'; import { TechnicalRuleFieldMap } from '../../../../../rule_registry/common/assets/field_maps/technical_rule_field_map'; import { TypeOfFieldMap } from '../../../../../rule_registry/common/field_map'; -import { - AlertTypeWithExecutor, - PersistenceServices, - IRuleDataClient, -} from '../../../../../rule_registry/server'; +import { PersistenceServices, IRuleDataClient } from '../../../../../rule_registry/server'; import { BaseHit } from '../../../../common/detection_engine/types'; import { ConfigType } from '../../../config'; import { SetupPlugins } from '../../../plugin'; import { RuleParams } from '../schemas/rule_schemas'; import { BuildRuleMessage } from '../signals/rule_messages'; -import { AlertAttributes, BulkCreate, WrapHits, WrapSequences } from '../signals/types'; +import { + AlertAttributes, + BulkCreate, + SearchAfterAndBulkCreateReturnType, + WrapHits, + WrapSequences, +} from '../signals/types'; import { AlertsFieldMap, RulesFieldMap } from './field_maps'; import { ExperimentalFeatures } from '../../../../common/experimental_features'; import { IEventLogService } from '../../../../../event_log/server'; @@ -50,12 +52,6 @@ export interface SecurityAlertTypeReturnValue { warningMessages: string[]; } -type SimpleAlertType< - TState extends AlertTypeState, - TParams extends AlertTypeParams = {}, - TAlertInstanceContext extends AlertInstanceContext = {} -> = AlertType; - export interface RunOpts { buildRuleMessage: BuildRuleMessage; bulkCreate: BulkCreate; @@ -72,44 +68,42 @@ export interface RunOpts { wrapSequences: WrapSequences; } -export type SecurityAlertTypeExecutor< - TState extends AlertTypeState, - TServices extends PersistenceServices, +export type SecurityAlertType< TParams extends RuleParams, - TAlertInstanceContext extends AlertInstanceContext = {} -> = ( - options: Parameters['executor']>[0] & { - runOpts: RunOpts; - } & { services: TServices } -) => Promise>; - -type SecurityAlertTypeWithExecutor< TState extends AlertTypeState, - TServices extends PersistenceServices, - TParams extends RuleParams, - TAlertInstanceContext extends AlertInstanceContext = {} + TInstanceContext extends AlertInstanceContext = {}, + TActionGroupIds extends string = never > = Omit< - AlertType, + AlertType, 'executor' > & { - executor: SecurityAlertTypeExecutor; + executor: ( + options: AlertExecutorOptions< + TParams, + TState, + AlertInstanceState, + TInstanceContext, + WithoutReservedActionGroups + > & { + services: PersistenceServices; + runOpts: RunOpts; + } + ) => Promise; }; -export type CreateSecurityRuleTypeFactory = (options: { +export type CreateSecurityRuleTypeWrapper = (options: { lists: SetupPlugins['lists']; logger: Logger; config: ConfigType; ruleDataClient: IRuleDataClient; eventLogService: IEventLogService; }) => < - TParams extends RuleParams & { index?: string[] | undefined }, - TAlertInstanceContext extends AlertInstanceContext, - TServices extends PersistenceServices, - TState extends AlertTypeState + TParams extends RuleParams, + TState extends AlertTypeState, + TInstanceContext extends AlertInstanceContext = {} >( - type: SecurityAlertTypeWithExecutor - // eslint-disable-next-line @typescript-eslint/no-explicit-any -) => AlertTypeWithExecutor; + type: SecurityAlertType +) => AlertType; export type RACAlertSignal = TypeOfFieldMap & TypeOfFieldMap; export type RACAlert = Exclude< @@ -124,11 +118,7 @@ export type WrappedRACAlert = BaseHit; export interface CreateRuleOptions { experimentalFeatures: ExperimentalFeatures; - lists: SetupPlugins['lists']; logger: Logger; - config: ConfigType; ml?: SetupPlugins['ml']; - ruleDataClient: IRuleDataClient; version: string; - eventLogService: IEventLogService; } diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index f0a91f8b06c00..d54ed18af01e3 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -109,6 +109,7 @@ import { ctiFieldMap } from './lib/detection_engine/rule_types/field_maps/cti'; import { legacyRulesNotificationAlertType } from './lib/detection_engine/notifications/legacy_rules_notification_alert_type'; // eslint-disable-next-line no-restricted-imports import { legacyIsNotificationAlertExecutor } from './lib/detection_engine/notifications/legacy_types'; +import { createSecurityRuleTypeWrapper } from './lib/detection_engine/rule_types/create_security_rule_type_wrapper'; import { IEventLogClientService, IEventLogService } from '../../event_log/server'; import { registerEventLogProvider } from './lib/detection_engine/rule_execution_log/event_log_adapter/register_event_log_provider'; @@ -268,20 +269,34 @@ export class Plugin implements IPlugin Date: Tue, 12 Oct 2021 14:47:00 -0400 Subject: [PATCH 091/287] [Security Solution][Endpoint][TA] Trusted Apps license downgrade experience (#113048) --- ...-plugin-core-public.doclinksstart.links.md | 3 + ...kibana-plugin-core-public.doclinksstart.md | 3 +- .../public/doc_links/doc_links_service.ts | 6 + src/core/public/public.api.md | 3 + .../__mocks__/use_endpoint_privileges.ts | 1 + .../components/user_privileges/index.tsx | 7 +- .../use_endpoint_privileges.test.ts | 26 ++- .../use_endpoint_privileges.ts | 6 +- .../alerts/use_alerts_privileges.test.tsx | 7 +- .../search_exceptions/search_exceptions.tsx | 4 +- .../view/event_filters_list_page.test.tsx | 1 + .../host_isolation_exceptions_list.test.tsx | 1 + .../policy_trusted_apps_flyout.test.tsx | 1 + .../components/create_trusted_app_flyout.tsx | 61 ++++++- .../create_trusted_app_form.test.tsx | 68 ++++++++ .../components/create_trusted_app_form.tsx | 32 +++- .../effected_policy_select.test.tsx | 1 + .../effected_policy_select.tsx | 11 +- .../view/trusted_app_deletion_dialog.tsx | 16 +- .../view/trusted_apps_page.test.tsx | 152 ++++++++++++++---- 20 files changed, 345 insertions(+), 65 deletions(-) diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index c8ccdfeedb83f..5871d7df6a7c5 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -145,6 +145,9 @@ readonly links: { readonly networkMap: string; readonly troubleshootGaps: string; }; + readonly securitySolution: { + readonly trustedApps: string; + }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 04c2495cf3f1d..a4e842c317256 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,4 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | - +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: string;
};
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly securitySolution: {
readonly trustedApps: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: string;
}>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 01108298adc99..a4ca5722c6aca 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -233,6 +233,9 @@ export class DocLinksService { networkMap: `${SECURITY_SOLUTION_DOCS}conf-map-ui.html`, troubleshootGaps: `${SECURITY_SOLUTION_DOCS}alerts-ui-monitor.html#troubleshoot-gaps`, }, + securitySolution: { + trustedApps: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/trusted-apps-ov.html`, + }, query: { eql: `${ELASTICSEARCH_DOCS}eql.html`, kueryQuerySyntax: `${KIBANA_DOCS}kuery-query.html`, @@ -641,6 +644,9 @@ export interface DocLinksStart { readonly networkMap: string; readonly troubleshootGaps: string; }; + readonly securitySolution: { + readonly trustedApps: string; + }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 45b7e3bdc02b5..324066764768d 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -614,6 +614,9 @@ export interface DocLinksStart { readonly networkMap: string; readonly troubleshootGaps: string; }; + readonly securitySolution: { + readonly trustedApps: string; + }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts index 80cf11fecd847..80ca534534187 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/__mocks__/use_endpoint_privileges.ts @@ -12,6 +12,7 @@ export const useEndpointPrivileges = jest.fn(() => { loading: false, canAccessFleet: true, canAccessEndpointManagement: true, + isPlatinumPlus: true, }; return endpointPrivilegesMock; }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index 028473f5c2001..437d27278102b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -24,7 +24,12 @@ export interface UserPrivilegesState { export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, - endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, + endpointPrivileges: { + loading: true, + canAccessEndpointManagement: false, + canAccessFleet: false, + isPlatinumPlus: false, + }, kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts index a05d1ac8d3588..82443e913499b 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.test.ts @@ -5,15 +5,27 @@ * 2.0. */ -import { renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; +import { act, renderHook, RenderHookResult, RenderResult } from '@testing-library/react-hooks'; import { useHttp, useCurrentUser } from '../../lib/kibana'; import { EndpointPrivileges, useEndpointPrivileges } from './use_endpoint_privileges'; import { securityMock } from '../../../../../security/public/mocks'; import { appRoutesService } from '../../../../../fleet/common'; import { AuthenticatedUser } from '../../../../../security/common'; +import { licenseService } from '../../hooks/use_license'; import { fleetGetCheckPermissionsHttpMock } from '../../../management/pages/mocks'; jest.mock('../../lib/kibana'); +jest.mock('../../hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); describe('When using useEndpointPrivileges hook', () => { let authenticatedUser: AuthenticatedUser; @@ -33,6 +45,7 @@ describe('When using useEndpointPrivileges hook', () => { fleetApiMock = fleetGetCheckPermissionsHttpMock( useHttp() as Parameters[0] ); + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); render = () => { const hookRenderResponse = renderHook(() => useEndpointPrivileges()); @@ -60,6 +73,7 @@ describe('When using useEndpointPrivileges hook', () => { canAccessEndpointManagement: false, canAccessFleet: false, loading: true, + isPlatinumPlus: true, }); // Make user service available @@ -69,15 +83,19 @@ describe('When using useEndpointPrivileges hook', () => { canAccessEndpointManagement: false, canAccessFleet: false, loading: true, + isPlatinumPlus: true, }); // Release the API response - releaseApiResponse!(); - await fleetApiMock.waitForApi(); + await act(async () => { + fleetApiMock.waitForApi(); + releaseApiResponse!(); + }); expect(result.current).toEqual({ canAccessEndpointManagement: true, canAccessFleet: true, loading: false, + isPlatinumPlus: true, }); }); @@ -99,6 +117,7 @@ describe('When using useEndpointPrivileges hook', () => { canAccessEndpointManagement: false, canAccessFleet: true, // this is only true here because I did not adjust the API mock loading: false, + isPlatinumPlus: true, }); }); @@ -115,6 +134,7 @@ describe('When using useEndpointPrivileges hook', () => { canAccessEndpointManagement: false, canAccessFleet: false, loading: false, + isPlatinumPlus: true, }); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts index b8db0c5c0fbc9..315935104d107 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/use_endpoint_privileges.ts @@ -8,6 +8,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useCurrentUser, useHttp } from '../../lib/kibana'; import { appRoutesService, CheckPermissionsResponse } from '../../../../../fleet/common'; +import { useLicense } from '../../hooks/use_license'; export interface EndpointPrivileges { loading: boolean; @@ -15,6 +16,7 @@ export interface EndpointPrivileges { canAccessFleet: boolean; /** If user has permissions to access Endpoint management (includes check to ensure they also have access to fleet) */ canAccessEndpointManagement: boolean; + isPlatinumPlus: boolean; } /** @@ -27,6 +29,7 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { const http = useHttp(); const user = useCurrentUser(); const isMounted = useRef(true); + const license = useLicense(); const [canAccessFleet, setCanAccessFleet] = useState(false); const [fleetCheckDone, setFleetCheckDone] = useState(false); @@ -62,8 +65,9 @@ export const useEndpointPrivileges = (): EndpointPrivileges => { loading: !fleetCheckDone || !user, canAccessFleet, canAccessEndpointManagement: canAccessFleet && isSuperUser, + isPlatinumPlus: license.isPlatinumPlus(), }; - }, [canAccessFleet, fleetCheckDone, isSuperUser, user]); + }, [canAccessFleet, fleetCheckDone, isSuperUser, user, license]); // Capture if component is unmounted useEffect( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index cbab24835c1ac..40894c1d01929 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -86,7 +86,12 @@ const userPrivilegesInitial: ReturnType = { result: undefined, error: undefined, }, - endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, + endpointPrivileges: { + loading: true, + canAccessEndpointManagement: false, + canAccessFleet: false, + isPlatinumPlus: true, + }, kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index a77a2a41038d7..2b7b2e6b66884 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiButton } from '@elastic/e import { i18n } from '@kbn/i18n'; import { PolicySelectionItem, PoliciesSelector } from '../policies_selector'; import { ImmutableArray, PolicyData } from '../../../../common/endpoint/types'; +import { useEndpointPrivileges } from '../../../common/components/user_privileges/use_endpoint_privileges'; export interface SearchExceptionsProps { defaultValue?: string; @@ -31,6 +32,7 @@ export const SearchExceptions = memo( defaultIncludedPolicies, defaultExcludedPolicies, }) => { + const { isPlatinumPlus } = useEndpointPrivileges(); const [query, setQuery] = useState(defaultValue); const [includedPolicies, setIncludedPolicies] = useState(defaultIncludedPolicies || ''); const [excludedPolicies, setExcludedPolicies] = useState(defaultExcludedPolicies || ''); @@ -88,7 +90,7 @@ export const SearchExceptions = memo( data-test-subj="searchField" /> - {hasPolicyFilter && policyList ? ( + {isPlatinumPlus && hasPolicyFilter && policyList ? ( { let render: () => ReturnType; diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx index ac472fdae4d7b..9de3d83ed8bab 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/host_isolation_exceptions_list.test.tsx @@ -15,6 +15,7 @@ import { isFailedResourceState, isLoadedResourceState } from '../../../state'; import { getHostIsolationExceptionItems } from '../service'; import { HostIsolationExceptionsList } from './host_isolation_exceptions_list'; +jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); jest.mock('../service'); const getHostIsolationExceptionItemsMock = getHostIsolationExceptionItems as jest.Mock; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx index a586c3c9d1b29..b07005908ed1e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/flyout/policy_trusted_apps_flyout.test.tsx @@ -21,6 +21,7 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; jest.mock('../../../../trusted_apps/service'); +jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx index 7abf5d77dd5e9..f72d54aa9e3c9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_flyout.tsx @@ -8,22 +8,25 @@ import { EuiButton, EuiButtonEmpty, + EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiFlyout, EuiFlyoutBody, EuiFlyoutFooter, EuiFlyoutHeader, + EuiLink, EuiSpacer, EuiText, EuiTitle, } from '@elastic/eui'; -import React, { memo, useCallback, useEffect, useMemo } from 'react'; +import React, { memo, useCallback, useEffect, useState, useMemo } from 'react'; import { EuiFlyoutProps } from '@elastic/eui/src/components/flyout/flyout'; import { FormattedMessage } from '@kbn/i18n/react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; import { CreateTrustedAppForm, CreateTrustedAppFormProps } from './create_trusted_app_form'; import { editTrustedAppFetchError, @@ -43,10 +46,14 @@ import { useTrustedAppsSelector } from '../hooks'; import { ABOUT_TRUSTED_APPS, CREATE_TRUSTED_APP_ERROR } from '../translations'; import { defaultNewTrustedApp } from '../../store/builders'; import { getTrustedAppsListPath } from '../../../../common/routing'; -import { useToasts } from '../../../../../common/lib/kibana'; +import { useKibana, useToasts } from '../../../../../common/lib/kibana'; import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; +import { useLicense } from '../../../../../common/hooks/use_license'; +import { isGlobalEffectScope } from '../../state/type_guards'; +import { NewTrustedApp } from '../../../../../../common/endpoint/types'; +import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; -type CreateTrustedAppFlyoutProps = Omit; +export type CreateTrustedAppFlyoutProps = Omit; export const CreateTrustedAppFlyout = memo( ({ onClose, ...flyoutProps }) => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -63,6 +70,9 @@ export const CreateTrustedAppFlyout = memo( const trustedAppFetchError = useTrustedAppsSelector(editTrustedAppFetchError); const formValues = useTrustedAppsSelector(getCreationDialogFormEntry) || defaultNewTrustedApp(); const location = useTrustedAppsSelector(getCurrentLocation); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + const docLinks = useKibana().services.docLinks; + const [isFormDirty, setIsFormDirty] = useState(false); const dataTestSubj = flyoutProps['data-test-subj']; @@ -124,10 +134,28 @@ export const CreateTrustedAppFlyout = memo( type: 'trustedAppCreationDialogFormStateUpdated', payload: { entry: newFormState.item, isValid: newFormState.isValid }, }); + if (_.isEqual(formValues, newFormState.item) === false) { + setIsFormDirty(true); + } }, - [dispatch] + + [dispatch, formValues] + ); + + const isTrustedAppsByPolicyEnabled = useIsExperimentalFeatureEnabled( + 'trustedAppsByPolicyEnabled' ); + const isGlobal = useMemo(() => { + return isGlobalEffectScope((formValues as NewTrustedApp).effectScope); + }, [formValues]); + + const showExpiredLicenseBanner = useMemo(() => { + return ( + isTrustedAppsByPolicyEnabled && !isPlatinumPlus && isEditMode && (!isGlobal || isFormDirty) + ); + }, [isTrustedAppsByPolicyEnabled, isPlatinumPlus, isEditMode, isGlobal, isFormDirty]); + // If there was a failure trying to retrieve the Trusted App for edit item, // then redirect back to the list ++ show toast message. useEffect(() => { @@ -181,7 +209,28 @@ export const CreateTrustedAppFlyout = memo( - + {showExpiredLicenseBanner && ( + + + + + + + )}

@@ -203,6 +252,8 @@ export const CreateTrustedAppFlyout = memo( fullWidth onChange={handleFormOnChange} isInvalid={!!creationErrors} + isEditMode={isEditMode} + isDirty={isFormDirty} error={creationErrorsMessage} policies={policies} trustedApp={formValues} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx index 45eaa2c890a78..d3b4a541bd18d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.test.tsx @@ -24,10 +24,23 @@ import { defaultNewTrustedApp } from '../../store/builders'; import { forceHTMLElementOffsetWidth } from './effected_policy_select/test_utils'; import { EndpointDocGenerator } from '../../../../../../common/endpoint/generate_data'; import { useIsExperimentalFeatureEnabled } from '../../../../../common/hooks/use_experimental_features'; +import { licenseService } from '../../../../../common/hooks/use_license'; jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +jest.mock('../../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + describe('When using the Trusted App Form', () => { const dataTestSubjForForm = 'createForm'; const generator = new EndpointDocGenerator('effected-policy-select'); @@ -112,6 +125,7 @@ describe('When using the Trusted App Form', () => { beforeEach(() => { resetHTMLElementOffsetWidth = forceHTMLElementOffsetWidth(); useIsExperimentalFeatureEnabledMock.mockReturnValue(true); + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); mockedContext = createAppRootMockRenderer(); @@ -120,6 +134,8 @@ describe('When using the Trusted App Form', () => { formProps = { 'data-test-subj': dataTestSubjForForm, trustedApp: latestUpdatedTrustedApp, + isEditMode: false, + isDirty: false, onChange: jest.fn((updates) => { latestUpdatedTrustedApp = updates.item; }), @@ -303,6 +319,58 @@ describe('When using the Trusted App Form', () => { }); }); + describe('the Policy Selection area when the license downgrades to gold or below', () => { + beforeEach(() => { + // select per policy for trusted app + const policy = generator.generatePolicyPackagePolicy(); + policy.name = 'test policy A'; + policy.id = '123'; + + formProps.policies.options = [policy]; + + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'policy', + policies: ['123'], + }; + + formProps.isEditMode = true; + + // downgrade license + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + }); + + it('maintains policy configuration but does not allow the user to edit add/remove individual policies in edit mode', () => { + render(); + const perPolicyButton = renderResult.getByTestId( + `${dataTestSubjForForm}-effectedPolicies-perPolicy` + ) as HTMLButtonElement; + + expect(perPolicyButton.classList.contains('euiButtonGroupButton-isSelected')).toEqual(true); + expect(renderResult.getByTestId('policy-123').getAttribute('aria-disabled')).toEqual('true'); + expect(renderResult.getByTestId('policy-123-checkbox')).toBeChecked(); + }); + it("allows the user to set the trusted app entry to 'Global' in the edit option", () => { + render(); + const globalButtonInput = renderResult.getByTestId('globalPolicy') as HTMLButtonElement; + + reactTestingLibrary.act(() => { + fireEvent.click(globalButtonInput); + }); + + expect(formProps.onChange.mock.calls[0][0].item.effectScope.type).toBe('global'); + }); + it('hides the policy assignment section if the TA is set to global', () => { + (formProps.trustedApp as NewTrustedApp).effectScope = { + type: 'global', + }; + expect(renderResult.queryByTestId(`${dataTestSubjForForm}-effectedPolicies`)).toBeNull(); + }); + it('hides the policy assignment section if the user is adding a new TA', () => { + formProps.isEditMode = false; + expect(renderResult.queryByTestId(`${dataTestSubjForForm}-effectedPolicies`)).toBeNull(); + }); + }); + describe('and the user visits required fields but does not fill them out', () => { beforeEach(() => { render(); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx index 5db9a8557fa10..50485ccde00ad 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/create_trusted_app_form.tsx @@ -48,6 +48,7 @@ import { EffectedPolicySelectProps, } from './effected_policy_select'; import { useTestIdGenerator } from '../../../../components/hooks/use_test_id_generator'; +import { useLicense } from '../../../../../common/hooks/use_license'; const OPERATING_SYSTEMS: readonly OperatingSystem[] = [ OperatingSystem.MAC, @@ -189,6 +190,8 @@ export type CreateTrustedAppFormProps = Pick< > & { /** The trusted app values that will be passed to the form */ trustedApp: MaybeImmutable; + isEditMode: boolean; + isDirty: boolean; onChange: (state: TrustedAppFormState) => void; /** Setting passed on to the EffectedPolicySelect component */ policies: Pick; @@ -196,7 +199,15 @@ export type CreateTrustedAppFormProps = Pick< fullWidth?: boolean; }; export const CreateTrustedAppForm = memo( - ({ fullWidth, onChange, trustedApp: _trustedApp, policies = { options: [] }, ...formProps }) => { + ({ + fullWidth, + isEditMode, + isDirty, + onChange, + trustedApp: _trustedApp, + policies = { options: [] }, + ...formProps + }) => { const trustedApp = _trustedApp as NewTrustedApp; const dataTestSubj = formProps['data-test-subj']; @@ -205,6 +216,16 @@ export const CreateTrustedAppForm = memo( 'trustedAppsByPolicyEnabled' ); + const isPlatinumPlus = useLicense().isPlatinumPlus(); + + const isGlobal = useMemo(() => { + return isGlobalEffectScope(trustedApp.effectScope); + }, [trustedApp]); + + const hideAssignmentSection = useMemo(() => { + return !isPlatinumPlus && (!isEditMode || (isGlobal && !isDirty)); + }, [isEditMode, isGlobal, isDirty, isPlatinumPlus]); + const osOptions: Array> = useMemo( () => OPERATING_SYSTEMS.map((os) => ({ value: os, inputDisplay: OS_TITLES[os] })), [] @@ -213,7 +234,7 @@ export const CreateTrustedAppForm = memo( // We create local state for the list of policies because we want the selected policies to // persist while the user is on the form and possibly toggling between global/non-global const [selectedPolicies, setSelectedPolicies] = useState({ - isGlobal: isGlobalEffectScope(trustedApp.effectScope), + isGlobal, selected: [], }); @@ -406,7 +427,7 @@ export const CreateTrustedAppForm = memo( }, [notifyOfChange, trustedApp]); // Anytime the TrustedApp has an effective scope of `policies`, then ensure that - // those polices are selected in the UI while at teh same time preserving prior + // those polices are selected in the UI while at the same time preserving prior // selections (UX requirement) useEffect(() => { setSelectedPolicies((currentSelection) => { @@ -530,12 +551,13 @@ export const CreateTrustedAppForm = memo( data-test-subj={getTestId('conditionsBuilder')} /> - {isTrustedAppsByPolicyEnabled ? ( + {isTrustedAppsByPolicyEnabled && !hideAssignmentSection ? ( <> { componentProps = { options: [], isGlobal: true, + isPlatinumPlus: true, onChange: handleOnChange, 'data-test-subj': 'test', }; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx index bb620ee5e7c01..e247602060384 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/components/effected_policy_select/effected_policy_select.tsx @@ -56,12 +56,14 @@ export type EffectedPolicySelectProps = Omit< > & { options: PolicyData[]; isGlobal: boolean; + isPlatinumPlus: boolean; onChange: (selection: EffectedPolicySelection) => void; selected?: PolicyData[]; }; export const EffectedPolicySelect = memo( ({ isGlobal, + isPlatinumPlus, onChange, listProps, options, @@ -107,7 +109,8 @@ export const EffectedPolicySelect = memo( id={htmlIdGenerator()()} onChange={NOOP} checked={isPolicySelected.has(policy.id)} - disabled={isGlobal} + disabled={isGlobal || !isPlatinumPlus} + data-test-subj={`policy-${policy.id}-checkbox`} /> ), append: ( @@ -124,11 +127,11 @@ export const EffectedPolicySelect = memo( ), policy, checked: isPolicySelected.has(policy.id) ? 'on' : undefined, - disabled: isGlobal, + disabled: isGlobal || !isPlatinumPlus, 'data-test-subj': `policy-${policy.id}`, })) .sort(({ label: labelA }, { label: labelB }) => labelA.localeCompare(labelB)); - }, [getAppUrl, isGlobal, options, selected]); + }, [getAppUrl, isGlobal, isPlatinumPlus, options, selected]); const handleOnPolicySelectChange = useCallback< Required>['onChange'] @@ -178,7 +181,7 @@ export const EffectedPolicySelect = memo(

{i18n.translate('xpack.securitySolution.trustedApps.assignmentSectionDescription', { defaultMessage: - 'You can assign this trusted application globally across all policies or assign it to specific policies.', + 'Assign this trusted application globally across all policies, or assign it to specific policies.', })}

diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx index 9e76cfd001c97..87c7439c236cc 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_app_deletion_dialog.tsx @@ -24,13 +24,7 @@ import { EuiText, } from '@elastic/eui'; -import { - Immutable, - ImmutableObject, - PolicyEffectScope, - GlobalEffectScope, - TrustedApp, -} from '../../../../../common/endpoint/types'; +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; import { AppAction } from '../../../../common/store/actions'; import { useTrustedAppsSelector } from './hooks'; import { @@ -38,14 +32,10 @@ import { isDeletionDialogOpen, isDeletionInProgress, } from '../store/selectors'; +import { isPolicyEffectScope } from '../state/type_guards'; const CANCEL_SUBJ = 'trustedAppDeletionCancel'; const CONFIRM_SUBJ = 'trustedAppDeletionConfirm'; -const isTrustedAppByPolicy = ( - trustedApp: ImmutableObject -): trustedApp is ImmutableObject => { - return (trustedApp as ImmutableObject).policies !== undefined; -}; const getTranslations = (entry: Immutable | undefined) => ({ title: ( @@ -67,7 +57,7 @@ const getTranslations = (entry: Immutable | undefined) => ({ defaultMessage="Deleting this entry will remove it from {count} associated {count, plural, one {policy} other {policies}}." values={{ count: - entry && isTrustedAppByPolicy(entry.effectScope) + entry && isPolicyEffectScope(entry.effectScope) ? entry.effectScope.policies.length : 'all', }} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx index faeef26032cd5..7ced93abc7fe1 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.test.tsx @@ -34,6 +34,7 @@ import { forceHTMLElementOffsetWidth } from './components/effected_policy_select import { toUpdateTrustedApp } from '../../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { resolvePathVariables } from '../../../../common/utils/resolve_path_variables'; +import { licenseService } from '../../../../common/hooks/use_license'; jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => 'mockId', @@ -43,6 +44,20 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ jest.mock('../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; +jest.mock('../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); + +jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); + describe('When on the Trusted Apps Page', () => { const expectedAboutInfo = 'Add a trusted application to improve performance or alleviate conflicts with other ' + @@ -58,26 +73,7 @@ describe('When on the Trusted Apps Page', () => { const originalScrollTo = window.scrollTo; const act = reactTestingLibrary.act; - const getFakeTrustedApp = (): TrustedApp => ({ - id: '1111-2222-3333-4444', - version: 'abc123', - name: 'one app', - os: OperatingSystem.WINDOWS, - created_at: '2021-01-04T13:55:00.561Z', - created_by: 'me', - updated_at: '2021-01-04T13:55:00.561Z', - updated_by: 'me', - description: 'a good one', - effectScope: { type: 'global' }, - entries: [ - { - field: ConditionEntryField.PATH, - value: 'one/two', - operator: 'included', - type: 'match', - }, - ], - }); + const getFakeTrustedApp = jest.fn(); const createListApiResponse = ( page: number = 1, @@ -137,9 +133,32 @@ describe('When on the Trusted Apps Page', () => { beforeEach(() => { mockedContext = createAppRootMockRenderer(); + getFakeTrustedApp.mockImplementation( + (): TrustedApp => ({ + id: '1111-2222-3333-4444', + version: 'abc123', + name: 'one app', + os: OperatingSystem.WINDOWS, + created_at: '2021-01-04T13:55:00.561Z', + created_by: 'me', + updated_at: '2021-01-04T13:55:00.561Z', + updated_by: 'me', + description: 'a good one', + effectScope: { type: 'global' }, + entries: [ + { + field: ConditionEntryField.PATH, + value: 'one/two', + operator: 'included', + type: 'match', + }, + ], + }) + ); history = mockedContext.history; coreStart = mockedContext.coreStart; + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(true); waitForAction = mockedContext.middlewareSpy.waitForAction; render = () => mockedContext.render(); reactTestingLibrary.act(() => { @@ -178,19 +197,94 @@ describe('When on the Trusted Apps Page', () => { }); describe('and the Grid view is being displayed', () => { - describe('and the edit trusted app button is clicked', () => { - let renderResult: ReturnType; + let renderResult: ReturnType; + + const renderWithListDataAndClickOnEditCard = async () => { + renderResult = await renderWithListData(); + + await act(async () => { + (await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[0].click(); + }); + + act(() => { + fireEvent.click(renderResult.getByTestId('editTrustedAppAction')); + }); + }; + + const renderWithListDataAndClickAddButton = async (): Promise< + ReturnType + > => { + renderResult = await renderWithListData(); + + act(() => { + const addButton = renderResult.getByTestId('trustedAppsListAddButton'); + fireEvent.click(addButton, { button: 1 }); + }); + + // Wait for the policies to be loaded + await act(async () => { + await waitForAction('trustedAppsPoliciesStateChanged', { + validate: (action) => { + return isLoadedResourceState(action.payload); + }, + }); + }); + + return renderResult; + }; + describe('the license is downgraded to gold or below and the user is editing a per policy TA', () => { beforeEach(async () => { - renderResult = await renderWithListData(); + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); - await act(async () => { - (await renderResult.findAllByTestId('trustedAppCard-header-actions-button'))[0].click(); + const originalFakeTrustedAppProvider = getFakeTrustedApp.getMockImplementation(); + getFakeTrustedApp.mockImplementation(() => { + return { + ...originalFakeTrustedAppProvider!(), + effectScope: { + type: 'policy', + policies: ['abc123'], + }, + }; }); + await renderWithListDataAndClickOnEditCard(); + }); + + it('shows a message at the top of the flyout to inform the user their license is expired', () => { + expect( + renderResult.queryByTestId('addTrustedAppFlyout-expired-license-callout') + ).toBeTruthy(); + }); + }); + + describe('the license is downgraded to gold or below and the user is adding a new TA', () => { + beforeEach(async () => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + useIsExperimentalFeatureEnabledMock.mockReturnValue(true); - act(() => { - fireEvent.click(renderResult.getByTestId('editTrustedAppAction')); + const originalFakeTrustedAppProvider = getFakeTrustedApp.getMockImplementation(); + getFakeTrustedApp.mockImplementation(() => { + return { + ...originalFakeTrustedAppProvider!(), + effectScope: { + type: 'policy', + policies: ['abc123'], + }, + }; }); + await renderWithListDataAndClickAddButton(); + }); + it('does not show the expired license message at the top of the flyout', async () => { + expect( + renderResult.queryByTestId('addTrustedAppFlyout-expired-license-callout') + ).toBeNull(); + }); + }); + + describe('and the edit trusted app button is clicked', () => { + beforeEach(async () => { + await renderWithListDataAndClickOnEditCard(); }); it('should persist edit params to url', () => { @@ -281,7 +375,7 @@ describe('When on the Trusted Apps Page', () => { } ); - const renderResult = await renderWithListData(); + renderResult = await renderWithListData(); await reactTestingLibrary.act(async () => { await apiResponseForEditTrustedApp; @@ -314,7 +408,7 @@ describe('When on the Trusted Apps Page', () => { }); it('should retrieve trusted app via API using url `id`', async () => { - const renderResult = await renderAndWaitForGetApi(); + renderResult = await renderAndWaitForGetApi(); expect(coreStart.http.get).toHaveBeenCalledWith(TRUSTED_APP_GET_URI); From 16c049a2d9e834623fc207a27c3e5c8ccbe48197 Mon Sep 17 00:00:00 2001 From: Catherine Liu Date: Tue, 12 Oct 2021 11:53:40 -0700 Subject: [PATCH 092/287] [Canvas] Toolbar UI Updates (#113329) --- .../application/top_nav/dashboard_top_nav.tsx | 2 - .../application/top_nav/editor_menu.tsx | 22 ++- .../solution_toolbar/items/popover.tsx | 21 ++- .../solution_toolbar/items/quick_group.scss | 13 -- .../solution_toolbar/items/quick_group.tsx | 10 +- .../solution_toolbar.stories.tsx | 96 +++++----- .../components/workpad_app/workpad_app.scss | 2 +- .../element_menu.stories.storyshot | 4 +- .../__stories__/element_menu.stories.tsx | 8 +- .../element_menu/element_menu.component.tsx | 64 ++----- .../element_menu/element_menu.scss | 3 - .../element_menu/element_menu.tsx | 42 +---- .../workpad_header.component.tsx | 178 ++++++++++++------ .../workpad_header/workpad_header.tsx | 49 ++++- x-pack/plugins/canvas/public/style/index.scss | 1 - x-pack/plugins/canvas/tsconfig.json | 1 + 16 files changed, 266 insertions(+), 250 deletions(-) delete mode 100644 x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.scss diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index e6a2c41fd4ecb..712c070e17b9f 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -543,7 +543,6 @@ export function DashboardTopNav({ createType: title, onClick: createNewVisType(visType as VisTypeAlias), 'data-test-subj': `dashboardQuickButton${name}`, - isDarkModeEnabled: IS_DARK_THEME, }; } else { const { name, icon, title, titleInWizard } = visType as BaseVisType; @@ -553,7 +552,6 @@ export function DashboardTopNav({ createType: titleInWizard || title, onClick: createNewVisType(visType as BaseVisType), 'data-test-subj': `dashboardQuickButton${name}`, - isDarkModeEnabled: IS_DARK_THEME, }; } } diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 46ae4d9456d92..8a46a16c1bf0c 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -238,16 +238,18 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { panelPaddingSize="none" data-test-subj="dashboardEditorMenuButton" > - + {() => ( + + )} ); }; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx index 33850005b498b..fea6bf41a1601 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/popover.tsx @@ -18,13 +18,17 @@ type AllowedPopoverProps = Omit< 'button' | 'isOpen' | 'closePopover' | 'anchorPosition' >; -export type Props = AllowedButtonProps & AllowedPopoverProps; +export type Props = AllowedButtonProps & + AllowedPopoverProps & { + children: (arg: { closePopover: () => void }) => React.ReactNode; + }; export const SolutionToolbarPopover = ({ label, iconType, primary, iconSide, + children, ...popover }: Props) => { const [isOpen, setIsOpen] = useState(false); @@ -33,10 +37,21 @@ export const SolutionToolbarPopover = ({ const closePopover = () => setIsOpen(false); const button = ( - + ); return ( - + + {children({ closePopover })} + ); }; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 876ee659b71d7..535570a51d777 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -8,17 +8,4 @@ border-color: $euiBorderColor !important; } } - - // Temporary fix for two tone icons to make them monochrome - .quickButtonGroup__button--dark { - .euiIcon path { - fill: $euiColorGhost; - } - } - // Temporary fix for two tone icons to make them monochrome - .quickButtonGroup__button--light { - .euiIcon path { - fill: $euiColorInk; - } - } } diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx index eb0a395548cd9..66b22eeb570db 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.tsx @@ -17,27 +17,23 @@ import './quick_group.scss'; export interface QuickButtonProps extends Pick { createType: string; onClick: () => void; - isDarkModeEnabled?: boolean; } export interface Props { buttons: QuickButtonProps[]; } -type Option = EuiButtonGroupOptionProps & - Omit; +type Option = EuiButtonGroupOptionProps & Omit; export const QuickButtonGroup = ({ buttons }: Props) => { const buttonGroupOptions: Option[] = buttons.map((button: QuickButtonProps, index) => { - const { createType: label, isDarkModeEnabled, ...rest } = button; + const { createType: label, ...rest } = button; const title = strings.getAriaButtonLabel(label); return { ...rest, 'aria-label': title, - className: `quickButtonGroup__button ${ - isDarkModeEnabled ? 'quickButtonGroup__button--dark' : 'quickButtonGroup__button--light' - }`, + className: `quickButtonGroup__button`, id: `${htmlIdGenerator()()}${index}`, label, title, diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx index fa33f53f9ae4f..3a04a4c974538 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx +++ b/src/plugins/presentation_util/public/components/solution_toolbar/solution_toolbar.stories.tsx @@ -54,29 +54,31 @@ const primaryButtonConfigs = { panelPaddingSize="none" primary={true} > - + {() => ( + + )} ), Dashboard: ( @@ -93,29 +95,31 @@ const extraButtonConfigs = { Canvas: undefined, Dashboard: [ - + {() => ( + + )} , ], }; diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss index 3f6d6887e0c80..4acdca10d61cc 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: 1px $euiSize 0; + padding: $euiSizeS; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot index 371a5133fe88e..1e71803d22c21 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/__stories__/__snapshots__/element_menu.stories.storyshot @@ -3,13 +3,13 @@ exports[`Storyshots components/WorkpadHeader/ElementMenu default 1`] = `
- - -
- -
- - -
- - -
-
- Request failed, Error: simulated bulkRequest error + + Load Kibana objects + -
-
-
-
+ + + + - + - + + +
+ + +
+
+ + Request failed, Error: simulated bulkRequest error + +
+
+
`; @@ -446,260 +351,161 @@ exports[`bulkCreate should display success message when bulkCreate is successful ] } > - - - - -

- Imports index pattern, visualizations and pre-defined dashboards. -

-
-
- - - Load Kibana objects - - -
- - - , - "key": "installStep", - "status": "complete", - "title": "Load Kibana objects", - }, - ] - } + +

+ Load Kibana objects +

+
+
- +
-
- - - - - - - - +
-

- Load Kibana objects +

+ Imports index pattern, visualizations and pre-defined dashboards.

- -
-
+ +
+ + +
+ - -
- -
- -
-

- Imports index pattern, visualizations and pre-defined dashboards. -

-
-
-
-
- -
- - - - - -
-
-
-
- -
- - -
-
- 1 saved objects successfully added + + Load Kibana objects + -
-
-
-
+ + + +
- +
+
+ + +
+ + +
+
+ + 1 saved objects successfully added + +
- +
`; exports[`renders 1`] = ` - - - - -

- Imports index pattern, visualizations and pre-defined dashboards. -

-
-
- - - Load Kibana objects - - -
- - , - "key": "installStep", - "status": "incomplete", - "title": "Load Kibana objects", - }, - ] - } -/> + + +

+ Load Kibana objects +

+
+ + + +

+ Imports index pattern, visualizations and pre-defined dashboards. +

+
+
+ + + Load Kibana objects + + +
+ +
`; diff --git a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap index ac697fae17f69..91dcdabd75dee 100644 --- a/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap +++ b/src/plugins/home/public/application/components/tutorial/__snapshots__/tutorial.test.js.snap @@ -1,173 +1,146 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`isCloudEnabled is false should not render instruction toggle when ON_PREM_ELASTIC_CLOUD instructions are not provided 1`] = ` - - -
- - -
- - - + + + - -
- - + ], + }, + ] + } + isCloudEnabled={false} + offset={1} + onStatusCheck={[Function]} + paramValues={Object {}} + replaceTemplateStrings={[Function]} + setParameter={[Function]} + statusCheckState="NOT_CHECKED" + title="Instruction title" + /> +
+ `; exports[`isCloudEnabled is false should render ON_PREM instructions with instruction toggle 1`] = ` - - -
- + + + + - -
- - - - - -
- - - + + - -
-
-
+ ], + }, + ] + } + isCloudEnabled={false} + offset={1} + onStatusCheck={[Function]} + paramValues={Object {}} + replaceTemplateStrings={[Function]} + setParameter={[Function]} + statusCheckState="NOT_CHECKED" + title="Instruction title" + /> +
+ `; exports[`should render ELASTIC_CLOUD instructions when isCloudEnabled is true 1`] = ` - - -
- - -
- - - + + + - -
- - + ], + }, + ] + } + isCloudEnabled={true} + offset={1} + onStatusCheck={[Function]} + paramValues={Object {}} + replaceTemplateStrings={[Function]} + setParameter={[Function]} + statusCheckState="NOT_CHECKED" + title="Instruction title" + /> +
+ `; diff --git a/src/plugins/home/public/application/components/tutorial/_tutorial.scss b/src/plugins/home/public/application/components/tutorial/_tutorial.scss index b517476885e2e..6d6ca4781346d 100644 --- a/src/plugins/home/public/application/components/tutorial/_tutorial.scss +++ b/src/plugins/home/public/application/components/tutorial/_tutorial.scss @@ -1,7 +1,3 @@ -.homTutorial__notFoundPanel { - background: $euiColorEmptyShade; - padding: $euiSizeL; -} .homTutorial__instruction { flex-shrink: 0; diff --git a/src/plugins/home/public/application/components/tutorial/content.js b/src/plugins/home/public/application/components/tutorial/content.js index 8b0e09d2eb851..d076957521eee 100644 --- a/src/plugins/home/public/application/components/tutorial/content.js +++ b/src/plugins/home/public/application/components/tutorial/content.js @@ -8,19 +8,10 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { Markdown } from '../../../../../kibana_react/public'; - -const whiteListedRules = ['backticks', 'emphasis', 'link', 'list']; +import { EuiMarkdownFormat } from '@elastic/eui'; export function Content({ text }) { - return ( - - ); + return {text}; } Content.propTypes = { diff --git a/src/plugins/home/public/application/components/tutorial/content.test.js b/src/plugins/home/public/application/components/tutorial/content.test.js index e0b0a256f207c..f8a75d0a04f1c 100644 --- a/src/plugins/home/public/application/components/tutorial/content.test.js +++ b/src/plugins/home/public/application/components/tutorial/content.test.js @@ -11,12 +11,6 @@ import { shallow } from 'enzyme'; import { Content } from './content'; -jest.mock('../../../../../kibana_react/public', () => { - return { - Markdown: () =>
, - }; -}); - test('should render content with markdown', () => { const component = shallow( - + + + +

+ +

+
+
- - - -

- -

-
-
- - - - {label} - - -
-
+ + + {label} + + + ); } diff --git a/src/plugins/home/public/application/components/tutorial/instruction.js b/src/plugins/home/public/application/components/tutorial/instruction.js index e4b3b3f321bf9..ebe78b78f300d 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction.js +++ b/src/plugins/home/public/application/components/tutorial/instruction.js @@ -10,18 +10,7 @@ import React, { Suspense, useMemo } from 'react'; import PropTypes from 'prop-types'; import { Content } from './content'; -import { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiCopy, - EuiButton, - EuiLoadingSpinner, - EuiErrorBoundary, -} from '@elastic/eui'; - -import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiCodeBlock, EuiSpacer, EuiLoadingSpinner, EuiErrorBoundary } from '@elastic/eui'; import { getServices } from '../../kibana_services'; @@ -39,16 +28,21 @@ export function Instruction({ let pre; if (textPre) { - pre = ; + pre = ( + <> + + + + ); } let post; if (textPost) { post = ( -
+ <> -
+ ); } const customComponent = tutorialService.getCustomComponent(customComponentName); @@ -59,7 +53,6 @@ export function Instruction({ } }, [customComponent]); - let copyButton; let commandBlock; if (commands) { const cmdText = commands @@ -67,35 +60,16 @@ export function Instruction({ return replaceTemplateStrings(cmd, paramValues); }) .join('\n'); - copyButton = ( - - {(copy) => ( - - - - )} - - ); commandBlock = ( -
- - {cmdText} -
+ + {cmdText} + ); } return (
- - {pre} - - - {copyButton} - - + {pre} {commandBlock} @@ -114,8 +88,6 @@ export function Instruction({ )} {post} - -
); } diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.js b/src/plugins/home/public/application/components/tutorial/instruction_set.js index 08b55a527b3cf..822c60cdc31ba 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.js @@ -21,12 +21,13 @@ import { EuiFlexItem, EuiButton, EuiCallOut, - EuiButtonEmpty, EuiTitle, + EuiSplitPanel, } from '@elastic/eui'; import * as StatusCheckStates from './status_check_states'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; +import { euiThemeVars } from '@kbn/ui-shared-deps-src/theme'; class InstructionSetUi extends React.Component { constructor(props) { @@ -97,7 +98,12 @@ class InstructionSetUi extends React.Component { color = 'warning'; break; } - return ; + return ( + <> + + + + ); } getStepStatus(statusCheckState) { @@ -131,27 +137,20 @@ class InstructionSetUi extends React.Component { const { statusCheckState, statusCheckConfig, onStatusCheck } = this.props; const checkStatusStep = ( - - - - - - - - {statusCheckConfig.btnLabel || ( - - )} - - - + + + {statusCheckConfig.btnLabel || ( + + )} + {this.renderStatusCheckMessage()} @@ -202,27 +201,29 @@ class InstructionSetUi extends React.Component { steps.push(this.renderStatusCheck()); } - return ; + return ( + <> + + + + ); }; renderHeader = () => { let paramsVisibilityToggle; if (this.props.params) { - const ariaLabel = this.props.intl.formatMessage({ - id: 'home.tutorial.instructionSet.toggleAriaLabel', - defaultMessage: 'toggle command parameters visibility', - }); paramsVisibilityToggle = ( - - + ); } @@ -245,11 +246,14 @@ class InstructionSetUi extends React.Component { } return ( - + <> + + + ); }; @@ -257,28 +261,29 @@ class InstructionSetUi extends React.Component { let paramsForm; if (this.props.params && this.state.isParamFormVisible) { paramsForm = ( - + <> + + + ); } return ( -
- {this.renderHeader()} - - {this.renderCallOut()} - - {paramsForm} - - {this.renderTabs()} - - - - {this.renderInstructions()} -
+ + + {this.renderTabs()} + + + {this.renderHeader()} + {paramsForm} + {this.renderCallOut()} + {this.renderInstructions()} + + ); } } diff --git a/src/plugins/home/public/application/components/tutorial/instruction_set.test.js b/src/plugins/home/public/application/components/tutorial/instruction_set.test.js index 1bce4f72fde60..6faadf275bea3 100644 --- a/src/plugins/home/public/application/components/tutorial/instruction_set.test.js +++ b/src/plugins/home/public/application/components/tutorial/instruction_set.test.js @@ -34,12 +34,6 @@ const instructionVariants = [ }, ]; -jest.mock('../../../../../kibana_react/public', () => { - return { - Markdown: () =>
, - }; -}); - test('render', () => { const component = shallowWithIntl( - ); + />, + ]; } let exportedFields; if (exportedFieldsUrl) { exportedFields = ( -
- - + <> +
+ -
-
- ); - } - let icon; - if (iconType) { - icon = ( - - - + + ); } let betaBadge; @@ -81,31 +64,28 @@ function IntroductionUI({ ); } return ( - - - - {icon} - - -

- {title} - {betaBadge && ( - <> -   - {betaBadge} - - )} -

-
-
-
- - - {exportedFields} -
- - {img} -
+ + {title} + {betaBadge && ( + <> +   + {betaBadge} + + )} + + } + description={ + <> + + {exportedFields} + {notices} + + } + rightSideItems={rightSideItems} + /> ); } @@ -116,6 +96,7 @@ IntroductionUI.propTypes = { exportedFieldsUrl: PropTypes.string, iconType: PropTypes.string, isBeta: PropTypes.bool, + notices: PropTypes.node, }; IntroductionUI.defaultProps = { diff --git a/src/plugins/home/public/application/components/tutorial/introduction.test.js b/src/plugins/home/public/application/components/tutorial/introduction.test.js index a0ab9d8c8e6a7..949f84d0181ed 100644 --- a/src/plugins/home/public/application/components/tutorial/introduction.test.js +++ b/src/plugins/home/public/application/components/tutorial/introduction.test.js @@ -11,12 +11,6 @@ import { shallowWithIntl } from '@kbn/test/jest'; import { Introduction } from './introduction'; -jest.mock('../../../../../kibana_react/public', () => { - return { - Markdown: () =>
, - }; -}); - test('render', () => { const component = shallowWithIntl( { + render() { const installMsg = this.props.installMsg ? this.props.installMsg : this.props.intl.formatMessage({ id: 'home.tutorial.savedObject.installLabel', defaultMessage: 'Imports index pattern, visualizations and pre-defined dashboards.', }); - const installStep = ( - + + return ( + <> + +

+ {this.props.intl.formatMessage({ + id: 'home.tutorial.savedObject.loadTitle', + defaultMessage: 'Load Kibana objects', + })} +

+
@@ -190,22 +199,8 @@ Click 'Confirm overwrite' to import and overwrite existing objects. Any changes {this.renderInstallMessage()} -
+ ); - - return { - title: this.props.intl.formatMessage({ - id: 'home.tutorial.savedObject.loadTitle', - defaultMessage: 'Load Kibana objects', - }), - status: this.state.isInstalled ? 'complete' : 'incomplete', - children: installStep, - key: 'installStep', - }; - }; - - render() { - return ; } } diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.js b/src/plugins/home/public/application/components/tutorial/tutorial.js index 52daa53d4585c..508a236bf45d4 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.js @@ -7,26 +7,18 @@ */ import _ from 'lodash'; -import React from 'react'; +import React, { Fragment } from 'react'; import PropTypes from 'prop-types'; import { Footer } from './footer'; import { Introduction } from './introduction'; import { InstructionSet } from './instruction_set'; import { SavedObjectsInstaller } from './saved_objects_installer'; -import { - EuiSpacer, - EuiPage, - EuiPanel, - EuiText, - EuiPageBody, - EuiButtonGroup, - EuiFlexGroup, - EuiFlexItem, -} from '@elastic/eui'; +import { EuiSpacer, EuiPanel, EuiButton, EuiButtonGroup, EuiFormRow } from '@elastic/eui'; import * as StatusCheckStates from './status_check_states'; import { injectI18n, FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; import { getServices } from '../../kibana_services'; +import { KibanaPageTemplate } from '../../../../../kibana_react/public'; const INSTRUCTIONS_TYPE = { ELASTIC_CLOUD: 'elasticCloud', @@ -250,19 +242,22 @@ class TutorialUi extends React.Component { }, ]; return ( - - + <> + + - - + + ); } }; @@ -286,23 +281,25 @@ class TutorialUi extends React.Component { offset += instructionSet.instructionVariants[0].instructions.length; return ( - { - this.onStatusCheck(index); - }} - offset={currentOffset} - params={instructions.params} - paramValues={this.state.paramValues} - setParameter={this.setParameter} - replaceTemplateStrings={this.props.replaceTemplateStrings} - key={index} - isCloudEnabled={this.props.isCloudEnabled} - /> + + { + this.onStatusCheck(index); + }} + offset={currentOffset} + params={instructions.params} + paramValues={this.state.paramValues} + setParameter={this.setParameter} + replaceTemplateStrings={this.props.replaceTemplateStrings} + isCloudEnabled={this.props.isCloudEnabled} + /> + {index < instructions.instructionSets.length - 1 && } + ); }); }; @@ -313,11 +310,16 @@ class TutorialUi extends React.Component { } return ( - + <> + + + + + ); }; @@ -338,22 +340,23 @@ class TutorialUi extends React.Component { } if (url && label) { - return
; + return ( + <> + + +
+ + + ); } }; renderModuleNotices() { const notices = getServices().tutorialService.getModuleNotices(); if (notices.length && this.state.tutorial.moduleName) { - return ( - - {notices.map((ModuleNotice, index) => ( - - - - ))} - - ); + return notices.map((ModuleNotice, index) => ( + + )); } else { return null; } @@ -363,17 +366,34 @@ class TutorialUi extends React.Component { let content; if (this.state.notFound) { content = ( -
- -

+ -

-
-
+ ), + rightSideItems: [ + + {i18n.translate('home.tutorial.backToDirectory', { + defaultMessage: 'Back to directory', + })} + , + ], + }} + /> ); } @@ -405,27 +425,20 @@ class TutorialUi extends React.Component { exportedFieldsUrl={exportedFieldsUrl} iconType={icon} isBeta={this.state.tutorial.isBeta} + notices={this.renderModuleNotices()} /> - {this.renderModuleNotices()} - -
{this.renderInstructionSetsToggle()}
+ {this.renderInstructionSetsToggle()} - - {this.renderInstructionSets(instructions)} - {this.renderSavedObjectsInstaller()} - {this.renderFooter()} - + {this.renderInstructionSets(instructions)} + {this.renderSavedObjectsInstaller()} + {this.renderFooter()}
); } - return ( - - {content} - - ); + return {content}; } } diff --git a/src/plugins/home/public/application/components/tutorial/tutorial.test.js b/src/plugins/home/public/application/components/tutorial/tutorial.test.js index c76b20e63ae95..c68f5ec69e161 100644 --- a/src/plugins/home/public/application/components/tutorial/tutorial.test.js +++ b/src/plugins/home/public/application/components/tutorial/tutorial.test.js @@ -33,11 +33,6 @@ jest.mock('../../kibana_services', () => ({ }, }), })); -jest.mock('../../../../../kibana_react/public', () => { - return { - Markdown: () =>
, - }; -}); function buildInstructionSet(type) { return { diff --git a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx index de9c7f651019e..8f66658785b97 100644 --- a/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx +++ b/x-pack/plugins/apm/public/components/fleet_integration/apm_agents/agent_instructions_accordion.tsx @@ -24,7 +24,6 @@ import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { AgentIcon } from '../../shared/agent_icon'; import { NewPackagePolicy } from '../apm_policy_form/typings'; import { getCommands } from '../../../tutorial/config_agent/commands/get_commands'; -import { CopyCommands } from '../../../tutorial/config_agent/copy_commands'; import { replaceTemplateStrings } from './replace_template_strings'; function AccordionButtonContent({ @@ -91,14 +90,9 @@ function TutorialConfigAgent({ policyDetails: { apmServerUrl, secretToken }, }); return ( - - - - - - {commandBlock} - - + + {commandBlock} + ); } @@ -153,23 +147,16 @@ export function AgentInstructionsAccordion({ {textPre && ( - - - - - {commandBlock && ( - - - - )} - + )} {commandBlock && ( <> - {commandBlock} + + {commandBlock} + )} {customComponentName === 'TutorialConfigAgent' && ( diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx deleted file mode 100644 index c5261cfc1dc04..0000000000000 --- a/x-pack/plugins/apm/public/tutorial/config_agent/copy_commands.tsx +++ /dev/null @@ -1,26 +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, EuiCopy } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; - -interface Props { - commands: string; -} -export function CopyCommands({ commands }: Props) { - return ( - - {(copy) => ( - - {i18n.translate('xpack.apm.tutorial.copySnippet', { - defaultMessage: 'Copy snippet', - })} - - )} - - ); -} diff --git a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx index 5ff1fd7f42119..bce16ae6ef1f9 100644 --- a/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx +++ b/x-pack/plugins/apm/public/tutorial/config_agent/index.tsx @@ -4,20 +4,13 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - EuiCodeBlock, - EuiFlexGroup, - EuiFlexItem, - EuiLoadingSpinner, - EuiSpacer, -} from '@elastic/eui'; +import { EuiCodeBlock, EuiLoadingSpinner, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { HttpStart } from 'kibana/public'; import React, { useEffect, useMemo, useState } from 'react'; import styled from 'styled-components'; import { APIReturnType } from '../..//services/rest/createCallApmApi'; import { getCommands } from './commands/get_commands'; -import { CopyCommands } from './copy_commands'; import { getPolicyOptions, PolicyOption } from './get_policy_options'; import { PolicySelector } from './policy_selector'; @@ -136,27 +129,19 @@ function TutorialConfigAgent({ return ( <> - - - - setSelectedOption(newSelectedOption) - } - fleetLink={getFleetLink({ - isFleetEnabled: data.isFleetEnabled, - hasFleetAgents, - basePath, - })} - /> - - - - - + setSelectedOption(newSelectedOption)} + fleetLink={getFleetLink({ + isFleetEnabled: data.isFleetEnabled, + hasFleetAgents, + basePath, + })} + /> + - + {commands} diff --git a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts index fb9fbae33ac82..acc0ce69e0e4e 100644 --- a/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts +++ b/x-pack/plugins/apm/server/tutorial/envs/on_prem.ts @@ -76,7 +76,12 @@ export function onPremInstructions({ { id: INSTRUCTION_VARIANT.FLEET, instructions: [ - { customComponentName: 'TutorialFleetInstructions' }, + { + title: i18n.translate('xpack.apm.tutorial.fleet.title', { + defaultMessage: 'Fleet', + }), + customComponentName: 'TutorialFleetInstructions', + }, ], }, ] diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx index 6b9b441551a56..24d9dc8e2c100 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx @@ -7,7 +7,8 @@ import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiText, EuiLink, EuiSpacer } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiText, EuiLink, EuiSpacer, EuiIcon } from '@elastic/eui'; import type { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; import { useGetPackages, useLink, useCapabilities } from '../../hooks'; @@ -31,16 +32,20 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }

- - + ), availableAsIntegrationLink: ( Date: Tue, 12 Oct 2021 13:23:00 -0600 Subject: [PATCH 094/287] [Maps] fix flaky getTile test (#114689) --- x-pack/test/api_integration/apis/maps/get_tile.js | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/x-pack/test/api_integration/apis/maps/get_tile.js b/x-pack/test/api_integration/apis/maps/get_tile.js index a1d4f10ca7be8..9705064464843 100644 --- a/x-pack/test/api_integration/apis/maps/get_tile.js +++ b/x-pack/test/api_integration/apis/maps/get_tile.js @@ -66,14 +66,21 @@ export default function ({ getService }) { expect(metadataFeature.type).to.be(3); expect(metadataFeature.extent).to.be(4096); expect(metadataFeature.id).to.be(undefined); + const fieldMeta = JSON.parse(metadataFeature.properties.fieldMeta); + delete metadataFeature.properties.fieldMeta; expect(metadataFeature.properties).to.eql({ __kbn_feature_count__: 2, __kbn_is_tile_complete__: true, __kbn_metadata_feature__: true, __kbn_vector_shape_type_counts__: '{"POINT":2,"LINE":0,"POLYGON":0}', - fieldMeta: - '{"machine.os.raw":{"categories":{"categories":[{"key":"ios","count":1},{"count":1}]}},"bytes":{"range":{"min":9252,"max":9583,"delta":331},"categories":{"categories":[{"key":9252,"count":1},{"key":9583,"count":1}]}}}', }); + expect(fieldMeta.bytes.range).to.eql({ + min: 9252, + max: 9583, + delta: 331, + }); + expect(fieldMeta.bytes.categories.categories.length).to.be(2); + expect(fieldMeta['machine.os.raw'].categories.categories.length).to.be(2); expect(metadataFeature.loadGeometry()).to.eql([ [ { x: 0, y: 4096 }, From 0de7012bf2ba44271d57cb0169403158ea29c08b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 12 Oct 2021 14:39:43 -0500 Subject: [PATCH 095/287] Update polyfills (master) (#114564) Co-authored-by: Renovate Bot Co-authored-by: spalger --- package.json | 6 +++--- yarn.lock | 53 +++++++++++----------------------------------------- 2 files changed, 14 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index 16422e3fda27e..ce8e928688805 100644 --- a/package.json +++ b/package.json @@ -184,7 +184,7 @@ "@types/redux-logger": "^3.0.8", "JSONStream": "1.3.5", "abort-controller": "^3.0.0", - "abortcontroller-polyfill": "^1.4.0", + "abortcontroller-polyfill": "^1.7.3", "angular": "^1.8.0", "angular-aria": "^1.8.0", "angular-recursion": "^1.0.5", @@ -209,7 +209,7 @@ "constate": "^1.3.2", "content-disposition": "0.5.3", "copy-to-clipboard": "^3.0.8", - "core-js": "^3.6.5", + "core-js": "^3.18.2", "cronstrue": "^1.51.0", "cytoscape": "^3.10.0", "cytoscape-dagre": "^2.2.2", @@ -369,7 +369,7 @@ "remark-stringify": "^9.0.0", "require-in-the-middle": "^5.1.0", "reselect": "^4.0.0", - "resize-observer-polyfill": "^1.5.0", + "resize-observer-polyfill": "^1.5.1", "rison-node": "1.0.2", "rxjs": "^6.5.5", "safe-squel": "^5.12.5", diff --git a/yarn.lock b/yarn.lock index 9d141be857475..31b69268e6e7b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7605,10 +7605,10 @@ abort-controller@^3.0.0: dependencies: event-target-shim "^5.0.0" -abortcontroller-polyfill@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" - integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== +abortcontroller-polyfill@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" + integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" @@ -9774,18 +9774,7 @@ browserslist@4.14.2: escalade "^3.0.2" node-releases "^1.1.61" -browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4.6.0, browserslist@^4.8.5: - version "4.16.3" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.16.3.tgz#340aa46940d7db878748567c5dea24a48ddf3717" - integrity sha512-vIyhWmIkULaq04Gt93txdh+j02yX/JzlyhLYbV3YQCn/zvES3JnY7TifHHvvr1w5hTDluNKMkV05cs4vy8Q7sw== - dependencies: - caniuse-lite "^1.0.30001181" - colorette "^1.2.1" - electron-to-chromium "^1.3.649" - escalade "^3.1.1" - node-releases "^1.1.70" - -browserslist@^4.16.6, browserslist@^4.17.1: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.14.5, browserslist@^4.16.1, browserslist@^4.16.6, browserslist@^4.17.1, browserslist@^4.6.0, browserslist@^4.8.5: version "4.17.1" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.17.1.tgz#a98d104f54af441290b7d592626dd541fa642eb9" integrity sha512-aLD0ZMDSnF4lUt4ZDNgqi5BUn9BZ7YdQdI/cYlILrhdSSZJLU9aNZoD5/NBmM4SK34APB2e83MOsRt1EnkuyaQ== @@ -10128,12 +10117,7 @@ caniuse-api@^3.0.0: lodash.memoize "^4.1.2" lodash.uniq "^4.5.0" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001181: - version "1.0.30001258" - resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001258.tgz" - integrity sha512-RBByOG6xWXUp0CR2/WU2amXz3stjKpSl5J1xU49F1n2OxD//uBZO4wCKUiG+QMGf7CHGfDDcqoKriomoGVxTeA== - -caniuse-lite@^1.0.30001259: +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001097, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001125, caniuse-lite@^1.0.30001259: version "1.0.30001261" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001261.tgz#96d89813c076ea061209a4e040d8dcf0c66a1d01" integrity sha512-vM8D9Uvp7bHIN0fZ2KQ4wnmYFpJo/Etb4Vwsuc+ka0tfGDHvOPrFm6S/7CCNLSOkAUjenT2HnUPESdOIL91FaA== @@ -11231,15 +11215,10 @@ core-js@^2.4.0, core-js@^2.5.0, core-js@^2.6.9: resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.9.tgz#6b4b214620c834152e179323727fc19741b084f2" integrity sha512-HOpZf6eXmnl7la+cUdMnLvUxKNqLUzJvgIziQ0DiF3JwSImNphIqdGqzj6hIKyX04MmV0poclQ7+wjWvxQyR2A== -core-js@^3.0.1, core-js@^3.0.4, core-js@^3.6.5: - version "3.6.5" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.6.5.tgz#7395dc273af37fb2e50e9bd3d9fe841285231d1a" - integrity sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA== - -core-js@^3.8.2: - version "3.11.0" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.11.0.tgz#05dac6aa70c0a4ad842261f8957b961d36eb8926" - integrity sha512-bd79DPpx+1Ilh9+30aT5O1sgpQd4Ttg8oqkqi51ZzhedMM1omD2e6IOF48Z/DzDCZ2svp49tN/3vneTK6ZBkXw== +core-js@^3.0.1, core-js@^3.0.4, core-js@^3.18.2, core-js@^3.6.5, core-js@^3.8.2: + version "3.18.2" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.18.2.tgz#63a551e8a29f305cd4123754846e65896619ba5b" + integrity sha512-zNhPOUoSgoizoSQFdX1MeZO16ORRb9FFQLts8gSYbZU5FcgXhp24iMWMxnOQo5uIaIG7/6FA/IqJPwev1o9ZXQ== core-util-is@1.0.2, core-util-is@^1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -13273,11 +13252,6 @@ electron-to-chromium@^1.3.564: resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.642.tgz#8b884f50296c2ae2a9997f024d0e3e57facc2b94" integrity sha512-cev+jOrz/Zm1i+Yh334Hed6lQVOkkemk2wRozfMF4MtTR7pxf3r3L5Rbd7uX1zMcEqVJ7alJBnJL7+JffkC6FQ== -electron-to-chromium@^1.3.649: - version "1.3.690" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.690.tgz#54df63ec42fba6b8e9e05fe4be52caeeedb6e634" - integrity sha512-zPbaSv1c8LUKqQ+scNxJKv01RYFkVVF1xli+b+3Ty8ONujHjAMg+t/COmdZqrtnS1gT+g4hbSodHillymt1Lww== - electron-to-chromium@^1.3.846: version "1.3.853" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.853.tgz#f3ed1d31f092cb3a17af188bca6c6a3ec91c3e82" @@ -21199,11 +21173,6 @@ node-releases@^1.1.61: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.61.tgz#707b0fca9ce4e11783612ba4a2fcba09047af16e" integrity sha512-DD5vebQLg8jLCOzwupn954fbIiZht05DAZs0k2u8NStSe6h9XdsuIQL8hSRKYiU8WUQRznmSDrKGbv3ObOmC7g== -node-releases@^1.1.70: - version "1.1.71" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" - integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== - node-releases@^1.1.76: version "1.1.76" resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.76.tgz#df245b062b0cafbd5282ab6792f7dccc2d97f36e" @@ -25271,7 +25240,7 @@ reselect@^4.0.0: resolved "https://registry.yarnpkg.com/reselect/-/reselect-4.0.0.tgz#f2529830e5d3d0e021408b246a206ef4ea4437f7" integrity sha512-qUgANli03jjAyGlnbYVAV5vvnOmJnODyABz51RdBN7M4WaVu8mecZWgyQNkG8Yqe3KRGRt0l4K4B3XVEULC4CA== -resize-observer-polyfill@^1.5.0, resize-observer-polyfill@^1.5.1: +resize-observer-polyfill@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz#0e9020dd3d21024458d4ebd27e23e40269810464" integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg== From 3c8662f9faa3cd99ece0cbdb5b34e85f28ba7ef6 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 12 Oct 2021 12:42:38 -0700 Subject: [PATCH 096/287] [Reporting] deprecate capture.viewport setting from reporting config as unused (#114019) * [Reporting] remove capture.viewport setting from reporting config * update snapshots * update snapshot * add helpful version comment * self-review Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/reporting/common/constants.ts | 5 +++++ .../browsers/chromium/driver_factory/args.ts | 7 +++---- .../browsers/chromium/driver_factory/index.ts | 14 ++++++-------- .../chromium/driver_factory/start_logs.ts | 1 - x-pack/plugins/reporting/server/config/index.ts | 1 + .../reporting/server/config/schema.test.ts | 8 -------- x-pack/plugins/reporting/server/config/schema.ts | 4 ---- .../server/lib/layouts/create_layout.test.ts | 4 ++++ .../reporting/server/lib/layouts/print_layout.ts | 15 ++++++++------- .../server/lib/screenshots/observable.ts | 5 +---- .../create_mock_browserdriverfactory.ts | 1 - 11 files changed, 28 insertions(+), 37 deletions(-) diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 3fb02677dd981..cafab65677ee4 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -59,6 +59,11 @@ export const LAYOUT_TYPES = { PRINT: 'print', }; +export const DEFAULT_VIEWPORT = { + width: 1950, + height: 1200, +}; + // Export Type Definitions export const CSV_REPORT_TYPE = 'CSV'; export const CSV_JOB_TYPE = 'csv_searchsource'; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts index 3659e05bc3618..07ae13fa31849 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts @@ -6,18 +6,17 @@ */ import { CaptureConfig } from '../../../../server/types'; +import { DEFAULT_VIEWPORT } from '../../../../common/constants'; -type ViewportConfig = CaptureConfig['viewport']; type BrowserConfig = CaptureConfig['browser']['chromium']; interface LaunchArgs { userDataDir: string; - viewport: ViewportConfig; disableSandbox: BrowserConfig['disableSandbox']; proxy: BrowserConfig['proxy']; } -export const args = ({ userDataDir, viewport, disableSandbox, proxy: proxyConfig }: LaunchArgs) => { +export const args = ({ userDataDir, disableSandbox, proxy: proxyConfig }: LaunchArgs) => { const flags = [ // Disable built-in Google Translate service '--disable-translate', @@ -45,7 +44,7 @@ export const args = ({ userDataDir, viewport, disableSandbox, proxy: proxyConfig // NOTE: setting the window size does NOT set the viewport size: viewport and window size are different. // The viewport may later need to be resized depending on the position of the clip area. // These numbers come from the job parameters, so this is a close guess. - `--window-size=${Math.floor(viewport.width)},${Math.floor(viewport.height)}`, + `--window-size=${Math.floor(DEFAULT_VIEWPORT.width)},${Math.floor(DEFAULT_VIEWPORT.height)}`, // allow screenshot clip region to go outside of the viewport `--mainFrameClipsContent=false`, ]; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index a0487421a9a0d..688dd425fa8f3 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -5,10 +5,10 @@ * 2.0. */ -import apm from 'elastic-apm-node'; import { i18n } from '@kbn/i18n'; import { getDataPath } from '@kbn/utils'; import del from 'del'; +import apm from 'elastic-apm-node'; import fs from 'fs'; import path from 'path'; import puppeteer from 'puppeteer'; @@ -23,7 +23,7 @@ import { LevelLogger } from '../../../lib'; import { safeChildProcess } from '../../safe_child_process'; import { HeadlessChromiumDriver } from '../driver'; import { args } from './args'; -import { Metrics, getMetrics } from './metrics'; +import { getMetrics, Metrics } from './metrics'; // Puppeteer type definitions do not match the documentation. // See https://pptr.dev/#?product=Puppeteer&version=v8.0.0&show=api-puppeteerlaunchoptions @@ -38,14 +38,13 @@ declare module 'puppeteer' { } type BrowserConfig = CaptureConfig['browser']['chromium']; -type ViewportConfig = CaptureConfig['viewport']; export class HeadlessChromiumDriverFactory { private binaryPath: string; private captureConfig: CaptureConfig; private browserConfig: BrowserConfig; private userDataDir: string; - private getChromiumArgs: (viewport: ViewportConfig) => string[]; + private getChromiumArgs: () => string[]; private core: ReportingCore; constructor(core: ReportingCore, binaryPath: string, logger: LevelLogger) { @@ -60,10 +59,9 @@ export class HeadlessChromiumDriverFactory { } this.userDataDir = fs.mkdtempSync(path.join(getDataPath(), 'chromium-')); - this.getChromiumArgs = (viewport: ViewportConfig) => + this.getChromiumArgs = () => args({ userDataDir: this.userDataDir, - viewport, disableSandbox: this.browserConfig.disableSandbox, proxy: this.browserConfig.proxy, }); @@ -75,7 +73,7 @@ export class HeadlessChromiumDriverFactory { * Return an observable to objects which will drive screenshot capture for a page */ createPage( - { viewport, browserTimezone }: { viewport: ViewportConfig; browserTimezone?: string }, + { browserTimezone }: { browserTimezone?: string }, pLogger: LevelLogger ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { // FIXME: 'create' is deprecated @@ -83,7 +81,7 @@ export class HeadlessChromiumDriverFactory { const logger = pLogger.clone(['browser-driver']); logger.info(`Creating browser page driver`); - const chromiumArgs = this.getChromiumArgs(viewport); + const chromiumArgs = this.getChromiumArgs(); logger.debug(`Chromium launch args set to: ${chromiumArgs}`); let browser: puppeteer.Browser; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts index aa27e46b85acb..1a739488bf6ed 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts @@ -74,7 +74,6 @@ export const browserStartLogs = ( const kbnArgs = args({ userDataDir, - viewport: { width: 800, height: 600 }, disableSandbox, proxy, }); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index f8fa47bc00bb0..45a71d05165ba 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -20,6 +20,7 @@ export const config: PluginConfigDescriptor = { unused('capture.browser.chromium.maxScreenshotDimension'), // unused since 7.8 unused('poll.jobCompletionNotifier.intervalErrorMultiplier'), // unused since 7.10 unused('poll.jobsRefresh.intervalErrorMultiplier'), // unused since 7.10 + unused('capture.viewport'), // deprecated as unused since 7.16 (settings, fromPath, addDeprecation) => { const reporting = get(settings, fromPath); if (reporting?.index) { diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index 0b2e2cac6ff7c..6ad7d03bd1a8f 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -63,10 +63,6 @@ describe('Reporting Config Schema', () => { "renderComplete": "PT30S", "waitForElements": "PT30S", }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, "zoom": 2, }, "csv": Object { @@ -168,10 +164,6 @@ describe('Reporting Config Schema', () => { "renderComplete": "PT30S", "waitForElements": "PT30S", }, - "viewport": Object { - "height": 1200, - "width": 1950, - }, "zoom": 2, }, "csv": Object { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 832cf6c28e1fa..5b15260be06cb 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -74,10 +74,6 @@ const CaptureSchema = schema.object({ }), }), zoom: schema.number({ defaultValue: 2 }), - viewport: schema.object({ - width: schema.number({ defaultValue: 1950 }), - height: schema.number({ defaultValue: 1200 }), - }), loadDelay: schema.oneOf([schema.number(), schema.duration()], { defaultValue: moment.duration({ seconds: 3 }), }), diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts index be9a06267a7c8..f5c2373fc4bf0 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts @@ -67,6 +67,10 @@ describe('Create Layout', () => { "timefilterDurationAttribute": "data-shared-timefilter-duration", }, "useReportingBranding": true, + "viewport": Object { + "height": 1200, + "width": 1950, + }, } `); }); diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts index 03feb36496349..0849f8850f91d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts @@ -9,7 +9,7 @@ import path from 'path'; import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; import { EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; import { LevelLogger } from '../'; -import { LAYOUT_TYPES } from '../../../common/constants'; +import { DEFAULT_VIEWPORT, LAYOUT_TYPES } from '../../../common/constants'; import { Size } from '../../../common/types'; import { HeadlessChromiumDriver } from '../../browsers'; import { CaptureConfig } from '../../types'; @@ -22,7 +22,8 @@ export class PrintLayout extends Layout implements LayoutInstance { screenshot: '[data-shared-item]', // override '[data-shared-items-container]' }; public readonly groupCount = 2; - private captureConfig: CaptureConfig; + private readonly captureConfig: CaptureConfig; + private readonly viewport = DEFAULT_VIEWPORT; constructor(captureConfig: CaptureConfig) { super(LAYOUT_TYPES.PRINT); @@ -34,7 +35,7 @@ export class PrintLayout extends Layout implements LayoutInstance { } public getBrowserViewport() { - return this.captureConfig.viewport; + return this.viewport; } public getBrowserZoom() { @@ -44,8 +45,8 @@ export class PrintLayout extends Layout implements LayoutInstance { public getViewport(itemsCount: number) { return { zoom: this.captureConfig.zoom, - width: this.captureConfig.viewport.width, - height: this.captureConfig.viewport.height * itemsCount, + width: this.viewport.width, + height: this.viewport.height * itemsCount, }; } @@ -56,8 +57,8 @@ export class PrintLayout extends Layout implements LayoutInstance { logger.debug('positioning elements'); const elementSize: Size = { - width: this.captureConfig.viewport.width / this.captureConfig.zoom, - height: this.captureConfig.viewport.height / this.captureConfig.zoom, + width: this.viewport.width / this.captureConfig.zoom, + height: this.viewport.height / this.captureConfig.zoom, }; const evalOptions: { fn: EvaluateFn; args: SerializableOrJSHandle[] } = { fn: (selector: string, height: number, width: number) => { diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts index b7791cb2924e5..317b50b83548f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts @@ -45,10 +45,7 @@ export function getScreenshots$( const apmTrans = apm.startTransaction(`reporting screenshot pipeline`, 'reporting'); const apmCreatePage = apmTrans?.startSpan('create_page', 'wait'); - const create$ = browserDriverFactory.createPage( - { viewport: layout.getBrowserViewport(), browserTimezone }, - logger - ); + const create$ = browserDriverFactory.createPage({ browserTimezone }, logger); return create$.pipe( mergeMap(({ driver, exit$ }) => { diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts index 8cd0a63f860e8..83cdc986bb048 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts @@ -114,7 +114,6 @@ export const createMockBrowserDriverFactory = async ( autoDownload: false, }, networkPolicy: { enabled: true, rules: [] }, - viewport: { width: 800, height: 600 }, loadDelay: moment.duration(2, 's'), zoom: 2, maxAttempts: 1, From 1afac0ffbba861497521a4f732feebcdba2ef0ee Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Tue, 12 Oct 2021 16:13:58 -0400 Subject: [PATCH 097/287] [Fleet] Have EPR register new categories / Show category counts (#114429) --- .../custom_integrations/common/index.ts | 3 - .../custom_integrations/server/index.ts | 2 +- .../plugins/fleet/common/types/models/epm.ts | 2 + .../use_merge_epr_with_replacements.test.ts | 40 +----- .../hooks/use_merge_epr_with_replacements.ts | 19 +-- .../epm/components/package_card.stories.tsx | 1 + .../components/package_list_grid.stories.tsx | 6 + .../epm/screens/home/available_packages.tsx | 130 ++++++++---------- .../epm/screens/home/category_facets.tsx | 33 ++--- .../sections/epm/screens/home/index.tsx | 1 + .../epm/screens/home/installed_packages.tsx | 7 +- .../sections/epm/screens/home/util.ts | 49 ++++--- .../fleet/server/services/epm/packages/get.ts | 1 - 13 files changed, 130 insertions(+), 164 deletions(-) rename x-pack/plugins/fleet/public/{ => applications/integrations}/hooks/use_merge_epr_with_replacements.test.ts (82%) rename x-pack/plugins/fleet/public/{ => applications/integrations}/hooks/use_merge_epr_with_replacements.ts (75%) diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index 9af7c4ccd4633..de2a6592465a2 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -42,9 +42,6 @@ export const INTEGRATION_CATEGORY_DISPLAY = { // Kibana added upload_file: 'Upload a file', language_client: 'Language client', - - // Internal - updates_available: 'Updates available', }; /** diff --git a/src/plugins/custom_integrations/server/index.ts b/src/plugins/custom_integrations/server/index.ts index 490627ef90f8d..00372df501435 100755 --- a/src/plugins/custom_integrations/server/index.ts +++ b/src/plugins/custom_integrations/server/index.ts @@ -19,7 +19,7 @@ export function plugin(initializerContext: PluginInitializerContext) { export { CustomIntegrationsPluginSetup, CustomIntegrationsPluginStart } from './types'; -export type { IntegrationCategory, IntegrationCategoryCount, CustomIntegration } from '../common'; +export type { IntegrationCategory, CustomIntegration } from '../common'; export const config = { schema: schema.object({}), diff --git a/x-pack/plugins/fleet/common/types/models/epm.ts b/x-pack/plugins/fleet/common/types/models/epm.ts index a487fd0a37e70..df4cdec184dc8 100644 --- a/x-pack/plugins/fleet/common/types/models/epm.ts +++ b/x-pack/plugins/fleet/common/types/models/epm.ts @@ -227,6 +227,7 @@ export type RegistrySearchResult = Pick< | 'internal' | 'data_streams' | 'policy_templates' + | 'categories' >; export type ScreenshotItem = RegistryImage | PackageSpecScreenshot; @@ -376,6 +377,7 @@ export interface IntegrationCardItem { icons: Array; integration: string; id: string; + categories: string[]; } export type PackagesGroupedByStatus = Record, PackageList>; diff --git a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts similarity index 82% rename from x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts rename to x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts index 687fb01b04546..d5d8aa093e300 100644 --- a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.test.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts @@ -5,10 +5,10 @@ * 2.0. */ -import type { PackageListItem } from '../../common/types/models'; -import type { CustomIntegration } from '../../../../../src/plugins/custom_integrations/common'; +import type { PackageListItem } from '../../../../common/types/models'; +import type { CustomIntegration } from '../../../../../../../src/plugins/custom_integrations/common'; -import type { IntegrationCategory } from '../../../../../src/plugins/custom_integrations/common'; +import type { IntegrationCategory } from '../../../../../../../src/plugins/custom_integrations/common'; import { useMergeEprPackagesWithReplacements } from './use_merge_epr_with_replacements'; @@ -46,7 +46,7 @@ describe('useMergeEprWithReplacements', () => { }, ]); - expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([ + expect(useMergeEprPackagesWithReplacements(eprPackages, replacements)).toEqual([ { name: 'aws', release: 'ga', @@ -80,7 +80,7 @@ describe('useMergeEprWithReplacements', () => { }, ]); - expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([ + expect(useMergeEprPackagesWithReplacements(eprPackages, replacements)).toEqual([ { eprOverlap: 'activemq', id: 'activemq-logs', @@ -108,7 +108,7 @@ describe('useMergeEprWithReplacements', () => { }, ]); - expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([ + expect(useMergeEprPackagesWithReplacements(eprPackages, replacements)).toEqual([ { name: 'activemq', release: 'beta', @@ -120,32 +120,6 @@ describe('useMergeEprWithReplacements', () => { ]); }); - test('should respect category assignment', () => { - const eprPackages: PackageListItem[] = mockEprPackages([ - { - name: 'activemq', - release: 'beta', - }, - ]); - const replacements: CustomIntegration[] = mockIntegrations([ - { - id: 'prometheus', - categories: ['monitoring', 'datastore'], - }, - { - id: 'oracle', - categories: ['datastore'], - }, - ]); - - expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, 'web')).toEqual([ - { - name: 'activemq', - release: 'beta', - }, - ]); - }); - test('should consists of all 3 types (ga eprs, replacements for non-ga eprs, replacements without epr equivalent', () => { const eprPackages: PackageListItem[] = mockEprPackages([ { @@ -190,7 +164,7 @@ describe('useMergeEprWithReplacements', () => { }, ]); - expect(useMergeEprPackagesWithReplacements(eprPackages, replacements, '')).toEqual([ + expect(useMergeEprPackagesWithReplacements(eprPackages, replacements)).toEqual([ { name: 'aws', release: 'ga', diff --git a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts similarity index 75% rename from x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts rename to x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts index ac53badc2446d..4c59f0ef45123 100644 --- a/x-pack/plugins/fleet/public/hooks/use_merge_epr_with_replacements.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts @@ -5,12 +5,9 @@ * 2.0. */ -import type { PackageListItem } from '../../common/types/models'; -import type { - CustomIntegration, - IntegrationCategory, -} from '../../../../../src/plugins/custom_integrations/common'; -import { filterCustomIntegrations } from '../../../../../src/plugins/custom_integrations/public'; +import type { PackageListItem } from '../../../../common/types/models'; +import type { CustomIntegration } from '../../../../../../../src/plugins/custom_integrations/common'; +import { filterCustomIntegrations } from '../../../../../../../src/plugins/custom_integrations/public'; // Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package) function findReplacementsForEprPackage( @@ -26,17 +23,13 @@ function findReplacementsForEprPackage( export function useMergeEprPackagesWithReplacements( eprPackages: PackageListItem[], - replacements: CustomIntegration[], - category: IntegrationCategory | '' + replacements: CustomIntegration[] ): Array { const merged: Array = []; - - const filteredReplacements = replacements.filter((customIntegration) => { - return !category || customIntegration.categories.includes(category); - }); + const filteredReplacements = replacements; // Either select replacement or select beat - eprPackages.forEach((eprPackage) => { + eprPackages.forEach((eprPackage: PackageListItem) => { const hits = findReplacementsForEprPackage( filteredReplacements, eprPackage.name, diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx index bddbc4f027b4f..94370587ddec8 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.stories.tsx @@ -32,6 +32,7 @@ const args: Args = { url: '/', icons: [], integration: '', + categories: ['foobar'], }; const argTypes = { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx index e4bd1da842867..f43c18d167717 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_list_grid.stories.tsx @@ -47,6 +47,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, { title: 'Package Two', @@ -58,6 +59,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, { title: 'Package Three', @@ -69,6 +71,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, { title: 'Package Four', @@ -80,6 +83,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, { title: 'Package Five', @@ -91,6 +95,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, { title: 'Package Six', @@ -102,6 +107,7 @@ export const List = (props: Args) => ( url: 'https://example.com', icons: [], integration: 'integation', + categories: ['web'], }, ]} onSearchChange={action('onSearchChange')} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index 8aef9121bf67d..812320261e77f 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -8,6 +8,7 @@ import React, { memo, useMemo } from 'react'; import { useLocation, useHistory, useParams } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import _ from 'lodash'; import { pagePathGetters } from '../../../../constants'; import { @@ -26,30 +27,53 @@ import type { CustomIntegration } from '../../../../../../../../../../src/plugin import type { PackageListItem } from '../../../../types'; -import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common'; +import type { IntegrationCardItem } from '../../../../../../../common/types/models'; -import { useMergeEprPackagesWithReplacements } from '../../../../../../hooks/use_merge_epr_with_replacements'; +import { useMergeEprPackagesWithReplacements } from '../../../../hooks/use_merge_epr_with_replacements'; -import { mergeAndReplaceCategoryCounts } from './util'; -import { CategoryFacets } from './category_facets'; +import { mergeCategoriesAndCount } from './util'; +import { ALL_CATEGORY, CategoryFacets } from './category_facets'; import type { CategoryFacet } from './category_facets'; import type { CategoryParams } from '.'; import { getParams, categoryExists, mapToCard } from '.'; +function getAllCategoriesFromIntegrations(pkg: PackageListItem) { + if (!doesPackageHaveIntegrations(pkg)) { + return pkg.categories; + } + + const allCategories = pkg.policy_templates?.reduce((accumulator, integration) => { + return [...accumulator, ...(integration.categories || [])]; + }, pkg.categories || []); + + return _.uniq(allCategories); +} + // Packages can export multiple integrations, aka `policy_templates` // In the case where packages ship >1 `policy_templates`, we flatten out the // list of packages by bringing all integrations to top-level so that // each integration is displayed as its own tile const packageListToIntegrationsList = (packages: PackageList): PackageList => { return packages.reduce((acc: PackageList, pkg) => { - const { policy_templates: policyTemplates = [], ...restOfPackage } = pkg; + const { + policy_templates: policyTemplates = [], + categories: topCategories = [], + ...restOfPackage + } = pkg; + + const topPackage = { + ...restOfPackage, + categories: getAllCategoriesFromIntegrations(pkg), + }; + return [ ...acc, - restOfPackage, + topPackage, ...(doesPackageHaveIntegrations(pkg) ? policyTemplates.map((integration) => { - const { name, title, description, icons } = integration; + const { name, title, description, icons, categories = [] } = integration; + const allCategories = [...topCategories, ...categories]; return { ...restOfPackage, id: `${restOfPackage}-${name}`, @@ -57,6 +81,7 @@ const packageListToIntegrationsList = (packages: PackageList): PackageList => { title, description, icons: icons || restOfPackage.icons, + categories: _.uniq(allCategories), }; }) : []), @@ -72,14 +97,11 @@ const title = i18n.translate('xpack.fleet.epmList.allTitle', { // or `location` to load data. Ideally, we'll split this into "connected" and "pure" components. export const AvailablePackages: React.FC = memo(() => { useBreadcrumbs('integrations_all'); - const { selectedCategory, searchParam } = getParams( useParams(), useLocation().search ); - const history = useHistory(); - const { getHref, getAbsolutePath } = useLink(); function setSelectedCategory(categoryId: string) { @@ -89,7 +111,6 @@ export const AvailablePackages: React.FC = memo(() => { })[1]; history.push(url); } - function setSearchTerm(search: string) { // Use .replace so the browser's back button is not tied to single keystroke history.replace( @@ -97,84 +118,51 @@ export const AvailablePackages: React.FC = memo(() => { ); } - const { data: allCategoryPackagesRes, isLoading: isLoadingAllPackages } = useGetPackages({ + const { data: eprPackages, isLoading: isLoadingAllPackages } = useGetPackages({ category: '', }); - - const { data: categoryPackagesRes, isLoading: isLoadingCategoryPackages } = useGetPackages({ - category: selectedCategory, - }); - - const { data: categoriesRes, isLoading: isLoadingCategories } = useGetCategories({ - include_policy_templates: true, - }); - - const eprPackages = useMemo( - () => packageListToIntegrationsList(categoryPackagesRes?.response || []), - [categoryPackagesRes] - ); - - const allEprPackages = useMemo( - () => packageListToIntegrationsList(allCategoryPackagesRes?.response || []), - [allCategoryPackagesRes] + const eprIntegrationList = useMemo( + () => packageListToIntegrationsList(eprPackages?.response || []), + [eprPackages] ); - const { value: replacementCustomIntegrations } = useGetReplacementCustomIntegrations(); - const mergedEprPackages: Array = useMergeEprPackagesWithReplacements( - eprPackages || [], - replacementCustomIntegrations || [], - selectedCategory as IntegrationCategory + eprIntegrationList || [], + replacementCustomIntegrations || [] ); - const { loading: isLoadingAppendCustomIntegrations, value: appendCustomIntegrations } = useGetAppendCustomIntegrations(); - - const filteredAddableIntegrations = appendCustomIntegrations - ? appendCustomIntegrations.filter((integration: CustomIntegration) => { - if (!selectedCategory) { - return true; - } - return integration.categories.indexOf(selectedCategory as IntegrationCategory) >= 0; - }) - : []; - const eprAndCustomPackages: Array = [ ...mergedEprPackages, - ...filteredAddableIntegrations, + ...(appendCustomIntegrations || []), ]; - - eprAndCustomPackages.sort((a, b) => { + const cards: IntegrationCardItem[] = eprAndCustomPackages.map((item) => { + return mapToCard(getAbsolutePath, getHref, item); + }); + cards.sort((a, b) => { return a.title.localeCompare(b.title); }); + const { data: eprCategories, isLoading: isLoadingCategories } = useGetCategories({ + include_policy_templates: true, + }); const categories = useMemo(() => { const eprAndCustomCategories: CategoryFacet[] = - isLoadingCategories || - isLoadingAppendCustomIntegrations || - !appendCustomIntegrations || - !categoriesRes + isLoadingCategories || !eprCategories ? [] - : mergeAndReplaceCategoryCounts( - categoriesRes.response as CategoryFacet[], - appendCustomIntegrations + : mergeCategoriesAndCount( + eprCategories.response as Array<{ id: string; title: string; count: number }>, + cards ); - return [ { - id: '', - count: (allEprPackages?.length || 0) + (appendCustomIntegrations?.length || 0), + ...ALL_CATEGORY, + count: cards.length, }, ...(eprAndCustomCategories ? eprAndCustomCategories : []), ] as CategoryFacet[]; - }, [ - allEprPackages?.length, - appendCustomIntegrations, - categoriesRes, - isLoadingAppendCustomIntegrations, - isLoadingCategories, - ]); + }, [cards, eprCategories, isLoadingCategories]); if (!isLoadingCategories && !categoryExists(selectedCategory, categories)) { history.replace(pagePathGetters.integrations_all({ category: '', searchTerm: searchParam })[1]); @@ -183,7 +171,6 @@ export const AvailablePackages: React.FC = memo(() => { const controls = categories ? ( { /> ) : null; - const cards = eprAndCustomPackages.map((item) => { - return mapToCard(getAbsolutePath, getHref, item); + const filteredCards = cards.filter((c) => { + if (selectedCategory === '') { + return true; + } + return c.categories.includes(selectedCategory); }); return ( ) : ( categories.map((category) => { - let title; - - if (category.id === 'updates_available') { - title = i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', { - defaultMessage: 'Updates available', - }); - } else if (category.id === '') { - title = i18n.translate('xpack.fleet.epmList.allPackagesFilterLinkText', { - defaultMessage: 'All', - }); - } else { - title = INTEGRATION_CATEGORY_DISPLAY[category.id]; - } return ( onCategoryChange(category)} > - {title} + {category.title} ); }) diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx index bbebf9e90b16c..9528bd73f9192 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/index.tsx @@ -72,6 +72,7 @@ export const mapToCard = ( name: 'name' in item ? item.name || '' : '', version: 'version' in item ? item.version || '' : '', release: 'release' in item ? item.release : undefined, + categories: ((item.categories || []) as string[]).filter((c: string) => !!c), }; }; diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx index 404e8820f90b7..efcdb7b169edf 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/installed_packages.tsx @@ -23,6 +23,7 @@ import { CategoryFacets } from './category_facets'; import type { CategoryParams } from '.'; import { getParams, categoryExists, mapToCard } from '.'; +import { ALL_CATEGORY } from './category_facets'; const AnnouncementLink = () => { const { docLinks } = useStartServices(); @@ -114,12 +115,15 @@ export const InstalledPackages: React.FC = memo(() => { const categories: CategoryFacet[] = useMemo( () => [ { - id: '', + ...ALL_CATEGORY, count: allInstalledPackages.length, }, { id: 'updates_available', count: updatablePackages.length, + title: i18n.translate('xpack.fleet.epmList.updatesAvailableFilterLinkText', { + defaultMessage: 'Updates available', + }), }, ], [allInstalledPackages.length, updatablePackages.length] @@ -135,7 +139,6 @@ export const InstalledPackages: React.FC = memo(() => { const controls = ( setSelectedCategory(id)} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts index 53a62555650ab..70902b2bc1897 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/util.ts @@ -5,46 +5,57 @@ * 2.0. */ -import type { - CustomIntegration, - IntegrationCategory, -} from '../../../../../../../../../../src/plugins/custom_integrations/common'; +import type { IntegrationCategory } from '../../../../../../../../../../src/plugins/custom_integrations/common'; +import { INTEGRATION_CATEGORY_DISPLAY } from '../../../../../../../../../../src/plugins/custom_integrations/common'; + +import type { IntegrationCardItem } from '../../../../../../../common/types/models'; import type { CategoryFacet } from './category_facets'; -export function mergeAndReplaceCategoryCounts( - eprCounts: CategoryFacet[], - addableIntegrations: CustomIntegration[] +export function mergeCategoriesAndCount( + eprCategoryList: Array<{ id: string; title: string; count: number }>, // EPR-categories from backend call to EPR + cards: IntegrationCardItem[] ): CategoryFacet[] { - const merged: CategoryFacet[] = []; + const facets: CategoryFacet[] = []; - const addIfMissing = (category: string, count: number) => { - const match = merged.find((c) => { + const addIfMissing = (category: string, count: number, title: string) => { + const match = facets.find((c) => { return c.id === category; }); if (match) { match.count += count; } else { - merged.push({ - id: category as IntegrationCategory, + facets.push({ + id: category, count, + title, }); } }; - eprCounts.forEach((facet) => { - addIfMissing(facet.id, facet.count); + // Seed the list with the dynamic categories + eprCategoryList.forEach((facet) => { + addIfMissing(facet.id, 0, facet.title); }); - addableIntegrations.forEach((integration) => { - integration.categories.forEach((cat) => { - addIfMissing(cat, 1); + + // Count all the categories + cards.forEach((integration) => { + integration.categories.forEach((cat: string) => { + const title = INTEGRATION_CATEGORY_DISPLAY[cat as IntegrationCategory] + ? INTEGRATION_CATEGORY_DISPLAY[cat as IntegrationCategory] + : cat; + addIfMissing(cat, 1, title); }); }); - merged.sort((a, b) => { + const filledFacets = facets.filter((facet) => { + return facet.count > 0; + }); + + filledFacets.sort((a, b) => { return a.id.localeCompare(b.id); }); - return merged; + return filledFacets; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/get.ts b/x-pack/plugins/fleet/server/services/epm/packages/get.ts index d4f988e5fba8c..cf847cdf62bc2 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/get.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/get.ts @@ -53,7 +53,6 @@ export async function getPackages( }); // get the installed packages const packageSavedObjects = await getPackageSavedObjects(savedObjectsClient); - // filter out any internal packages const savedObjectsVisible = packageSavedObjects.saved_objects.filter( (o) => !o.attributes.internal From d014729ae2b7603d48d9e431095c02012a015d4a Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 12 Oct 2021 16:25:23 -0400 Subject: [PATCH 098/287] Add APM and Synthetics to auto update package list (#114641) --- x-pack/plugins/fleet/common/constants/epm.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/fleet/common/constants/epm.ts b/x-pack/plugins/fleet/common/constants/epm.ts index e482db6ae73ab..131cc276fc073 100644 --- a/x-pack/plugins/fleet/common/constants/epm.ts +++ b/x-pack/plugins/fleet/common/constants/epm.ts @@ -13,6 +13,8 @@ export const FLEET_SYSTEM_PACKAGE = 'system'; export const FLEET_ELASTIC_AGENT_PACKAGE = 'elastic_agent'; export const FLEET_SERVER_PACKAGE = 'fleet_server'; export const FLEET_ENDPOINT_PACKAGE = 'endpoint'; +export const FLEET_APM_PACKAGE = 'apm'; +export const FLEET_SYNTHETICS_PACKAGE = 'synthetics'; /* Package rules: @@ -37,7 +39,11 @@ export const unremovablePackages = [ export const defaultPackages = unremovablePackages.filter((p) => p !== FLEET_ENDPOINT_PACKAGE); -export const autoUpdatePackages = [FLEET_ENDPOINT_PACKAGE]; +export const autoUpdatePackages = [ + FLEET_ENDPOINT_PACKAGE, + FLEET_APM_PACKAGE, + FLEET_SYNTHETICS_PACKAGE, +]; export const agentAssetTypes = { Input: 'input', From 7290b6936a4b6647001e1f344b8f56b226eb41f6 Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Tue, 12 Oct 2021 13:58:12 -0700 Subject: [PATCH 099/287] [ci] Updates APM server (#114588) Signed-off-by: Tyler Smalley --- .buildkite/scripts/common/env.sh | 7 ++++++- packages/kbn-apm-config-loader/src/config.test.ts | 8 ++++---- packages/kbn-apm-config-loader/src/config.ts | 4 ++-- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index ac80a66d33fa0..cd33cdc714cbe 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -36,7 +36,12 @@ export ELASTIC_APM_ENVIRONMENT=ci export ELASTIC_APM_TRANSACTION_SAMPLE_RATE=0.1 if is_pr; then - export ELASTIC_APM_ACTIVE=false + if [[ "${GITHUB_PR_LABELS:-}" == *"ci:collect-apm"* ]]; then + export ELASTIC_APM_ACTIVE=true + else + export ELASTIC_APM_ACTIVE=false + fi + export CHECKS_REPORTER_ACTIVE=true # These can be removed once we're not supporting Jenkins and Buildkite at the same time diff --git a/packages/kbn-apm-config-loader/src/config.test.ts b/packages/kbn-apm-config-loader/src/config.test.ts index 4e4dbf81740b3..60d773e3a420b 100644 --- a/packages/kbn-apm-config-loader/src/config.test.ts +++ b/packages/kbn-apm-config-loader/src/config.test.ts @@ -94,8 +94,8 @@ describe('ApmConfiguration', () => { "globalLabels": Object {}, "logUncaughtExceptions": true, "metricsInterval": "30s", - "secretToken": "ZQHYvrmXEx04ozge8F", - "serverUrl": "https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io", + "secretToken": "7YKhoXsO4MzjhXjx2c", + "serverUrl": "https://kibana-ci-apm.apm.us-central1.gcp.cloud.es.io", "serviceName": "serviceName", "serviceVersion": "8.0.0", "transactionSampleRate": 1, @@ -117,8 +117,8 @@ describe('ApmConfiguration', () => { }, "logUncaughtExceptions": true, "metricsInterval": "120s", - "secretToken": "ZQHYvrmXEx04ozge8F", - "serverUrl": "https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io", + "secretToken": "7YKhoXsO4MzjhXjx2c", + "serverUrl": "https://kibana-ci-apm.apm.us-central1.gcp.cloud.es.io", "serviceName": "serviceName", "serviceVersion": "8.0.0", "transactionSampleRate": 1, diff --git a/packages/kbn-apm-config-loader/src/config.ts b/packages/kbn-apm-config-loader/src/config.ts index ad2fd63f0fec4..999e4ce3a6805 100644 --- a/packages/kbn-apm-config-loader/src/config.ts +++ b/packages/kbn-apm-config-loader/src/config.ts @@ -23,14 +23,14 @@ const DEFAULT_CONFIG: AgentConfigOptions = { }; const CENTRALIZED_SERVICE_BASE_CONFIG: AgentConfigOptions = { - serverUrl: 'https://38b80fbd79fb4c91bae06b4642d4d093.apm.us-east-1.aws.cloud.es.io', + serverUrl: 'https://kibana-ci-apm.apm.us-central1.gcp.cloud.es.io', // The secretToken below is intended to be hardcoded in this file even though // it makes it public. This is not a security/privacy issue. Normally we'd // instead disable the need for a secretToken in the APM Server config where // the data is transmitted to, but due to how it's being hosted, it's easier, // for now, to simply leave it in. - secretToken: 'ZQHYvrmXEx04ozge8F', + secretToken: '7YKhoXsO4MzjhXjx2c', centralConfig: false, metricsInterval: '30s', From 636581ac26d540411ec4d385b04125515f0bd0b6 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Tue, 12 Oct 2021 14:04:41 -0700 Subject: [PATCH 100/287] [Reporting/Docs] remove note about host name set to "0" (#114710) --- docs/settings/reporting-settings.asciidoc | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index 560f2d850c6d5..af10430ef8d01 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -79,11 +79,6 @@ The protocol for accessing {kib}, typically `http` or `https`. [[xpack-kibanaServer-hostname]] `xpack.reporting.kibanaServer.hostname`:: The hostname for accessing {kib}, if different from the <> value. -NOTE: Reporting authenticates requests on the {kib} page only when the hostname matches the -<> setting. Therefore Reporting fails if the -set value redirects to another server. For that reason, `"0"` is an invalid setting -because, in the Reporting browser, it becomes an automatic redirect to `"0.0.0.0"`. - [float] [[reporting-job-queue-settings]] ==== Background job settings From a51545ecc3ac6fc640dd901252535d2efb82d85b Mon Sep 17 00:00:00 2001 From: Bree Hall <40739624+breehall@users.noreply.github.com> Date: Tue, 12 Oct 2021 17:11:04 -0400 Subject: [PATCH 101/287] Bumping EUI to 39.0.0 (#113633) * Upgraded the version of EUI to 38.2.0 from 38.0.1 * Updated the i18n mappings required for EUI v.38.2.0 * Update i18n snapshots and resolve linting error * Removed html_id_generator mocks. Current mock was failing due to missing useGeneratedHtmlId export. This is safe to remove because EUI contains a .testenv that contains an mock for html_id_generator. More info at https://github.com/elastic/eui/blob/master/src/services/accessibility/html_id_generator.testenv.ts * Resolve linting error in i18n mapping file * Removed html_id_generator mocks. Current mock was failing due to missing useGeneratedHtmlId export. This is safe to remove because EUI contains a .testenv that contains a mock for html_id_generator. More info at https://github.com/elastic/eui/blob/master/src/services/accessibility/html_id_generator.testenv.ts * Update plugin snapshots * Resolve merge conflict in license_checker config.ts file * Upgrade EUI to version 39.0.0 from the original target (38.2.0) to handle an issue found with a functional test during the original upgrade * Updated the i18n mapping for EUI v.39.0.0 * Update various snapshots to account for the an i18n translation token addition in EUI v. 39.0.0 * Updated test cases marked as obsolete by CI * Update src/dev/license_checker/config.ts Removing TODO comments from src/dev/license_checker/config.ts as they are no longer needed. Co-authored-by: Constance Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Constance --- package.json | 2 +- .../__snapshots__/status_table.test.tsx.snap | 7 ++- .../__snapshots__/i18n_service.test.tsx.snap | 7 ++- src/core/public/i18n/i18n_eui_mapping.tsx | 23 +++++++-- src/dev/license_checker/config.ts | 2 +- .../__snapshots__/data_view.test.tsx.snap | 14 +++++- .../cron_editor/cron_editor.test.tsx | 6 --- .../client_integration/helpers/jest.mocks.tsx | 6 --- .../color/__snapshots__/color.test.tsx.snap | 21 ++++++-- .../__snapshots__/static_lookup.test.tsx.snap | 14 +++++- .../url/__snapshots__/url.test.tsx.snap | 2 +- .../editors/url/url.test.tsx | 6 --- .../__snapshots__/samples.test.tsx.snap | 7 ++- .../__snapshots__/table.test.tsx.snap | 14 +++++- .../error_group_overview/List/List.test.tsx | 6 --- .../List/__snapshots__/List.test.tsx.snap | 6 +-- .../shared/Stacktrace/Stackframe.test.tsx | 6 --- .../__snapshots__/managed_table.test.tsx.snap | 14 +++++- .../asset_manager.stories.storyshot | 8 +-- .../expression_input.stories.storyshot | 2 +- .../file_upload.stories.storyshot | 4 +- .../workpad_table.stories.storyshot | 14 +++--- .../footer/settings/settings.test.tsx | 6 +-- .../canvas/storybook/storyshots.test.tsx | 7 --- .../__jest__/add_license.test.js | 5 -- .../__jest__/request_trial_extension.test.js | 5 -- .../__jest__/revert_to_basic.test.js | 5 -- .../__jest__/start_trial.test.js | 6 --- .../__snapshots__/latest_active.test.js.snap | 7 ++- .../__snapshots__/latest_types.test.js.snap | 7 ++- .../latest_versions.test.js.snap | 7 ++- .../logs/__snapshots__/logs.test.js.snap | 7 ++- .../report_listing.test.tsx.snap | 50 +++++++++---------- .../public/management/report_listing.test.tsx | 6 --- .../__snapshots__/index.test.tsx.snap | 21 ++++++-- .../view/trusted_apps_page.test.tsx | 4 -- .../translations/translations/ja-JP.json | 2 - .../translations/translations/zh-CN.json | 2 - .../__snapshots__/donut_chart.test.tsx.snap | 12 ++++- .../location_status_tags.test.tsx | 6 --- .../monitor_list/monitor_list.test.tsx | 6 --- yarn.lock | 8 +-- 42 files changed, 204 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index ce8e928688805..fa03604178e27 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.21", "@elastic/ems-client": "7.15.0", - "@elastic/eui": "38.0.1", + "@elastic/eui": "39.0.0", "@elastic/filesaver": "1.1.2", "@elastic/maki": "6.3.0", "@elastic/node-crypto": "1.2.1", diff --git a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap index 16b67bfa0584f..5b0e831286aad 100644 --- a/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap +++ b/src/core/public/core_app/status/components/__snapshots__/status_table.test.tsx.snap @@ -35,7 +35,12 @@ exports[`StatusTable renders when statuses is provided 1`] = ` }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} rowProps={[Function]} tableLayout="fixed" diff --git a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap index 54e223cdc5d41..d714f2159d1a2 100644 --- a/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap +++ b/src/core/public/i18n/__snapshots__/i18n_service.test.tsx.snap @@ -7,6 +7,7 @@ exports[`#start() returns \`Context\` component 1`] = ` Object { "mapping": Object { "euiAccordion.isLoading": "Loading", + "euiBasicTable.noItemsMessage": "No items found", "euiBasicTable.selectAllRows": "Select all rows", "euiBasicTable.selectThisRow": "Select this row", "euiBasicTable.tableAutoCaptionWithPagination": [Function], @@ -120,13 +121,15 @@ exports[`#start() returns \`Context\` component 1`] = ` "euiLink.external.ariaLabel": "External link", "euiLink.newTarget.screenReaderOnlyText": "(opens in a new tab or window)", "euiMarkdownEditorFooter.closeButton": "Close", - "euiMarkdownEditorFooter.descriptionPrefix": "This editor uses", - "euiMarkdownEditorFooter.descriptionSuffix": "You can also utilize these additional syntax plugins to add rich content to your text.", "euiMarkdownEditorFooter.errorsTitle": "Errors", + "euiMarkdownEditorFooter.mdSyntaxLink": "GitHub flavored markdown", "euiMarkdownEditorFooter.openUploadModal": "Open upload files modal", "euiMarkdownEditorFooter.showMarkdownHelp": "Show markdown help", "euiMarkdownEditorFooter.showSyntaxErrors": "Show errors", "euiMarkdownEditorFooter.supportedFileTypes": [Function], + "euiMarkdownEditorFooter.syntaxModalDescriptionPrefix": "This editor uses", + "euiMarkdownEditorFooter.syntaxModalDescriptionSuffix": "You can also utilize these additional syntax plugins to add rich content to your text.", + "euiMarkdownEditorFooter.syntaxPopoverDescription": "This editor uses", "euiMarkdownEditorFooter.syntaxTitle": "Syntax help", "euiMarkdownEditorFooter.unsupportedFileType": "File type not supported", "euiMarkdownEditorFooter.uploadingFiles": "Click to upload files", diff --git a/src/core/public/i18n/i18n_eui_mapping.tsx b/src/core/public/i18n/i18n_eui_mapping.tsx index 2fe9657bce8c9..7585ada886c05 100644 --- a/src/core/public/i18n/i18n_eui_mapping.tsx +++ b/src/core/public/i18n/i18n_eui_mapping.tsx @@ -68,6 +68,9 @@ export const getEuiContextMapping = (): EuiTokensObject => { values: { tableCaption }, description: 'Screen reader text to describe the pagination controls', }), + 'euiBasicTable.noItemsMessage': i18n.translate('core.euiBasicTable.noItemsMessage', { + defaultMessage: 'No items found', + }), 'euiBottomBar.customScreenReaderAnnouncement': ({ landmarkHeading }: EuiValues) => i18n.translate('core.euiBottomBar.customScreenReaderAnnouncement', { defaultMessage: @@ -634,19 +637,31 @@ export const getEuiContextMapping = (): EuiTokensObject => { defaultMessage: 'Syntax help', } ), - 'euiMarkdownEditorFooter.descriptionPrefix': i18n.translate( - 'core.euiMarkdownEditorFooter.descriptionPrefix', + 'euiMarkdownEditorFooter.mdSyntaxLink': i18n.translate( + 'core.euiMarkdownEditorFooter.mdSyntaxLink', + { + defaultMessage: 'GitHub flavored markdown', + } + ), + 'euiMarkdownEditorFooter.syntaxModalDescriptionPrefix': i18n.translate( + 'core.euiMarkdownEditorFooter.syntaxModalDescriptionPrefix', { defaultMessage: 'This editor uses', } ), - 'euiMarkdownEditorFooter.descriptionSuffix': i18n.translate( - 'core.euiMarkdownEditorFooter.descriptionSuffix', + 'euiMarkdownEditorFooter.syntaxModalDescriptionSuffix': i18n.translate( + 'core.euiMarkdownEditorFooter.syntaxModalDescriptionSuffix', { defaultMessage: 'You can also utilize these additional syntax plugins to add rich content to your text.', } ), + 'euiMarkdownEditorFooter.syntaxPopoverDescription': i18n.translate( + 'core.euiMarkdownEditorFooter.syntaxPopoverDescription', + { + defaultMessage: 'This editor uses', + } + ), 'euiMarkdownEditorToolbar.editor': i18n.translate('core.euiMarkdownEditorToolbar.editor', { defaultMessage: 'Editor', }), diff --git a/src/dev/license_checker/config.ts b/src/dev/license_checker/config.ts index e4f1c6a2d2e01..818ea3675194e 100644 --- a/src/dev/license_checker/config.ts +++ b/src/dev/license_checker/config.ts @@ -75,6 +75,6 @@ export const LICENSE_OVERRIDES = { 'jsts@1.6.2': ['Eclipse Distribution License - v 1.0'], // cf. https://github.com/bjornharrtell/jsts '@mapbox/jsonlint-lines-primitives@2.0.2': ['MIT'], // license in readme https://github.com/tmcw/jsonlint '@elastic/ems-client@7.15.0': ['Elastic License 2.0'], - '@elastic/eui@38.0.1': ['SSPL-1.0 OR Elastic License 2.0'], + '@elastic/eui@39.0.0': ['SSPL-1.0 OR Elastic License 2.0'], 'language-subtag-registry@0.3.21': ['CC-BY-4.0'], // retired ODC‑By license https://github.com/mattcg/language-subtag-registry }; diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index 9cd0687a1074d..eae2032748396 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -884,7 +884,12 @@ exports[`Inspector Data View component should render single table without select }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { @@ -2449,7 +2454,12 @@ exports[`Inspector Data View component should support multiple datatables 1`] = }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { diff --git a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx index 26ef7483bbbbd..0ae82872124a4 100644 --- a/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx +++ b/src/plugins/es_ui_shared/public/components/cron_editor/cron_editor.test.tsx @@ -14,12 +14,6 @@ import { mountWithI18nProvider } from '@kbn/test/jest'; import { Frequency } from './types'; import { CronEditor } from './cron_editor'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - describe('CronEditor', () => { ['MINUTE', 'HOUR', 'DAY', 'WEEK', 'MONTH', 'YEAR'].forEach((unit) => { test(`is rendered with a ${unit} frequency`, () => { diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx index e291ec7b4ca08..d33a0d2a87fb5 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/helpers/jest.mocks.tsx @@ -9,12 +9,6 @@ import React from 'react'; const EDITOR_ID = 'testEditor'; -jest.mock('@elastic/eui/lib/services/accessibility', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - jest.mock('@elastic/eui', () => { const original = jest.requireActual('@elastic/eui'); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap index 7406e5ae9bb2d..1cff82729e6f9 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/color/__snapshots__/color.test.tsx.snap @@ -76,7 +76,12 @@ exports[`ColorFormatEditor should render multiple colors 1`] = ` }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} tableLayout="fixed" /> @@ -170,7 +175,12 @@ exports[`ColorFormatEditor should render other type normally (range field) 1`] = }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} tableLayout="fixed" /> @@ -264,7 +274,12 @@ exports[`ColorFormatEditor should render string type normally (regex field) 1`] }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} tableLayout="fixed" /> diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap index 664912789b0e3..3b476a6037aed 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/static_lookup/__snapshots__/static_lookup.test.tsx.snap @@ -55,7 +55,12 @@ exports[`StaticLookupFormatEditor should render multiple lookup entries and unkn }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} style={ Object { @@ -159,7 +164,12 @@ exports[`StaticLookupFormatEditor should render normally 1`] = ` }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} style={ Object { diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index 79c1a11cfef84..3890d6c2b9ddb 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -292,7 +292,7 @@ exports[`UrlFormatEditor should render normally 1`] = `

{ - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - const fieldType = 'string'; const format = { getConverterFor: jest diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap index 1a0b96c14fe35..71693a1e5cb8c 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/samples/__snapshots__/samples.test.tsx.snap @@ -52,7 +52,12 @@ exports[`FormatEditorSamples should render normally 1`] = ` }, ] } - noItemsMessage="No items found" + noItemsMessage={ + + } responsive={true} tableLayout="fixed" /> diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap index 8325e7dc886e8..bca54ff67591c 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/table.test.tsx.snap @@ -188,7 +188,12 @@ exports[`Table prevents saved objects from being deleted 1`] = ` ] } loading={false} - noItemsMessage="No items found" + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { @@ -403,7 +408,12 @@ exports[`Table should render normally 1`] = ` ] } loading={false} - noItemsMessage="No items found" + noItemsMessage={ + + } onChange={[Function]} pagination={ Object { diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx index a2a92b7e16f8e..12fa1c955ccc8 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/List.test.tsx @@ -15,12 +15,6 @@ import props from './__fixtures__/props.json'; import { MemoryRouter } from 'react-router-dom'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => { - return { - htmlIdGenerator: () => () => `generated-id`, - }; -}); - describe('ErrorGroupOverview -> List', () => { beforeAll(() => { mockMoment(); diff --git a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap index 890c692096a66..c8c7bf82dff04 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/error_group_overview/List/__snapshots__/List.test.tsx.snap @@ -56,7 +56,7 @@ exports[`ErrorGroupOverview -> List should render empty state 1`] = `
List should render with data 1`] = `
List should render with data 1`] = ` className="euiPagination__item" >