diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts index 9e21c45f75b4b..c8880b9bfe678 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/basic_pretty_printer.test.ts @@ -78,15 +78,6 @@ describe('single line query', () => { }); }); - describe('SHOW', () => { - /** @todo Enable once show command args are parsed as columns. */ - test.skip('info page', () => { - const { text } = reprint('SHOW info'); - - expect(text).toBe('SHOW info'); - }); - }); - describe('STATS', () => { test('with aggregates assignment', () => { const { text } = reprint('FROM a | STATS var = agg(123, fn(true))'); @@ -100,6 +91,30 @@ describe('single line query', () => { expect(text).toBe('FROM a | STATS A(1), B(2) BY asdf'); }); }); + + describe('GROK', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM search-movies | GROK Awards "text"'); + + expect(text).toBe('FROM search-movies | GROK Awards "text"'); + }); + }); + + describe('DISSECT', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM index | DISSECT input "pattern"'); + + expect(text).toBe('FROM index | DISSECT input "pattern"'); + }); + + test('with APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT input "pattern" APPEND_SEPARATOR=""' + ); + + expect(text).toBe('FROM index | DISSECT input "pattern" APPEND_SEPARATOR = ""'); + }); + }); }); describe('expressions', () => { diff --git a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts index 2dfe239ce5b88..6422ae9a451af 100644 --- a/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts +++ b/packages/kbn-esql-ast/src/pretty_print/__tests__/wrapping_pretty_printer.test.ts @@ -19,6 +19,83 @@ const reprint = (src: string, opts?: WrappingPrettyPrinterOptions) => { return { text }; }; +describe('commands', () => { + describe('GROK', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM search-movies | GROK Awards "text"'); + + expect(text).toBe('FROM search-movies | GROK Awards "text"'); + }); + + test('two long arguments', () => { + const { text } = reprint( + 'FROM search-movies | GROK AwardsAwardsAwardsAwardsAwardsAwardsAwardsAwards "texttexttexttexttexttexttexttexttexttexttexttexttexttexttext"' + ); + + expect('\n' + text).toBe(` +FROM search-movies + | GROK + AwardsAwardsAwardsAwardsAwardsAwardsAwardsAwards + "texttexttexttexttexttexttexttexttexttexttexttexttexttexttext"`); + }); + }); + + describe('DISSECT', () => { + test('two basic arguments', () => { + const { text } = reprint('FROM index | DISSECT input "pattern"'); + + expect(text).toBe('FROM index | DISSECT input "pattern"'); + }); + + test('two long arguments', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern"' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern"`); + }); + + test('with APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT input "pattern" APPEND_SEPARATOR=""' + ); + + expect(text).toBe('FROM index | DISSECT input "pattern" APPEND_SEPARATOR = ""'); + }); + + test('two long arguments with short APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" APPEND_SEPARATOR="sep"' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" + APPEND_SEPARATOR = "sep"`); + }); + + test('two long arguments with long APPEND_SEPARATOR option', () => { + const { text } = reprint( + 'FROM index | DISSECT InputInputInputInputInputInputInputInputInputInputInputInputInputInput "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" APPEND_SEPARATOR=""' + ); + + expect('\n' + text).toBe(` +FROM index + | DISSECT + InputInputInputInputInputInputInputInputInputInputInputInputInputInput + "PatternPatternPatternPatternPatternPatternPatternPatternPatternPattern" + APPEND_SEPARATOR = + ""`); + }); + }); +}); + describe('casing', () => { test('can chose command name casing', () => { const query = 'FROM index | WHERE a == 123'; diff --git a/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts index 2f1e3439cd3a3..cf252825c243f 100644 --- a/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/basic_pretty_printer.ts @@ -19,6 +19,7 @@ import { import { ESQLAstBaseItem, ESQLAstCommand, ESQLAstQueryExpression } from '../types'; import { ESQLAstExpressionNode, Visitor } from '../visitor'; import { resolveItem } from '../visitor/utils'; +import { commandOptionsWithEqualsSeparator, commandsWithNoCommaArgSeparator } from './constants'; import { LeafPrinter } from './leaf_printer'; export interface BasicPrettyPrinterOptions { @@ -378,7 +379,8 @@ export class BasicPrettyPrinter { args += (args ? ', ' : '') + arg; } - const argsFormatted = args ? ` ${args}` : ''; + const separator = commandOptionsWithEqualsSeparator.has(ctx.node.name) ? ' = ' : ' '; + const argsFormatted = args ? `${separator}${args}` : ''; const optionFormatted = `${option}${argsFormatted}`; return optionFormatted; @@ -392,7 +394,10 @@ export class BasicPrettyPrinter { let options = ''; for (const source of ctx.visitArguments()) { - args += (args ? ', ' : '') + source; + const needsSeparator = !!args; + const needsComma = !commandsWithNoCommaArgSeparator.has(ctx.node.name); + const separator = needsSeparator ? (needsComma ? ',' : '') + ' ' : ''; + args += separator + source; } for (const option of ctx.visitOptions()) { diff --git a/packages/kbn-esql-ast/src/pretty_print/constants.ts b/packages/kbn-esql-ast/src/pretty_print/constants.ts new file mode 100644 index 0000000000000..01208af98d025 --- /dev/null +++ b/packages/kbn-esql-ast/src/pretty_print/constants.ts @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the "Elastic License + * 2.0", the "GNU Affero General Public License v3.0 only", and the "Server Side + * Public License v 1"; you may not use this file except in compliance with, at + * your election, the "Elastic License 2.0", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +/** + * This set tracks commands that don't use commas to separate their + * arguments. + * + * Normally ES|QL command arguments are separated by commas. + * + * ``` + * COMMAND arg1, arg2, arg3 + * ``` + * + * But there are some commands (namely `grok` and `dissect`) which don't + * use commas to separate their arguments. + * + * ``` + * GROK input "pattern" + * DISSECT input "pattern" + * ``` + */ +export const commandsWithNoCommaArgSeparator = new Set(['grok', 'dissect']); + +/** + * This set tracks command options which use an equals sign to separate + * the option label from the option value. + * + * Most ES|QL commands use a space to separate the option label from the + * option value. + * + * ``` + * COMMAND arg1, arg2, arg3 OPTION option + * FROM index METADATA _id + * ``` + * + * However, the `APPEND_SEPARATOR` in the `DISSECT` command uses an equals + * sign to separate the option label from the option value. + * + * ``` + * DISSECT input "pattern" APPEND_SEPARATOR = "separator" + * | + * | + * equals sign + * ``` + */ +export const commandOptionsWithEqualsSeparator = new Set(['append_separator']); diff --git a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts index 91f65a389f0c3..2f863524740ee 100644 --- a/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts +++ b/packages/kbn-esql-ast/src/pretty_print/wrapping_pretty_printer.ts @@ -20,6 +20,7 @@ import { } from '../visitor'; import { children, singleItems } from '../visitor/utils'; import { BasicPrettyPrinter, BasicPrettyPrinterOptions } from './basic_pretty_printer'; +import { commandOptionsWithEqualsSeparator, commandsWithNoCommaArgSeparator } from './constants'; import { getPrettyPrintStats } from './helpers'; import { LeafPrinter } from './leaf_printer'; @@ -259,6 +260,8 @@ export class WrappingPrettyPrinter { } } + const commaBetweenArgs = !commandsWithNoCommaArgSeparator.has(ctx.node.name); + if (!oneArgumentPerLine) { ARGS: for (const arg of singleItems(ctx.arguments())) { if (arg.type === 'option') { @@ -271,7 +274,8 @@ export class WrappingPrettyPrinter { if (formattedArgLength > largestArg) { largestArg = formattedArgLength; } - let separator = txt ? ',' : ''; + + let separator = txt ? (commaBetweenArgs ? ',' : '') : ''; let fragment = ''; if (needsWrap) { @@ -329,7 +333,7 @@ export class WrappingPrettyPrinter { const arg = ctx.visitExpression(args[i], { indent, remaining: this.opts.wrap - indent.length, - suffix: isLastArg ? '' : ',', + suffix: isLastArg ? '' : commaBetweenArgs ? ',' : '', }); const separator = isFirstArg ? '' : '\n'; const indentation = arg.indented ? '' : indent; @@ -557,8 +561,9 @@ export class WrappingPrettyPrinter { indent: inp.indent, remaining: inp.remaining - option.length - 1, }); - const argsFormatted = args.txt ? ` ${args.txt}` : ''; - const txt = `${option}${argsFormatted}`; + const argsFormatted = args.txt ? `${args.txt[0] === '\n' ? '' : ' '}${args.txt}` : ''; + const separator = commandOptionsWithEqualsSeparator.has(ctx.node.name) ? ' =' : ''; + const txt = `${option}${separator}${argsFormatted}`; return { txt, lines: args.lines }; }) diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index b0357853720cb..df8a077e844f6 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -33,7 +33,7 @@ pageLoadAssetSize: dataViewFieldEditor: 42021 dataViewManagement: 5300 dataViews: 65000 - dataVisualizer: 27530 + dataVisualizer: 30000 devTools: 38637 discover: 99999 discoverEnhanced: 42730 diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/create_field_stats_table.tsx b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/create_field_stats_table.tsx index 2c1254732c24a..6b788e4acab46 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/create_field_stats_table.tsx +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/create_field_stats_table.tsx @@ -5,29 +5,22 @@ * 2.0. */ +import React from 'react'; + import { i18n } from '@kbn/i18n'; import type { PresentationContainer } from '@kbn/presentation-containers'; -import { tracksOverlays } from '@kbn/presentation-containers'; import type { EmbeddableApiContext } from '@kbn/presentation-publishing'; import type { UiActionsActionDefinition } from '@kbn/ui-actions-plugin/public'; -import { IncompatibleActionError } from '@kbn/ui-actions-plugin/public'; import type { CoreStart } from '@kbn/core-lifecycle-browser'; -import { toMountPoint } from '@kbn/react-kibana-mount'; -import { KibanaContextProvider } from '@kbn/kibana-react-plugin/public'; -import React from 'react'; -import { isDefined } from '@kbn/ml-is-defined'; import { COMMON_VISUALIZATION_GROUPING } from '@kbn/visualizations-plugin/public'; import { ENABLE_ESQL } from '@kbn/esql-utils'; -import { FIELD_STATS_EMBEDDABLE_TYPE } from '../embeddables/field_stats/constants'; + import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin'; import type { FieldStatisticsTableEmbeddableApi, FieldStatsControlsApi, } from '../embeddables/field_stats/types'; -import { FieldStatsInitializerViewType } from '../embeddables/grid_embeddable/types'; import type { FieldStatsInitialState } from '../embeddables/grid_embeddable/types'; -import { getOrCreateDataViewByIndexPattern } from '../search_strategy/requests/get_data_view_by_index_pattern'; -import { FieldStatisticsInitializer } from '../embeddables/field_stats/field_stats_initializer'; const parentApiIsCompatible = async ( parentApi: unknown @@ -57,6 +50,21 @@ async function updatePanelFromFlyoutEdits({ initialState: FieldStatsInitialState; fieldStatsControlsApi?: FieldStatsControlsApi; }) { + const [ + { getOrCreateDataViewByIndexPattern }, + { FieldStatisticsInitializer }, + { tracksOverlays }, + { toMountPoint }, + { KibanaContextProvider }, + { isDefined }, + ] = await Promise.all([ + import('../search_strategy/requests/get_data_view_by_index_pattern'), + import('../embeddables/field_stats/field_stats_initializer'), + import('@kbn/presentation-containers'), + import('@kbn/react-kibana-mount'), + import('@kbn/kibana-react-plugin/public'), + import('@kbn/ml-is-defined'), + ]); const parentApi = api.parentApi; const overlayTracker = tracksOverlays(parentApi) ? parentApi : undefined; const services = { @@ -148,6 +156,16 @@ export function createAddFieldStatsTableAction( ); }, async execute(context) { + const [ + { IncompatibleActionError }, + { FIELD_STATS_EMBEDDABLE_TYPE }, + { FieldStatsInitializerViewType }, + ] = await Promise.all([ + import('@kbn/ui-actions-plugin/public'), + import('../embeddables/field_stats/constants'), + import('../embeddables/grid_embeddable/types'), + ]); + const presentationContainerParent = await parentApiIsCompatible(context.embeddable); if (!presentationContainerParent) throw new IncompatibleActionError(); diff --git a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts index faa8f34bdfbd7..ac2f73860e4fb 100644 --- a/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts +++ b/x-pack/plugins/data_visualizer/public/application/index_data_visualizer/ui_actions/index.ts @@ -8,14 +8,13 @@ import type { CoreStart } from '@kbn/core-lifecycle-browser'; import type { UiActionsSetup } from '@kbn/ui-actions-plugin/public'; import type { DataVisualizerStartDependencies } from '../../common/types/data_visualizer_plugin'; +import { createAddFieldStatsTableAction } from './create_field_stats_table'; export function registerDataVisualizerUiActions( uiActions: UiActionsSetup, coreStart: CoreStart, pluginStart: DataVisualizerStartDependencies ) { - import('./create_field_stats_table').then(({ createAddFieldStatsTableAction }) => { - const addFieldStatsAction = createAddFieldStatsTableAction(coreStart, pluginStart); - uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addFieldStatsAction); - }); + const addFieldStatsAction = createAddFieldStatsTableAction(coreStart, pluginStart); + uiActions.addTriggerAction('ADD_PANEL_TRIGGER', addFieldStatsAction); } diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx index 87026e0613296..8df04b23a5435 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.test.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { fireEvent, render, screen, within } from '@testing-library/react'; +import { fireEvent, render, screen, within, act } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { IncludeExcludeRow, IncludeExcludeRowProps } from './include_exclude_options'; @@ -152,4 +152,168 @@ describe('IncludeExcludeComponent', () => { }); expect(onUpdateSpy).toHaveBeenCalledTimes(2); }); + + it('should prevent identical include and exclude values on change when making single selections', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('ABC'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('ABC'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('ABC'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + }); + + it('should prevent identical include and exclude values on change when making multiple selections', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('ABC'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.click(screen.getByRole('option', { name: 'FEF' })); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('FEF'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.click(screen.getByRole('option', { name: 'ABC' })); + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('ABC'); + + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('ABC'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(4); + }); + + it('should prevent identical include and exclude values on create option', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Exclude values' }), 'test{enter}'); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('test'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('test'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + }); + + it('should prevent identical include and exclude values when creating multiple options', async () => { + renderIncludeExcludeRow({ + include: undefined, + exclude: undefined, + isNumberField: false, + tableRows, + }); + + await userEvent.click(screen.getByRole('combobox', { name: 'Include values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test'); + + await userEvent.type(screen.getByRole('combobox', { name: 'Include values' }), 'test1{enter}'); + expect(screen.getByTestId('lens-include-terms-combobox')).toHaveTextContent('test1'); + + await userEvent.click(screen.getByRole('combobox', { name: 'Exclude values' })); + await userEvent.type(screen.getByRole('combobox', { name: 'Exclude values' }), 'test1{enter}'); + expect(screen.getByTestId('lens-exclude-terms-combobox')).toHaveTextContent('test1'); + + expect(screen.getByTestId('lens-include-terms-combobox')).not.toHaveTextContent('test1'); + + expect(onUpdateSpy).toHaveBeenCalledTimes(4); + }); + + it('should prevent identical include value on exclude regex value change', async () => { + jest.useFakeTimers(); + + renderIncludeExcludeRow({ + include: [''], + exclude: [''], + includeIsRegex: true, + excludeIsRegex: true, + tableRows, + }); + + const includeRegexInput = screen.getByTestId('lens-include-terms-regex-input'); + const excludeRegexInput = screen.getByTestId('lens-exclude-terms-regex-input'); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + + await user.type(includeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(includeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('include', ['test.*'], 'includeIsRegex', true); + + await user.type(excludeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(excludeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', ['test.*'], 'excludeIsRegex', true); + + expect(includeRegexInput).toHaveValue(''); + expect(onUpdateSpy).toHaveBeenCalledWith('include', [''], 'includeIsRegex', true); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + + jest.useRealTimers(); + }); + + it('should prevent identical exclude value on include regex value change', async () => { + jest.useFakeTimers(); + + renderIncludeExcludeRow({ + include: [''], + exclude: [''], + includeIsRegex: true, + excludeIsRegex: true, + tableRows, + }); + + const includeRegexInput = screen.getByTestId('lens-include-terms-regex-input'); + const excludeRegexInput = screen.getByTestId('lens-exclude-terms-regex-input'); + const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime }); + + await user.type(excludeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(excludeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', ['test.*'], 'excludeIsRegex', true); + + await user.type(includeRegexInput, 'test.*'); + act(() => { + jest.advanceTimersByTime(256); + }); + expect(includeRegexInput).toHaveValue('test.*'); + expect(onUpdateSpy).toHaveBeenCalledWith('include', ['test.*'], 'includeIsRegex', true); + + expect(excludeRegexInput).toHaveValue(''); + expect(onUpdateSpy).toHaveBeenCalledWith('exclude', [''], 'excludeIsRegex', true); + + expect(onUpdateSpy).toHaveBeenCalledTimes(3); + jest.useRealTimers(); + }); }); diff --git a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx index 41f521088af94..b2a8abb62c1ae 100644 --- a/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx +++ b/x-pack/plugins/lens/public/datasources/form_based/operations/definitions/terms/include_exclude_options.tsx @@ -99,68 +99,77 @@ export const IncludeExcludeRow = ({ selectedOptions: IncludeExcludeOptions[], operation: 'include' | 'exclude' ) => { - const options = { - ...includeExcludeSelectedOptions, - [operation]: selectedOptions, - }; - setIncludeExcludeSelectedOptions(options); - const terms = selectedOptions.map((option) => { - if (!Number.isNaN(Number(option.label))) { - return Number(option.label); - } - return option.label; + const otherOperation = operation === 'include' ? 'exclude' : 'include'; + const otherSelectedOptions = includeExcludeSelectedOptions[otherOperation] ?? []; + const hasIdenticalOptions = selectedOptions.some((option) => { + return otherSelectedOptions.some((otherOption) => otherOption.label === option.label); }); - const param = `${operation}IsRegex`; - updateParams(operation, terms, param, false); - }; - - const onCreateOption = ( - searchValue: string, - flattenedOptions: IncludeExcludeOptions[] = [], - operation: 'include' | 'exclude' - ) => { - const newOption = { - label: searchValue, - }; - let includeExcludeOptions = []; + const otherSelectedNonIdenticalOptions = hasIdenticalOptions + ? otherSelectedOptions.filter( + (otherOption) => !selectedOptions.some((option) => option.label === otherOption.label) + ) + : otherSelectedOptions; - const includeORExcludeSelectedOptions = includeExcludeSelectedOptions[operation] ?? []; - includeExcludeOptions = [...includeORExcludeSelectedOptions, newOption]; const options = { - ...includeExcludeSelectedOptions, - [operation]: includeExcludeOptions, + [otherOperation]: otherSelectedNonIdenticalOptions, + [operation]: selectedOptions, }; setIncludeExcludeSelectedOptions(options); - const terms = includeExcludeOptions.map((option) => { - if (!Number.isNaN(Number(option.label))) { - return Number(option.label); - } - return option.label; - }); + const getTerms = (updatedSelectedOptions: IncludeExcludeOptions[]) => + updatedSelectedOptions.map((option) => { + if (!Number.isNaN(Number(option.label))) { + return Number(option.label); + } + return option.label; + }); + + const terms = getTerms(selectedOptions); const param = `${operation}IsRegex`; updateParams(operation, terms, param, false); + + if (hasIdenticalOptions) { + const otherTerms = getTerms(otherSelectedNonIdenticalOptions); + const otherParam = `${otherOperation}IsRegex`; + updateParams(otherOperation, otherTerms, otherParam, false); + } + }; + + const onCreateOption = (searchValue: string, operation: 'include' | 'exclude') => { + const newOption = { label: searchValue }; + const selectedOptions = [...(includeExcludeSelectedOptions[operation] ?? []), newOption]; + onChangeIncludeExcludeOptions(selectedOptions, operation); }; const onIncludeRegexChangeToDebounce = useCallback( (newIncludeValue: string | number | undefined) => { + const isEqualToExcludeValue = newIncludeValue === regex.exclude; + const excludeValue = isEqualToExcludeValue ? '' : regex.exclude; setRegex({ - ...regex, + exclude: excludeValue, include: newIncludeValue, }); updateParams('include', [newIncludeValue ?? ''], 'includeIsRegex', true); + if (isEqualToExcludeValue) { + updateParams('exclude', [''], 'excludeIsRegex', true); + } }, [regex, updateParams] ); const onExcludeRegexChangeToDebounce = useCallback( (newExcludeValue: string | number | undefined) => { + const isEqualToIncludeValue = newExcludeValue === regex.include; + const includeValue = isEqualToIncludeValue ? '' : regex.include; setRegex({ - ...regex, + include: includeValue, exclude: newExcludeValue, }); updateParams('exclude', [newExcludeValue ?? ''], 'excludeIsRegex', true); + if (isEqualToIncludeValue) { + updateParams('include', [''], 'includeIsRegex', true); + } }, [regex, updateParams] ); @@ -247,9 +256,7 @@ export const IncludeExcludeRow = ({ options={termsOptions} selectedOptions={includeExcludeSelectedOptions.include} onChange={(options) => onChangeIncludeExcludeOptions(options, 'include')} - onCreateOption={(searchValue, options) => - onCreateOption(searchValue, options, 'include') - } + onCreateOption={(searchValue) => onCreateOption(searchValue, 'include')} isClearable={true} data-test-subj="lens-include-terms-combobox" autoFocus @@ -300,6 +307,7 @@ export const IncludeExcludeRow = ({ defaultMessage: 'Enter a regex to filter values', } )} + data-test-subj="lens-exclude-terms-regex-input" value={excludeRegexValue} onChange={(e) => { onExcludeRegexValueChange(e.target.value); @@ -322,9 +330,7 @@ export const IncludeExcludeRow = ({ options={termsOptions} selectedOptions={includeExcludeSelectedOptions.exclude} onChange={(options) => onChangeIncludeExcludeOptions(options, 'exclude')} - onCreateOption={(searchValue, options) => - onCreateOption(searchValue, options, 'exclude') - } + onCreateOption={(searchValue) => onCreateOption(searchValue, 'exclude')} isClearable={true} data-test-subj="lens-exclude-terms-combobox" autoFocus diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss deleted file mode 100644 index 9f9f16dff7e13..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_chart_tooltip.scss +++ /dev/null @@ -1,47 +0,0 @@ -.mlChartTooltip { - @include euiToolTipStyle('s'); - @include euiFontSizeXS; - padding: 0; - transition: opacity $euiAnimSpeedNormal; - pointer-events: none; - user-select: none; - max-width: 512px; - - &__list { - margin: $euiSizeXS; - padding-bottom: $euiSizeXS; - } - - &__header { - font-weight: $euiFontWeightBold; - padding: $euiSizeXS ($euiSizeXS * 2); - margin-bottom: $euiSizeXS; - border-bottom: $euiBorderThin solid transparentize($euiBorderColor, .8); - } - - &__item { - display: flex; - padding: 3px; - box-sizing: border-box; - border-left: $euiSizeXS solid transparent; - } - - &__label { - min-width: 1px; - } - - &__value { - font-weight: $euiFontWeightBold; - text-align: right; - font-feature-settings: 'tnum'; - margin-left: 8px; - } - - &__rowHighlighted { - background-color: transparentize($euiColorGhost, .9); - } - - &--hidden { - opacity: 0; - } -} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss b/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss deleted file mode 100644 index 11b36a0a21001..0000000000000 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/_index.scss +++ /dev/null @@ -1 +0,0 @@ -@import 'chart_tooltip'; \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx index 0c6fe9095f4e2..f279175d01107 100644 --- a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip.tsx @@ -9,14 +9,14 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import classNames from 'classnames'; import TooltipTrigger from 'react-popper-tooltip'; +import type { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; + import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import type { TooltipValueFormatter } from '@elastic/charts'; -import './_index.scss'; - -import type { ChildrenArg, TooltipTriggerProps } from 'react-popper-tooltip/dist/types'; import type { ChartTooltipValue, TooltipData } from './chart_tooltip_service'; import { ChartTooltipService } from './chart_tooltip_service'; +import { useChartTooltipStyles } from './chart_tooltip_styles'; const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFormatter) => { if (!headerData) { @@ -30,17 +30,26 @@ const renderHeader = (headerData?: ChartTooltipValue, formatter?: TooltipValueFo * Pure component for rendering the tooltip content with a custom layout across the ML plugin. */ export const FormattedTooltip: FC<{ tooltipData: TooltipData }> = ({ tooltipData }) => { + const { + mlChartTooltip, + mlChartTooltipList, + mlChartTooltipHeader, + mlChartTooltipItem, + mlChartTooltipLabel, + mlChartTooltipValue, + } = useChartTooltipStyles(); + return ( -
+
{tooltipData.length > 0 && tooltipData[0].skipHeader === undefined && ( -
{renderHeader(tooltipData[0])}
+
{renderHeader(tooltipData[0])}
)} {tooltipData.length > 1 && ( -
+
{tooltipData .slice(1) .map(({ label, value, color, isHighlighted, seriesIdentifier, valueAccessor }) => { - const classes = classNames('mlChartTooltip__item', { + const classes = classNames({ // eslint-disable-next-line @typescript-eslint/naming-convention echTooltip__rowHighlighted: isHighlighted, }); @@ -52,16 +61,21 @@ export const FormattedTooltip: FC<{ tooltipData: TooltipData }> = ({ tooltipData return (
- + {label} - + {renderValue} diff --git a/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts new file mode 100644 index 0000000000000..c53bdb5242f3c --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/chart_tooltip/chart_tooltip_styles.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { css } from '@emotion/react'; + +import { mathWithUnits, transparentize, useEuiTheme } from '@elastic/eui'; +// @ts-expect-error style types not defined +import { euiToolTipStyles } from '@elastic/eui/lib/components/tool_tip/tool_tip.styles'; + +import { useCurrentEuiThemeVars } from '@kbn/ml-kibana-theme'; + +import { useMlKibana } from '../../contexts/kibana'; + +export const useChartTooltipStyles = () => { + const euiThemeContext = useEuiTheme(); + const { + services: { theme }, + } = useMlKibana(); + const { euiTheme } = useCurrentEuiThemeVars(theme); + const euiStyles = euiToolTipStyles(euiThemeContext); + + return { + mlChartTooltip: css([ + euiStyles.euiToolTip, + { + fontSize: euiTheme.euiFontSizeXS, + padding: 0, + transition: `opacity ${euiTheme.euiAnimSpeedNormal}`, + pointerEvents: 'none', + userSelect: 'none', + maxWidth: '512px', + position: 'relative', + }, + ]), + mlChartTooltipList: css({ + margin: euiTheme.euiSizeXS, + paddingBottom: euiTheme.euiSizeXS, + }), + mlChartTooltipHeader: css({ + fontWeight: euiTheme.euiFontWeightBold, + padding: `${euiTheme.euiSizeXS} ${mathWithUnits(euiTheme.euiSizeS, (x) => x * 2)}`, + marginBottom: euiTheme.euiSizeXS, + borderBottom: `1px solid ${transparentize(euiTheme.euiBorderColor, 0.8)}`, + }), + mlChartTooltipItem: css({ + display: 'flex', + padding: '3px', + boxSizing: 'border-box', + borderLeft: `${euiTheme.euiSizeXS} solid transparent`, + }), + mlChartTooltipLabel: css({ + minWidth: '1px', + }), + mlChartTooltipValue: css({ + fontWeight: euiTheme.euiFontWeightBold, + textAlign: 'right', + fontFeatureSettings: 'tnum', + marginLeft: '8px', + }), + }; +}; diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss deleted file mode 100644 index 322cdb4971f05..0000000000000 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.scss +++ /dev/null @@ -1,8 +0,0 @@ -.mlScatterplotMatrix { - overflow-x: auto; - - .vega-bind span { - font-size: $euiFontSizeXS; - padding: 0 $euiSizeXS; - } -} \ No newline at end of file diff --git a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx index dab7dc4117083..763addd4aaa87 100644 --- a/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx +++ b/x-pack/plugins/ml/public/application/components/scatterplot_matrix/scatterplot_matrix.tsx @@ -7,6 +7,7 @@ import type { FC } from 'react'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { css } from '@emotion/react'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import type { EuiComboBoxOptionOption } from '@elastic/eui'; @@ -35,6 +36,7 @@ import { type RuntimeMappings, } from '@kbn/ml-runtime-field-utils'; import { getProcessedFields } from '@kbn/ml-data-grid'; +import { euiThemeVars } from '@kbn/ui-theme'; import { useCurrentThemeVars, useMlApi, useMlKibana } from '../../contexts/kibana'; @@ -48,7 +50,17 @@ import { OUTLIER_SCORE_FIELD, } from './scatterplot_matrix_vega_lite_spec'; -import './scatterplot_matrix.scss'; +const cssOverrides = css({ + // Prevent the chart from overflowing the container + overflowX: 'auto', + // Overrides for the outlier threshold slider + '.vega-bind': { + span: { + fontSize: euiThemeVars.euiFontSizeXS, + padding: `0 ${euiThemeVars.euiSizeXS}`, + }, + }, +}); const SCATTERPLOT_MATRIX_DEFAULT_FIELDS = 4; const SCATTERPLOT_MATRIX_DEFAULT_FETCH_SIZE = 1000; @@ -413,7 +425,7 @@ export const ScatterplotMatrix: FC = ({ ) : (
diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts index 196ffc71162db..8ba1de0bb70d9 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/audit_log.ts @@ -27,7 +27,12 @@ export function registerActionAuditLogRoutes( .get({ access: 'public', path: ENDPOINT_ACTION_LOG_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts index 96b466a251cf4..1d524b08aefce 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/details.ts @@ -32,7 +32,12 @@ export const registerActionDetailsRoutes = ( .get({ access: 'public', path: ACTION_DETAILS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts index 2e16c57886f7d..29aa6f4bba3d8 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_download_handler.ts @@ -38,7 +38,12 @@ export const registerActionFileDownloadRoutes = ( // we need to enable setting the version number via query params enableQueryVersion: true, path: ACTION_AGENT_FILE_DOWNLOAD_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts index 1cb4e95e1eaf1..63118a64fc453 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/file_info_handler.ts @@ -74,7 +74,12 @@ export const registerActionFileInfoRoute = ( .get({ access: 'public', path: ACTION_AGENT_FILE_INFO_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts index a858909f5e2ed..05e5d77eb945b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/list.ts @@ -30,7 +30,12 @@ export function registerActionListRoutes( .get({ access: 'public', path: BASE_ENDPOINT_ACTION_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts index b6eb2376bd1cb..0fc90c7589b99 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/response_actions.ts @@ -80,7 +80,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: ISOLATE_HOST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -99,7 +104,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UNISOLATE_HOST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -119,7 +129,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: ISOLATE_HOST_ROUTE_V2, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -139,7 +154,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UNISOLATE_HOST_ROUTE_V2, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -159,7 +179,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: KILL_PROCESS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -182,7 +207,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: SUSPEND_PROCESS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -205,7 +235,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: GET_PROCESSES_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -225,7 +260,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: GET_FILE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -245,7 +285,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: EXECUTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -265,9 +310,14 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: UPLOAD_ROUTE, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, options: { authRequired: true, - tags: ['access:securitySolution'], + body: { accepts: ['multipart/form-data'], output: 'stream', @@ -293,7 +343,12 @@ export function registerResponseActionRoutes( .post({ access: 'public', path: SCAN_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts index 3ea4d9fa35753..c2d5b850d6bb2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/state.ts @@ -32,7 +32,12 @@ export function registerActionStateRoutes( .get({ access: 'public', path: ACTION_STATE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts index 6172bf07d2320..8a245dfb451ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/status.ts @@ -29,7 +29,12 @@ export function registerActionStatusRoutes( .get({ access: 'public', path: ACTION_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts index e6ea2f7595785..7ca24156b45fd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/agent/agent_status_handler.ts @@ -27,7 +27,12 @@ export const registerAgentStatusRoute = ( .get({ access: 'internal', path: AGENT_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 2f6e46d1d7727..3f028719fe5ad 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -54,7 +54,12 @@ export function registerEndpointRoutes( .get({ access: 'public', path: HOST_METADATA_LIST_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -94,7 +99,12 @@ export function registerEndpointRoutes( .get({ access: 'public', path: METADATA_TRANSFORMS_STATUS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) @@ -114,7 +124,12 @@ export function registerEndpointRoutes( .get({ access: 'internal', path: METADATA_TRANSFORMS_STATUS_INTERNAL_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 00054964e4401..6010c56557273 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -240,8 +240,8 @@ describe('test endpoint routes', () => { }); expect(routeConfig.options).toEqual({ authRequired: true, - tags: ['access:securitySolution'], }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const endpointResultList = mockResponse.ok.mock.calls[0][0]?.body as MetadataListResponse; expect(endpointResultList.data.length).toEqual(1); @@ -614,8 +614,8 @@ describe('test endpoint routes', () => { expect(esClientMock.transform.getTransformStats).toHaveBeenCalledTimes(1); expect(routeConfig.options).toEqual({ authRequired: true, - tags: ['access:securitySolution'], }); + expect(routeConfig.security?.authz).toEqual({ requiredPrivileges: ['securitySolution'] }); expect(mockResponse.ok).toBeCalled(); const response = mockResponse.ok.mock.calls[0][0]?.body as TransformGetTransformStatsResponse; expect(response.count).toEqual(expectedResponse.count); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts index 7b28ccfcf9fe7..4355684407bb1 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/protection_updates_note/index.ts @@ -25,7 +25,12 @@ export function registerProtectionUpdatesNoteRoutes( .post({ access: 'public', path: PROTECTION_UPDATES_NOTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { @@ -45,7 +50,12 @@ export function registerProtectionUpdatesNoteRoutes( .get({ access: 'public', path: PROTECTION_UPDATES_NOTE_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts index 677fb004ee862..bbee33114534b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/suggestions/index.ts @@ -42,7 +42,12 @@ export function registerEndpointSuggestionsRoutes( .post({ access: 'public', path: SUGGESTIONS_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, // @ts-expect-error TODO(https://github.com/elastic/kibana/issues/196095): Replace {RouteDeprecationInfo} deprecated: true, }) @@ -64,7 +69,12 @@ export function registerEndpointSuggestionsRoutes( .post({ access: 'internal', path: SUGGESTIONS_INTERNAL_ROUTE, - options: { authRequired: true, tags: ['access:securitySolution'] }, + security: { + authz: { + requiredPrivileges: ['securitySolution'], + }, + }, + options: { authRequired: true }, }) .addVersion( { diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts new file mode 100644 index 0000000000000..d7a36e3e447b7 --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) { + describe('entities', () => { + loadTestFile(require.resolve('./service_logs_error_rate_timeseries.spec.ts')); + loadTestFile(require.resolve('./service_logs_rate_timeseries.spec.ts')); + }); +} diff --git a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts similarity index 85% rename from x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts rename to x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts index 282039b8957c9..f6e167db0318e 100644 --- a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_error_rate_timeseries.spec.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_error_rate_timeseries.spec.ts @@ -9,12 +9,12 @@ import { log, timerange } from '@kbn/apm-synthtrace-client'; import { first, last } from 'lodash'; import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const logSynthtrace = getService('logSynthtraceEsClient'); +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); const serviceName = 'synth-go'; const start = new Date('2024-01-01T00:00:00.000Z').getTime(); @@ -45,25 +45,26 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); return response; } + describe('logs error rate timeseries', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getLogsErrorRateTimeseries(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + }); - registry.when( - 'Logs error rate timeseries when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('Logs error rate api', () => { - it('handles the empty state', async () => { - const response = await getLogsErrorRateTimeseries(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); + describe('when data loaded', () => { + let logSynthtrace: LogsSynthtraceEsClient; + + before(async () => { + logSynthtrace = await synthtrace.createLogsSynthtraceEsClient(); + }); + + after(async () => { + await logSynthtrace.clean(); }); - } - ); - registry.when( - 'Logs error rate timeseries when data loaded', - { config: 'basic', archives: [] }, - () => { describe('Logs without log level field', () => { before(async () => { return logSynthtrace.index([ @@ -170,6 +171,6 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); }); }); - } - ); + }); + }); } diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts new file mode 100644 index 0000000000000..fb10925b9906d --- /dev/null +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/entities/service_logs_rate_timeseries.spec.ts @@ -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 expect from '@kbn/expect'; +import { log, timerange } from '@kbn/apm-synthtrace-client'; +import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; +import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { first, last } from 'lodash'; +import { LogsSynthtraceEsClient } from '@kbn/apm-synthtrace'; +import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context'; + +export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) { + const apmApiClient = getService('apmApi'); + const synthtrace = getService('synthtrace'); + + const serviceName = 'synth-go'; + const start = new Date('2024-01-01T00:00:00.000Z').getTime(); + const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1; + + const hostName = 'synth-host'; + + async function getLogsRateTimeseries( + overrides?: RecursivePartial< + APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>['params'] + > + ) { + const response = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries', + params: { + path: { + serviceName: 'synth-go', + ...overrides?.path, + }, + query: { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + kuery: '', + ...overrides?.query, + }, + }, + }); + return response; + } + describe('logs rate timeseries', () => { + describe('when data is not loaded', () => { + it('handles empty state', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + }); + + describe('when data loaded', () => { + let logSynthtrace: LogsSynthtraceEsClient; + + before(async () => { + logSynthtrace = await synthtrace.createLogsSynthtraceEsClient(); + }); + + after(async () => { + await logSynthtrace.clean(); + }); + + describe('Logs without log level field', () => { + before(async () => { + return logSynthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log.create().message('This is a log message').timestamp(timestamp).defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + }) + ), + ]); + }); + after(async () => { + await logSynthtrace.clean(); + }); + + it('returns {} if log level is not available ', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + }); + }); + + describe('Logs with log.level=error', () => { + before(async () => { + return logSynthtrace.index([ + timerange(start, end) + .interval('1m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is a log message') + .logLevel('error') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': serviceName, + 'host.name': hostName, + 'service.environment': 'test', + }) + ), + timerange(start, end) + .interval('2m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is an error log message') + .logLevel('error') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': 'my-service', + 'host.name': hostName, + 'service.environment': 'production', + }) + ), + timerange(start, end) + .interval('5m') + .rate(1) + .generator((timestamp) => + log + .create() + .message('This is an info message') + .logLevel('info') + .timestamp(timestamp) + .defaults({ + 'log.file.path': '/my-service.log', + 'service.name': 'my-service', + 'host.name': hostName, + 'service.environment': 'production', + }) + ), + ]); + }); + after(async () => { + await logSynthtrace.clean(); + }); + + it('returns log rate timeseries', async () => { + const response = await getLogsRateTimeseries(); + expect(response.status).to.be(200); + expect( + response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667) + ).to.be(true); + }); + + it('handles environment filter', async () => { + const response = await getLogsRateTimeseries({ query: { environment: 'foo' } }); + expect(response.status).to.be(200); + expect(response.body.currentPeriod).to.empty(); + }); + + describe('when my-service is selected', () => { + it('returns some data', async () => { + const response = await getLogsRateTimeseries({ + path: { serviceName: 'my-service' }, + }); + + expect(response.status).to.be(200); + expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be( + 0.18181818181818182 + ); + expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091); + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts index e9defca84b6d3..b181d73c54cff 100644 --- a/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts +++ b/x-pack/test/api_integration/deployment_agnostic/apis/observability/apm/index.ts @@ -16,6 +16,7 @@ export default function apmApiIntegrationTests({ loadTestFile(require.resolve('./custom_dashboards')); loadTestFile(require.resolve('./dependencies')); loadTestFile(require.resolve('./data_view')); + loadTestFile(require.resolve('./entities')); loadTestFile(require.resolve('./cold_start')); }); } diff --git a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts b/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts deleted file mode 100644 index d4717b25bba93..0000000000000 --- a/x-pack/test/apm_api_integration/tests/entities/logs/service_logs_rate_timeseries.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import expect from '@kbn/expect'; -import { log, timerange } from '@kbn/apm-synthtrace-client'; -import { RecursivePartial } from '@kbn/apm-plugin/typings/common'; -import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; -import { first, last } from 'lodash'; -import { FtrProviderContext } from '../../../common/ftr_provider_context'; - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const apmApiClient = getService('apmApiClient'); - const logSynthtrace = getService('logSynthtraceEsClient'); - - const serviceName = 'synth-go'; - const start = new Date('2024-01-01T00:00:00.000Z').getTime(); - const end = new Date('2024-01-01T00:15:00.000Z').getTime() - 1; - - const hostName = 'synth-host'; - - async function getLogsRateTimeseries( - overrides?: RecursivePartial< - APIClientRequestParamsOf<'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries'>['params'] - > - ) { - const response = await apmApiClient.readUser({ - endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries', - params: { - path: { - serviceName: 'synth-go', - ...overrides?.path, - }, - query: { - start: new Date(start).toISOString(), - end: new Date(end).toISOString(), - environment: 'ENVIRONMENT_ALL', - kuery: '', - ...overrides?.query, - }, - }, - }); - return response; - } - - registry.when( - 'Logs rate timeseries when data is not loaded', - { config: 'basic', archives: [] }, - () => { - describe('Logs rate api', () => { - it('handles the empty state', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); - }); - } - ); - - registry.when('Logs rate timeseries when data loaded', { config: 'basic', archives: [] }, () => { - describe('Logs without log level field', () => { - before(async () => { - return logSynthtrace.index([ - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - log.create().message('This is a log message').timestamp(timestamp).defaults({ - 'log.file.path': '/my-service.log', - 'service.name': serviceName, - 'host.name': hostName, - }) - ), - ]); - }); - after(async () => { - await logSynthtrace.clean(); - }); - - it('returns {} if log level is not available ', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - }); - }); - - describe('Logs with log.level=error', () => { - before(async () => { - return logSynthtrace.index([ - timerange(start, end) - .interval('1m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is a log message') - .logLevel('error') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': serviceName, - 'host.name': hostName, - 'service.environment': 'test', - }) - ), - timerange(start, end) - .interval('2m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is an error log message') - .logLevel('error') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': 'my-service', - 'host.name': hostName, - 'service.environment': 'production', - }) - ), - timerange(start, end) - .interval('5m') - .rate(1) - .generator((timestamp) => - log - .create() - .message('This is an info message') - .logLevel('info') - .timestamp(timestamp) - .defaults({ - 'log.file.path': '/my-service.log', - 'service.name': 'my-service', - 'host.name': hostName, - 'service.environment': 'production', - }) - ), - ]); - }); - after(async () => { - await logSynthtrace.clean(); - }); - - it('returns log rate timeseries', async () => { - const response = await getLogsRateTimeseries(); - expect(response.status).to.be(200); - expect( - response.body.currentPeriod[serviceName].every(({ y }) => y === 0.06666666666666667) - ).to.be(true); - }); - - it('handles environment filter', async () => { - const response = await getLogsRateTimeseries({ query: { environment: 'foo' } }); - expect(response.status).to.be(200); - expect(response.body.currentPeriod).to.empty(); - }); - - describe('when my-service is selected', () => { - it('returns some data', async () => { - const response = await getLogsRateTimeseries({ - path: { serviceName: 'my-service' }, - }); - - expect(response.status).to.be(200); - expect(first(response.body.currentPeriod?.['my-service'])?.y).to.be(0.18181818181818182); - expect(last(response.body.currentPeriod?.['my-service'])?.y).to.be(0.09090909090909091); - }); - }); - }); - }); -} diff --git a/x-pack/test/functional/apps/dataset_quality/dataset_quality_privileges.ts b/x-pack/test/functional/apps/dataset_quality/dataset_quality_privileges.ts index e196f92c1cf18..48d7216c33d59 100644 --- a/x-pack/test/functional/apps/dataset_quality/dataset_quality_privileges.ts +++ b/x-pack/test/functional/apps/dataset_quality/dataset_quality_privileges.ts @@ -20,6 +20,8 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid const security = getService('security'); const synthtrace = getService('logSynthtraceEsClient'); const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const to = new Date(new Date().setDate(new Date().getDate() - 1)).toISOString(); const apacheAccessDatasetName = 'apache.access'; @@ -144,15 +146,16 @@ export default function ({ getService, getPageObjects }: DatasetQualityFtrProvid const datasetWithMonitorPrivilege = apacheAccessDatasetHumanName; const datasetWithoutMonitorPrivilege = 'synth.1'; - // "Size" should be available for `apacheAccessDatasetName` - await testSubjects.missingOrFail( - `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityInsufficientPrivileges}-sizeBytes-${datasetWithMonitorPrivilege}` - ); - - // "Size" should not be available for `datasetWithoutMonitorPrivilege` - await testSubjects.existOrFail( - `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityInsufficientPrivileges}-sizeBytes-${datasetWithoutMonitorPrivilege}` - ); + await retry.tryForTime(5000, async () => { + // "Size" should be available for `apacheAccessDatasetName` + await testSubjects.missingOrFail( + `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityInsufficientPrivileges}-sizeBytes-${datasetWithMonitorPrivilege}` + ); + // "Size" should not be available for `datasetWithoutMonitorPrivilege` + await testSubjects.existOrFail( + `${PageObjects.datasetQuality.testSubjectSelectors.datasetQualityInsufficientPrivileges}-sizeBytes-${datasetWithoutMonitorPrivilege}` + ); + }); }); it('Details page shows insufficient privileges warning for underprivileged data stream', async () => {