diff --git a/docs/settings/telemetry-settings.asciidoc b/docs/settings/telemetry-settings.asciidoc index 65f78a2eaf12d..d7c81985fb357 100644 --- a/docs/settings/telemetry-settings.asciidoc +++ b/docs/settings/telemetry-settings.asciidoc @@ -17,13 +17,6 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea [[telemetry-general-settings]] ==== General telemetry settings - -[[telemetry-enabled]] `telemetry.enabled`:: - Set to `true` to send cluster statistics to Elastic. Reporting your - cluster statistics helps us improve your user experience. Your data is never - shared with anyone. Set to `false` to disable statistics reporting from any - browser connected to the {kib} instance. Defaults to `true`. - `telemetry.sendUsageFrom`:: Set to `'server'` to report the cluster statistics from the {kib} server. If the server fails to connect to our endpoint at https://telemetry.elastic.co/, it assumes @@ -31,13 +24,13 @@ See our https://www.elastic.co/legal/privacy-statement[Privacy Statement] to lea when they are navigating through {kib}. Defaults to `'server'`. [[telemetry-optIn]] `telemetry.optIn`:: - Set to `true` to automatically opt into reporting cluster statistics. You can also opt out through - *Advanced Settings* in {kib}. Defaults to `true`. + Set to `true` to send cluster statistics to Elastic. Reporting your + cluster statistics helps us improve your user experience. Your data is never + shared with anyone. Set to `false` to stop sending any telemetry data to Elastic. + ++ +This setting can be changed at any time in <>. +To prevent users from changing it, +set <> to `false`. Defaults to `true`. `telemetry.allowChangingOptInStatus`:: - Set to `true` to allow overwriting the <> setting via the {kib} UI. Defaults to `true`. + -+ -[NOTE] -============ -When `false`, <> must be `true`. To disable telemetry and not allow users to change that parameter, use <>. -============ + Set to `true` to allow overwriting the <> setting via the <> in {kib}. Defaults to `true`. diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 145f58cf202f3..3ef6f4f722520 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -533,23 +533,19 @@ server status API and status page. *Default: `false`* [[telemetry-allowChangingOptInStatus]] `telemetry.allowChangingOptInStatus`:: When `true`, users are able to change the telemetry setting at a later time in -<>. When `false`, -{kib} looks at the value of <> to determine whether to send -telemetry data or not. <> and <> -cannot be `false` at the same time. *Default: `true`*. +<>. When `false`, users cannot change the opt-in status through *Advanced Settings*, and +{kib} only looks at the value of <> to determine whether to send telemetry data or not. *Default: `true`*. [[settings-telemetry-optIn]] `telemetry.optIn`:: -When `true`, telemetry data is sent to Elastic. -When `false`, collection of telemetry data is disabled. -To enable telemetry and prevent users from disabling it, -set <> to `false` and <> to `true`. -*Default: `true`* - -`telemetry.enabled`:: Reporting your cluster statistics helps -us improve your user experience. Your data is never shared with anyone. Set to -`false` to disable telemetry capabilities entirely. You can alternatively opt -out through *Advanced Settings*. *Default: `true`* +us improve your user experience. Your data is never shared with anyone. +Set to `true` to allow telemetry data to be sent to Elastic. +When `false`, the telemetry data is never sent to Elastic. + ++ +This setting can be changed at any time in <>. +To prevent users from changing it, +set <> to `false`. +*Default: `true`* `vis_type_vega.enableExternalUrls` {ess-icon}:: Set this value to true to allow Vega to use any URL to access external data diff --git a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts index f13597e933bb2..8ee5781bf852f 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts +++ b/src/plugins/console/public/application/models/legacy_core_editor/legacy_core_editor.ts @@ -414,4 +414,48 @@ export class LegacyCoreEditor implements CoreEditor { destroy() { this.editor.destroy(); } + + /** + * Formats body of the request in the editor by removing the extra whitespaces at the beginning of lines, + * And adds the correct indentation for each line + * @param reqRange request range to indent + */ + autoIndent(reqRange: Range) { + const session = this.editor.getSession(); + const mode = session.getMode(); + const startRow = reqRange.start.lineNumber; + const endRow = reqRange.end.lineNumber; + const tab = session.getTabString(); + + for (let row = startRow; row <= endRow; row++) { + let prevLineState = ''; + let prevLineIndent = ''; + if (row > 0) { + prevLineState = session.getState(row - 1); + const prevLine = session.getLine(row - 1); + prevLineIndent = mode.getNextLineIndent(prevLineState, prevLine, tab); + } + + const line = session.getLine(row); + // @ts-ignore + // Brace does not expose type definition for mode.$getIndent, though we have access to this method provided by the underlying Ace editor. + // See https://github.com/ajaxorg/ace/blob/87ce087ed1cf20eeabe56fb0894e048d9bc9c481/lib/ace/mode/text.js#L259 + const currLineIndent = mode.$getIndent(line); + if (prevLineIndent !== currLineIndent) { + if (currLineIndent.length > 0) { + // If current line has indentation, remove it. + // Next we will add the correct indentation by looking at the previous line + const range = new _AceRange(row, 0, row, currLineIndent.length); + session.remove(range); + } + if (prevLineIndent.length > 0) { + // If previous line has indentation, add indentation at the current line + session.insert({ row, column: 0 }, prevLineIndent); + } + } + + // Lastly outdent any closing braces + mode.autoOutdent(prevLineState, session, row); + } + } } diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js index ffbfa264a77b8..6c188eca0f0cc 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/input_highlight_rules.js @@ -39,7 +39,7 @@ export function InputHighlightRules() { start: mergeTokens( [ { token: 'warning', regex: '#!.*$' }, - { token: 'comment', regex: /^#.*$/ }, + { include: 'comments' }, { token: 'paren.lparen', regex: '{', next: 'json', push: true }, ], addEOL(['method'], /([a-zA-Z]+)/, 'start', 'method_sep'), @@ -88,9 +88,46 @@ export function InputHighlightRules() { addEOL(['url.param'], /([^&=]+)/, 'start-sql'), addEOL(['url.amp'], /(&)/, 'start-sql') ), + /** + * Each key in this.$rules considered to be a state in state machine. Regular expressions define the tokens for the current state, as well as the transitions into another state. + * See for more details https://cloud9-sdk.readme.io/docs/highlighting-rules#section-defining-states + * * + * Define a state for comments, these comment rules then can be included in other states. E.g. in 'start' and 'json' states by including { include: 'comments' } + * This will avoid duplicating the same rules in other states + */ + comments: [ + { + // Capture a line comment, indicated by # + token: ['comment.punctuation', 'comment.line'], + regex: /(#)(.*$)/, + }, + { + // Begin capturing a block comment, indicated by /* + token: 'comment.punctuation', + regex: /\/\*/, + push: [ + { + // Finish capturing a block comment, indicated by */ + token: 'comment.punctuation', + regex: /\*\//, + next: 'pop', + }, + { + defaultToken: 'comment.block', + }, + ], + }, + { + // Capture a line comment, indicated by // + token: ['comment.punctuation', 'comment.line'], + regex: /(\/\/)(.*$)/, + }, + ], }; addXJsonToRules(this); + // Add comment rules to json rule set + this.$rules.json.unshift({ include: 'comments' }); if (this.constructor === InputHighlightRules) { this.normalizeRules(); diff --git a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js index efd7dbd088581..a570f97ced0a3 100644 --- a/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js +++ b/src/plugins/console/public/application/models/legacy_core_editor/mode/worker/worker.js @@ -1901,8 +1901,8 @@ ace.define( reset(i + upTo.length); return text.substring(currentAt, i); }, - peek = function (c) { - return text.substr(at, c.length) === c; // nocommit - double check + peek = function (offset) { + return text.charAt(at + offset); }, number = function () { let number, @@ -1948,7 +1948,8 @@ ace.define( uffff; if (ch === '"') { - if (peek('""')) { + let c = '""'; + if (text.substring(at, c.length) === c) { // literal next('"'); next('"'); @@ -1984,8 +1985,31 @@ ace.define( error('Bad string'); }, white = function () { - while (ch && ch <= ' ') { - next(); + while (ch) { + // Skip whitespace. + while (ch && ch <= ' ') { + next(); + } + // if the current char in iteration is '#' or the char and the next char is equal to '//' + // we are on the single line comment + if (ch === '#' || ch === '/' && peek(0) === '/') { + // Until we are on the new line, skip to the next char + while (ch && ch !== '\n') { + next(); + } + } else if (ch === '/' && peek(0) === '*') { + // If the chars starts with '/*', we are on the multiline comment + next(); + next(); + while (ch && !(ch === '*' && peek(0) === '/')) { + // Until we have closing tags '*/', skip to the next char + next(); + } + if (ch) { + next(); + next(); + } + } else break; } }, strictWhite = function () { diff --git a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts index ac65afce2c18a..37c3799c05119 100644 --- a/src/plugins/console/public/application/models/sense_editor/sense_editor.ts +++ b/src/plugins/console/public/application/models/sense_editor/sense_editor.ts @@ -96,6 +96,15 @@ export class SenseEditor { return; } + if (parsedReq.data.some((doc) => utils.hasComments(doc))) { + /** + * Comments require different approach for indentation and do not have condensed format + * We need to delegate indentation logic to coreEditor since it has access to session and other methods used for formatting and indenting the comments + */ + this.coreEditor.autoIndent(parsedReq.range); + return; + } + if (parsedReq.data && parsedReq.data.length > 0) { let indent = parsedReq.data.length === 1; // unindent multi docs by default let formattedData = utils.formatRequestBodyDoc(parsedReq.data, indent); diff --git a/src/plugins/console/public/lib/autocomplete/autocomplete.ts b/src/plugins/console/public/lib/autocomplete/autocomplete.ts index 3e59c4e6bb023..21c1acf73bfd5 100644 --- a/src/plugins/console/public/lib/autocomplete/autocomplete.ts +++ b/src/plugins/console/public/lib/autocomplete/autocomplete.ts @@ -1104,6 +1104,9 @@ export default function ({ case 'paren.rparen': case 'punctuation.colon': case 'punctuation.comma': + case 'comment.line': + case 'comment.punctuation': + case 'comment.block': case 'UNKNOWN': return; } diff --git a/src/plugins/console/public/lib/row_parser.ts b/src/plugins/console/public/lib/row_parser.ts index e18bf6ac6446b..5faca5ffc4a7d 100644 --- a/src/plugins/console/public/lib/row_parser.ts +++ b/src/plugins/console/public/lib/row_parser.ts @@ -40,7 +40,7 @@ export default class RowParser { return MODE.IN_REQUEST; } let line = (this.editor.getLineValue(lineNumber) || '').trim(); - if (!line || line[0] === '#') { + if (!line || line.startsWith('#') || line.startsWith('//') || line.startsWith('/*')) { return MODE.BETWEEN_REQUESTS; } // empty line or a comment waiting for a new req to start diff --git a/src/plugins/console/public/lib/utils/index.ts b/src/plugins/console/public/lib/utils/index.ts index 3e6eb4a93f1f5..02d4023282cb2 100644 --- a/src/plugins/console/public/lib/utils/index.ts +++ b/src/plugins/console/public/lib/utils/index.ts @@ -48,6 +48,12 @@ export function formatRequestBodyDoc(data: string[], indent: boolean) { }; } +export function hasComments(data: string) { + // matches single line and multiline comments + const re = /(\/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+\/)|(\/\/.*)|(#.*)/; + return re.test(data); +} + export function extractWarningMessages(warnings: string) { // pattern for valid warning header const re = diff --git a/src/plugins/console/public/lib/utils/utils.test.js b/src/plugins/console/public/lib/utils/utils.test.js index d7fc690e1bc24..738aa5b9bf5c3 100644 --- a/src/plugins/console/public/lib/utils/utils.test.js +++ b/src/plugins/console/public/lib/utils/utils.test.js @@ -82,4 +82,95 @@ describe('Utils class', () => { 'e", f"', ]); }); + + describe('formatRequestBodyDoc', function () { + const tests = [ + { + source: ['{\n "test": {}\n}'], + indent: false, + assert: ['{"test":{}}'], + }, + { + source: ['{"test":{}}'], + indent: true, + assert: ['{\n "test": {}\n}'], + }, + { + source: ['{\n "test": """a\n b"""\n}'], + indent: false, + assert: ['{"test":"a\\n b"}'], + }, + { + source: ['{"test":"a\\n b"}'], + indent: true, + assert: ['{\n "test": """a\n b"""\n}'], + }, + ]; + + tests.forEach(({ source, indent, assert }, id) => { + test(`Test ${id}`, () => { + const formattedData = utils.formatRequestBodyDoc(source, indent); + expect(formattedData.data).toEqual(assert); + }); + }); + }); + + describe('hasComments', function () { + const runCommentTests = (tests) => { + tests.forEach(({ source, assert }) => { + test(`\n${source}`, () => { + const hasComments = utils.hasComments(source); + expect(hasComments).toEqual(assert); + }); + }); + }; + + describe('match single line comments', () => { + runCommentTests([ + { source: '{\n "test": {\n // "f": {}\n "a": "b"\n }\n}', assert: true }, + { + source: '{\n "test": {\n "a": "b",\n "f": {\n # "b": {}\n }\n }\n}', + assert: true, + }, + ]); + }); + + describe('match multiline comments', () => { + runCommentTests([ + { source: '{\n /* "test": {\n "a": "b"\n } */\n}', assert: true }, + { + source: + '{\n "test": {\n "a": "b",\n /* "f": {\n "b": {}\n } */\n "c": 1\n }\n}', + assert: true, + }, + ]); + }); + + describe('ignore non-comment tokens', () => { + runCommentTests([ + { source: '{"test":{"a":"b","f":{"b":{"c":{}}}}}', assert: false }, + { + source: '{\n "test": {\n "a": "b",\n "f": {\n "b": {}\n }\n }\n}', + assert: false, + }, + { source: '{\n "test": {\n "f": {}\n }\n}', assert: false }, + ]); + }); + + describe.skip('ignore comment tokens as values', () => { + runCommentTests([ + { source: '{\n "test": {\n "f": "//"\n }\n}', assert: false }, + { source: '{\n "test": {\n "f": "/* */"\n }\n}', assert: false }, + { source: '{\n "test": {\n "f": "#"\n }\n}', assert: false }, + ]); + }); + + describe.skip('ignore comment tokens as field names', () => { + runCommentTests([ + { source: '{\n "#": {\n "f": {}\n }\n}', assert: false }, + { source: '{\n "//": {\n "f": {}\n }\n}', assert: false }, + { source: '{\n "/* */": {\n "f": {}\n }\n}', assert: false }, + ]); + }); + }); }); diff --git a/src/plugins/console/public/types/core_editor.ts b/src/plugins/console/public/types/core_editor.ts index db8010afe762b..700b805aaecc0 100644 --- a/src/plugins/console/public/types/core_editor.ts +++ b/src/plugins/console/public/types/core_editor.ts @@ -272,4 +272,9 @@ export interface CoreEditor { * Release any resources in use by the editor. */ destroy(): void; + + /** + * Indent document within request range + */ + autoIndent(reqRange: Range): void; } diff --git a/src/plugins/kibana_usage_collection/kibana.json b/src/plugins/kibana_usage_collection/kibana.json index 0013b81604aa3..41fc5c6c37b78 100644 --- a/src/plugins/kibana_usage_collection/kibana.json +++ b/src/plugins/kibana_usage_collection/kibana.json @@ -10,7 +10,5 @@ "requiredPlugins": [ "usageCollection" ], - "optionalPlugins": [ - "telemetry" - ] + "optionalPlugins": [] } diff --git a/src/plugins/telemetry/public/components/index.ts b/src/plugins/kibana_usage_collection/public/plugin.test.mocks.ts similarity index 71% rename from src/plugins/telemetry/public/components/index.ts rename to src/plugins/kibana_usage_collection/public/plugin.test.mocks.ts index c3976c5ded878..107cfad58452c 100644 --- a/src/plugins/telemetry/public/components/index.ts +++ b/src/plugins/kibana_usage_collection/public/plugin.test.mocks.ts @@ -6,4 +6,8 @@ * Side Public License, v 1. */ -export { OptedInNoticeBanner } from './opted_in_notice_banner'; +export const registerEbtCountersMock = jest.fn(); + +jest.doMock('./ebt_counters', () => ({ + registerEbtCounters: registerEbtCountersMock, +})); diff --git a/src/plugins/kibana_usage_collection/public/plugin.test.ts b/src/plugins/kibana_usage_collection/public/plugin.test.ts index 7520be32610c5..0f1dc3795e18f 100644 --- a/src/plugins/kibana_usage_collection/public/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/public/plugin.test.ts @@ -8,32 +8,19 @@ import { coreMock } from '@kbn/core/public/mocks'; import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; -import { telemetryPluginMock } from '@kbn/telemetry-plugin/public/mocks'; +import { registerEbtCountersMock } from './plugin.test.mocks'; import { plugin } from '.'; describe('kibana_usage_collection/public', () => { const pluginInstance = plugin(); - describe('optIn fallback from telemetry', () => { - test('should call optIn(false) when telemetry is disabled', () => { + describe('EBT stats -> UI Counters', () => { + test('should report UI counters when EBT emits', () => { const coreSetup = coreMock.createSetup(); const usageCollectionMock = usageCollectionPluginMock.createSetupContract(); - expect(pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock })).toBe( - undefined - ); - expect(coreSetup.analytics.optIn).toHaveBeenCalledWith({ global: { enabled: false } }); - }); - - test('should NOT call optIn(false) when telemetry is enabled', () => { - const coreSetup = coreMock.createSetup(); - const usageCollectionMock = usageCollectionPluginMock.createSetupContract(); - const telemetry = telemetryPluginMock.createSetupContract(); - - expect( - pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock, telemetry }) - ).toBe(undefined); - expect(coreSetup.analytics.optIn).not.toHaveBeenCalled(); + pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock }); + expect(registerEbtCountersMock).toHaveBeenCalledTimes(1); }); }); }); diff --git a/src/plugins/kibana_usage_collection/public/plugin.ts b/src/plugins/kibana_usage_collection/public/plugin.ts index 8801e5f00d00f..2b7a4b868b76a 100644 --- a/src/plugins/kibana_usage_collection/public/plugin.ts +++ b/src/plugins/kibana_usage_collection/public/plugin.ts @@ -8,23 +8,14 @@ import type { UsageCollectionSetup } from '@kbn/usage-collection-plugin/public'; import type { CoreSetup, Plugin } from '@kbn/core/public'; -import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/public'; import { registerEbtCounters } from './ebt_counters'; interface KibanaUsageCollectionPluginsDepsSetup { usageCollection: UsageCollectionSetup; - telemetry?: TelemetryPluginSetup; } export class KibanaUsageCollectionPlugin implements Plugin { - public setup( - coreSetup: CoreSetup, - { usageCollection, telemetry }: KibanaUsageCollectionPluginsDepsSetup - ) { - if (!telemetry) { - // If the telemetry plugin is disabled, let's set optIn false to flush the queues. - coreSetup.analytics.optIn({ global: { enabled: false } }); - } + public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { registerEbtCounters(coreSetup.analytics, usageCollection); } diff --git a/src/plugins/kibana_usage_collection/server/plugin.test.ts b/src/plugins/kibana_usage_collection/server/plugin.test.ts index 8ac2558fe8850..ef26492c2d6fd 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.test.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.test.ts @@ -15,13 +15,11 @@ import { CollectorOptions, createUsageCollectionSetupMock, } from '@kbn/usage-collection-plugin/server/mocks'; -import { telemetryPluginMock } from '@kbn/telemetry-plugin/server/mocks'; import { cloudDetailsMock, registerEbtCountersMock } from './plugin.test.mocks'; import { plugin } from '.'; describe('kibana_usage_collection', () => { const pluginInstance = plugin(coreMock.createPluginInitializerContext({})); - const telemetry = telemetryPluginMock.createSetupContract(); const usageCollectors: CollectorOptions[] = []; @@ -143,26 +141,4 @@ describe('kibana_usage_collection', () => { test('Runs the stop method without issues', () => { expect(pluginInstance.stop()).toBe(undefined); }); - - describe('optIn fallback from telemetry', () => { - test('should call optIn(false) when telemetry is disabled', () => { - const coreSetup = coreMock.createSetup(); - const usageCollectionMock = createUsageCollectionSetupMock(); - - expect(pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock })).toBe( - undefined - ); - expect(coreSetup.analytics.optIn).toHaveBeenCalledWith({ global: { enabled: false } }); - }); - - test('should NOT call optIn(false) when telemetry is enabled', () => { - const coreSetup = coreMock.createSetup(); - const usageCollectionMock = createUsageCollectionSetupMock(); - - expect( - pluginInstance.setup(coreSetup, { usageCollection: usageCollectionMock, telemetry }) - ).toBe(undefined); - expect(coreSetup.analytics.optIn).not.toHaveBeenCalled(); - }); - }); }); diff --git a/src/plugins/kibana_usage_collection/server/plugin.ts b/src/plugins/kibana_usage_collection/server/plugin.ts index 9313dbfa09a2f..a464d50da340f 100644 --- a/src/plugins/kibana_usage_collection/server/plugin.ts +++ b/src/plugins/kibana_usage_collection/server/plugin.ts @@ -7,7 +7,6 @@ */ import type { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server'; -import type { TelemetryPluginSetup } from '@kbn/telemetry-plugin/server'; import { ReplaySubject, Subject, type Subscription } from 'rxjs'; import type { PluginInitializerContext, @@ -48,7 +47,6 @@ import { interface KibanaUsageCollectionPluginsDepsSetup { usageCollection: UsageCollectionSetup; - telemetry?: TelemetryPluginSetup; } type SavedObjectsRegisterType = SavedObjectsServiceSetup['registerType']; @@ -72,14 +70,7 @@ export class KibanaUsageCollectionPlugin implements Plugin { this.subscriptions = []; } - public setup( - coreSetup: CoreSetup, - { usageCollection, telemetry }: KibanaUsageCollectionPluginsDepsSetup - ) { - if (!telemetry) { - // If the telemetry plugin is disabled, let's set optIn false to flush the queues. - coreSetup.analytics.optIn({ global: { enabled: false } }); - } + public setup(coreSetup: CoreSetup, { usageCollection }: KibanaUsageCollectionPluginsDepsSetup) { registerEbtCounters(coreSetup.analytics, usageCollection); usageCollection.createUsageCounter('uiCounters'); this.eventLoopUsageCounter = usageCollection.createUsageCounter('eventLoop'); diff --git a/src/plugins/telemetry/common/constants.ts b/src/plugins/telemetry/common/constants.ts index 78887c27fbcfb..4987dc0b512ab 100644 --- a/src/plugins/telemetry/common/constants.ts +++ b/src/plugins/telemetry/common/constants.ts @@ -8,7 +8,7 @@ /** * The amount of time, in milliseconds, to wait between reports when enabled. - * Currently 24 hours. + * Currently, 24 hours. */ export const REPORT_INTERVAL_MS = 86400000; diff --git a/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.ts b/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.ts index a06c6932fcfe8..cb2292af8f5c8 100644 --- a/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.ts +++ b/src/plugins/telemetry/common/telemetry_config/get_telemetry_opt_in.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import SemVer from 'semver/classes/semver'; +import type SemVer from 'semver/classes/semver'; import semverParse from 'semver/functions/parse'; import { TelemetrySavedObject } from './types'; diff --git a/src/plugins/telemetry/public/components/welcome_telemetry_notice.test.tsx b/src/plugins/telemetry/public/components/welcome_telemetry_notice.test.tsx new file mode 100644 index 0000000000000..dfefaca5d570f --- /dev/null +++ b/src/plugins/telemetry/public/components/welcome_telemetry_notice.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 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 { mountWithIntl } from '@kbn/test-jest-helpers'; +import React from 'react'; +import { mockTelemetryConstants, mockTelemetryService } from '../mocks'; +import { WelcomeTelemetryNotice } from './welcome_telemetry_notice'; + +describe('WelcomeTelemetryNotice', () => { + const telemetryConstants = mockTelemetryConstants(); + + test('it should show the opt-out message', () => { + const telemetryService = mockTelemetryService(); + const component = mountWithIntl( + url} + /> + ); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(true); + }); + + test('it should show the opt-in message', () => { + const telemetryService = mockTelemetryService({ config: { optIn: false } }); + const component = mountWithIntl( + url} + /> + ); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(true); + }); + + test('it should not show opt-in/out options if user cannot change the settings', () => { + const telemetryService = mockTelemetryService({ config: { allowChangingOptInStatus: false } }); + const component = mountWithIntl( + url} + /> + ); + expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(false); + expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(false); + }); +}); diff --git a/src/plugins/telemetry/public/components/welcome_telemetry_notice.tsx b/src/plugins/telemetry/public/components/welcome_telemetry_notice.tsx new file mode 100644 index 0000000000000..3923016637cfb --- /dev/null +++ b/src/plugins/telemetry/public/components/welcome_telemetry_notice.tsx @@ -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 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 { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; +import type { TelemetryConstants } from '../plugin'; +import type { TelemetryService } from '../services'; + +interface Props { + telemetryService: TelemetryService; + addBasePath: (url: string) => string; + telemetryConstants: TelemetryConstants; +} + +export const WelcomeTelemetryNotice: React.FC = ({ + telemetryService, + addBasePath, + telemetryConstants, +}: Props) => { + return ( + <> + + + + + + {renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)} + + + + ); +}; + +function renderTelemetryEnabledOrDisabledText( + telemetryService: TelemetryService, + addBasePath: (url: string) => string +) { + if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) { + return null; + } + + const isOptedIn = telemetryService.getIsOptedIn(); + + if (isOptedIn) { + return ( + <> + + + + + + ); + } else { + return ( + <> + + + + + + ); + } +} diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts index f562cbad270d9..7c74d8f7d8813 100644 --- a/src/plugins/telemetry/public/mocks.ts +++ b/src/plugins/telemetry/public/mocks.ts @@ -33,7 +33,6 @@ export function mockTelemetryService({ config: configOverride = {}, }: TelemetryServiceMockOptions = {}) { const config = { - enabled: true, sendUsageTo: 'staging' as const, sendUsageFrom: 'browser' as const, optIn: true, diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index dea9cd1df2c3c..d6d0288cbb0bf 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -6,8 +6,6 @@ * Side Public License, v 1. */ -import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; - import type { Plugin, CoreStart, @@ -19,20 +17,15 @@ import type { ApplicationStart, DocLinksStart, } from '@kbn/core/public'; - import type { ScreenshotModePluginSetup } from '@kbn/screenshot-mode-plugin/public'; - import type { HomePublicPluginSetup } from '@kbn/home-plugin/public'; +import { ElasticV3BrowserShipper } from '@kbn/analytics-shippers-elastic-v3-browser'; + import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; import type { TelemetrySavedObjectAttributes, TelemetrySavedObject, } from '../common/telemetry_config/types'; -import { - getTelemetryAllowChangingOptInStatus, - getTelemetryOptIn, - getTelemetrySendUsageFrom, -} from '../common/telemetry_config'; import { getNotifyUserAboutOptInDefault } from '../common/telemetry_config/get_telemetry_notify_user_about_optin_default'; import { renderWelcomeTelemetryNotice } from './render_welcome_telemetry_notice'; @@ -95,8 +88,6 @@ interface TelemetryPluginSetupDependencies { * Public-exposed configuration */ export interface TelemetryPluginConfig { - /** Is the plugin enabled? **/ - enabled: boolean; /** The banner is expected to be shown when needed **/ banner: boolean; /** Does the cluster allow changing the opt-in/out status via the UI? **/ @@ -321,6 +312,9 @@ export class TelemetryPlugin implements Plugin { const telemetryConstants = mockTelemetryConstants(); - test('it should show the opt-out message', () => { + test('it should render the WelcomeTelemetryNotice component', () => { + const reactLazySpy = jest.spyOn(React, 'lazy'); const telemetryService = mockTelemetryService(); - const component = mountWithIntl( + shallowWithIntl( renderWelcomeTelemetryNotice(telemetryService, (url) => url, telemetryConstants) ); - expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(true); - }); - - test('it should show the opt-in message', () => { - const telemetryService = mockTelemetryService({ config: { optIn: false } }); - const component = mountWithIntl( - renderWelcomeTelemetryNotice(telemetryService, (url) => url, telemetryConstants) - ); - expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(true); - }); - - test('it should not show opt-in/out options if user cannot change the settings', () => { - const telemetryService = mockTelemetryService({ config: { allowChangingOptInStatus: false } }); - const component = mountWithIntl( - renderWelcomeTelemetryNotice(telemetryService, (url) => url, telemetryConstants) - ); - expect(component.exists('[id="telemetry.dataManagementDisableCollectionLink"]')).toBe(false); - expect(component.exists('[id="telemetry.dataManagementEnableCollectionLink"]')).toBe(false); + expect(reactLazySpy).toHaveBeenCalledTimes(1); }); }); diff --git a/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx index 7d460ded2571e..b065b5eb64b0a 100644 --- a/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx +++ b/src/plugins/telemetry/public/render_welcome_telemetry_notice.tsx @@ -7,8 +7,7 @@ */ import React from 'react'; -import { EuiLink, EuiSpacer, EuiTextColor } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n-react'; +import { withSuspense } from '@kbn/shared-ux-utility'; import type { TelemetryService } from './services'; import { TelemetryConstants } from './plugin'; @@ -17,65 +16,19 @@ export function renderWelcomeTelemetryNotice( addBasePath: (url: string) => string, telemetryConstants: TelemetryConstants ) { - return ( - <> - - - - - - {renderTelemetryEnabledOrDisabledText(telemetryService, addBasePath)} - - - + const WelcomeTelemetryNoticeLazy = withSuspense( + React.lazy(() => + import('./components/welcome_telemetry_notice').then(({ WelcomeTelemetryNotice }) => ({ + default: WelcomeTelemetryNotice, + })) + ) ); -} - -function renderTelemetryEnabledOrDisabledText( - telemetryService: TelemetryService, - addBasePath: (url: string) => string -) { - if (!telemetryService.userCanChangeSettings || !telemetryService.getCanChangeOptInStatus()) { - return null; - } - - const isOptedIn = telemetryService.getIsOptedIn(); - if (isOptedIn) { - return ( - <> - - - - - - ); - } else { - return ( - <> - - - - - - ); - } + return ( + + ); } diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx index 23754471e1630..e2aca2fafd2f9 100644 --- a/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opt_in_banner.tsx @@ -7,10 +7,10 @@ */ import React from 'react'; -import { OverlayStart } from '@kbn/core/public'; +import type { OverlayStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { OptInBanner } from '../../components/opt_in_banner'; -import { TelemetryConstants } from '../..'; +import { withSuspense } from '@kbn/shared-ux-utility'; +import type { TelemetryConstants } from '../..'; interface RenderBannerConfig { overlays: OverlayStart; @@ -19,8 +19,14 @@ interface RenderBannerConfig { } export function renderOptInBanner({ setOptIn, overlays, telemetryConstants }: RenderBannerConfig) { + const OptInBannerLazy = withSuspense( + React.lazy(() => + import('../../components/opt_in_banner').then(({ OptInBanner }) => ({ default: OptInBanner })) + ) + ); + const mount = toMountPoint( - + ); const bannerId = overlays.banners.add(mount, 10000); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx index 30720e43fcadf..4ef17b57b2dc0 100644 --- a/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx +++ b/src/plugins/telemetry/public/services/telemetry_notifications/render_opted_in_notice_banner.tsx @@ -7,10 +7,10 @@ */ import React from 'react'; -import { HttpStart, OverlayStart } from '@kbn/core/public'; +import type { HttpStart, OverlayStart } from '@kbn/core/public'; import { toMountPoint } from '@kbn/kibana-react-plugin/public'; -import { OptedInNoticeBanner } from '../../components/opted_in_notice_banner'; -import { TelemetryConstants } from '../..'; +import { withSuspense } from '@kbn/shared-ux-utility'; +import type { TelemetryConstants } from '../..'; interface RenderBannerConfig { http: HttpStart; @@ -24,8 +24,15 @@ export function renderOptedInNoticeBanner({ http, telemetryConstants, }: RenderBannerConfig) { + const OptedInNoticeBannerLazy = withSuspense( + React.lazy(() => + import('../../components/opted_in_notice_banner').then(({ OptedInNoticeBanner }) => ({ + default: OptedInNoticeBanner, + })) + ) + ); const mount = toMountPoint( - ; export const config: PluginConfigDescriptor = { schema: configSchema, exposeToBrowser: { - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: true, @@ -53,4 +46,17 @@ export const config: PluginConfigDescriptor = { sendUsageTo: true, hidePrivacyStatement: true, }, + deprecations: () => [ + (cfg) => { + if (cfg.telemetry?.enabled === false) { + return { + set: [ + { path: 'telemetry.optIn', value: false }, + { path: 'telemetry.allowChangingOptInStatus', value: false }, + ], + unset: [{ path: 'telemetry.enabled' }], + }; + } + }, + ], }; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index af96227c55284..9cd03e02f36dc 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -264,7 +264,6 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "defaultConfig": Object { "allowChangingOptInStatus": false, "banner": true, - "enabled": true, "optIn": true, "sendUsageFrom": "browser", "sendUsageTo": "staging", diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index 3de78d474570a..f747e9c6194db 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -26,7 +26,6 @@ describe('TelemetryManagementSectionComponent', () => { const telemetryService = new TelemetryService({ config: { sendUsageTo: 'staging', - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: true, @@ -57,7 +56,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: false, @@ -109,7 +107,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: false, @@ -155,7 +152,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: false, optIn: true, @@ -192,7 +188,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: false, @@ -233,7 +228,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: true, optIn: false, @@ -281,7 +275,6 @@ describe('TelemetryManagementSectionComponent', () => { const onQueryMatchChange = jest.fn(); const telemetryService = new TelemetryService({ config: { - enabled: true, banner: true, allowChangingOptInStatus: false, optIn: false, diff --git a/test/functional/apps/console/_comments.ts b/test/functional/apps/console/_comments.ts new file mode 100644 index 0000000000000..fe0fd882c607f --- /dev/null +++ b/test/functional/apps/console/_comments.ts @@ -0,0 +1,155 @@ +/* + * 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 expect from '@kbn/expect'; +import { asyncForEach } from '@kbn/std'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const log = getService('log'); + const retry = getService('retry'); + const PageObjects = getPageObjects(['common', 'console', 'header']); + + describe('console app', function testComments() { + this.tags('includeFirefox'); + before(async () => { + log.debug('navigateTo console'); + await PageObjects.common.navigateToApp('console'); + // blocks the close help button for several seconds so just retry until we can click it. + await retry.try(async () => { + await PageObjects.console.collapseHelp(); + }); + }); + + describe('with comments', async () => { + const enterRequest = async (url: string, body: string) => { + await PageObjects.console.clearTextArea(); + await PageObjects.console.enterRequest(url); + await PageObjects.console.pressEnter(); + await PageObjects.console.enterText(body); + }; + + async function runTests( + tests: Array<{ description: string; url?: string; body: string }>, + fn: () => Promise + ) { + await asyncForEach(tests, async ({ description, url, body }) => { + it(description, async () => { + await enterRequest(url ?? '\nGET search', body); + await fn(); + }); + }); + } + + describe('with single line comments', async () => { + await runTests( + [ + { + url: '\n// GET _search', + body: '', + description: 'should allow in request url, using //', + }, + { + body: '{\n\t\t"query": {\n\t\t\t// "match_all": {}', + description: 'should allow in request body, using //', + }, + { + url: '\n # GET _search', + body: '', + description: 'should allow in request url, using #', + }, + { + body: '{\n\t\t"query": {\n\t\t\t# "match_all": {}', + description: 'should allow in request body, using #', + }, + { + description: 'should accept as field names, using //', + body: '{\n "//": {}', + }, + { + description: 'should accept as field values, using //', + body: '{\n "f": "//"', + }, + { + description: 'should accept as field names, using #', + body: '{\n "#": {}', + }, + { + description: 'should accept as field values, using #', + body: '{\n "f": "#"', + }, + ], + async () => { + expect(await PageObjects.console.hasInvalidSyntax()).to.be(false); + expect(await PageObjects.console.hasErrorMarker()).to.be(false); + } + ); + }); + + describe('with multiline comments', async () => { + await runTests( + [ + { + url: '\n /* \nGET _search \n*/', + body: '', + description: 'should allow in request url, using /* */', + }, + { + body: '{\n\t\t"query": {\n\t\t\t/* "match_all": {} */', + description: 'should allow in request body, using /* */', + }, + { + description: 'should accept as field names, using /*', + body: '{\n "/*": {} \n\t\t /* "f": 1 */', + }, + { + description: 'should accept as field values, using */', + body: '{\n /* "f": 1 */ \n"f": "*/"', + }, + ], + async () => { + expect(await PageObjects.console.hasInvalidSyntax()).to.be(false); + expect(await PageObjects.console.hasErrorMarker()).to.be(false); + } + ); + }); + + describe('with invalid syntax in request body', async () => { + await runTests( + [ + { + description: 'should highlight invalid syntax', + body: '{\n "query": \'\'', // E.g. using single quotes + }, + ], + async () => { + expect(await PageObjects.console.hasInvalidSyntax()).to.be(true); + } + ); + }); + + describe('with invalid request', async () => { + await runTests( + [ + { + description: 'with invalid character should display error marker', + body: '{\n $ "query": {}', + }, + { + description: 'with missing field name', + body: '{\n "query": {},\n {}', + }, + ], + async () => { + expect(await PageObjects.console.hasErrorMarker()).to.be(true); + } + ); + }); + }); + }); +} diff --git a/test/functional/apps/console/index.js b/test/functional/apps/console/index.js index 4f0c362268b6f..f827a043176a8 100644 --- a/test/functional/apps/console/index.js +++ b/test/functional/apps/console/index.js @@ -20,6 +20,7 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_console')); loadTestFile(require.resolve('./_autocomplete')); loadTestFile(require.resolve('./_vector_tile')); + loadTestFile(require.resolve('./_comments')); } }); } diff --git a/test/functional/page_objects/console_page.ts b/test/functional/page_objects/console_page.ts index e8467ce714ff8..8f8308b4eef5a 100644 --- a/test/functional/page_objects/console_page.ts +++ b/test/functional/page_objects/console_page.ts @@ -14,6 +14,7 @@ export class ConsolePageObject extends FtrService { private readonly testSubjects = this.ctx.getService('testSubjects'); private readonly retry = this.ctx.getService('retry'); private readonly find = this.ctx.getService('find'); + log = this.ctx.getService('log'); public async getVisibleTextFromAceEditor(editor: WebElementWrapper) { const lines = await editor.findAllByClassName('ace_line_group'); @@ -187,4 +188,22 @@ export class ConsolePageObject extends FtrService { return false; } } + + public async hasInvalidSyntax() { + try { + const requestEditor = await this.getRequestEditor(); + return Boolean(await requestEditor.findByCssSelector('.ace_invalid')); + } catch (e) { + return false; + } + } + + public async hasErrorMarker() { + try { + const requestEditor = await this.getRequestEditor(); + return Boolean(await requestEditor.findByCssSelector('.ace_error')); + } catch (e) { + return false; + } + } } diff --git a/test/plugin_functional/test_suites/core_plugins/rendering.ts b/test/plugin_functional/test_suites/core_plugins/rendering.ts index 1044918ec1725..1e71c304165c5 100644 --- a/test/plugin_functional/test_suites/core_plugins/rendering.ts +++ b/test/plugin_functional/test_suites/core_plugins/rendering.ts @@ -143,9 +143,8 @@ export default function ({ getService }: PluginFunctionalProviderContext) { 'newsfeed.service.urlRoot (string)', 'telemetry.allowChangingOptInStatus (boolean)', 'telemetry.banner (boolean)', - 'telemetry.enabled (boolean)', 'telemetry.hidePrivacyStatement (boolean)', - 'telemetry.optIn (any)', + 'telemetry.optIn (boolean)', 'telemetry.sendUsageFrom (alternatives)', 'telemetry.sendUsageTo (any)', 'usageCollection.uiCounters.debug (boolean)', diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 8b0d8a85c21df..cf73b5a3f4eee 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -121,6 +121,7 @@ export enum SecurityPageName { kubernetes = 'kubernetes', exploreLanding = 'explore', dashboardsLanding = 'dashboards', + noPage = '', } export const EXPLORE_PATH = '/explore' as const; diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts index a5f814447ab59..230087c2310e7 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts @@ -184,7 +184,7 @@ describe('url state', () => { cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))` ); }); @@ -197,12 +197,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))` ); cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana'); @@ -213,21 +213,22 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" + "/app/security/hosts/siem-kibana/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" ); + cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2023-01-01T21:33:29.186Z')))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))` ); }); diff --git a/x-pack/plugins/security_solution/public/app/home/index.tsx b/x-pack/plugins/security_solution/public/app/home/index.tsx index 602e2709f2a2a..2597f1ab41ff2 100644 --- a/x-pack/plugins/security_solution/public/app/home/index.tsx +++ b/x-pack/plugins/security_solution/public/app/home/index.tsx @@ -23,7 +23,7 @@ import { useUpgradeSecurityPackages } from '../../common/hooks/use_upgrade_secur import { GlobalHeader } from './global_header'; import { SecuritySolutionTemplateWrapper } from './template_wrapper'; import { ConsoleManager } from '../../management/components/console/components/console_manager'; - +import { useSyncGlobalQueryString } from '../../common/utils/global_query_string'; interface HomePageProps { children: React.ReactNode; onAppLeave: (handler: AppLeaveHandler) => void; @@ -36,7 +36,7 @@ const HomePageComponent: React.FC = ({ setHeaderActionMenu, }) => { const { pathname } = useLocation(); - + useSyncGlobalQueryString(); useInitSourcerer(getScopeFromPath(pathname)); const { browserFields, indexPattern } = useSourcererDataView(getScopeFromPath(pathname)); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts index e0c4467bf5ae0..f35f8be0006c9 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.test.ts @@ -16,7 +16,7 @@ import { AdministrationSubTab } from '../../../../management/types'; import { renderHook } from '@testing-library/react-hooks'; import { TestProviders } from '../../../mock'; import { GetSecuritySolutionUrl } from '../../link_to'; -import { APP_UI_ID } from '../../../../../common/constants'; +import { APP_UI_ID, SecurityPageName } from '../../../../../common/constants'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { useIsGroupedNavigationEnabled } from '../helpers'; import { navTabs } from '../../../../app/home/home_navigations'; @@ -55,7 +55,7 @@ const mockDefaultTab = (pageName: string): SiemRouteType | undefined => { }; const getMockObject = ( - pageName: string, + pageName: SecurityPageName, pathName: string, detailName: string | undefined ): RouteSpyState & ObjectWithNavTabs => ({ @@ -100,7 +100,6 @@ const getMockObject = ( }, }, }, - sourcerer: {}, }, }; }); @@ -191,7 +190,7 @@ describe('Navigation Breadcrumbs', () => { describe('getBreadcrumbsForRoute', () => { test('should return Overview breadcrumbs when supplied overview pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('overview', '/', undefined), + getMockObject(SecurityPageName.overview, '/', undefined), getSecuritySolutionUrl, false ); @@ -206,7 +205,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Host breadcrumbs when supplied hosts pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', undefined), + getMockObject(SecurityPageName.hosts, '/', undefined), getSecuritySolutionUrl, false ); @@ -222,7 +221,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Network breadcrumbs when supplied network pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', undefined), + getMockObject(SecurityPageName.network, '/', undefined), getSecuritySolutionUrl, false ); @@ -238,7 +237,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Timelines breadcrumbs when supplied timelines pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('timelines', '/', undefined), + getMockObject(SecurityPageName.timelines, '/', undefined), getSecuritySolutionUrl, false ); @@ -253,7 +252,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', hostName), + getMockObject(SecurityPageName.hosts, '/', hostName), getSecuritySolutionUrl, false ); @@ -270,7 +269,7 @@ describe('Navigation Breadcrumbs', () => { test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv4), + getMockObject(SecurityPageName.network, '/', ipv4), getSecuritySolutionUrl, false ); @@ -287,7 +286,7 @@ describe('Navigation Breadcrumbs', () => { test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv6Encoded), + getMockObject(SecurityPageName.network, '/', ipv6Encoded), getSecuritySolutionUrl, false ); @@ -304,7 +303,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Alerts breadcrumbs when supplied alerts pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('alerts', '/alerts', undefined), + getMockObject(SecurityPageName.alerts, '/alerts', undefined), getSecuritySolutionUrl, false ); @@ -319,7 +318,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('exceptions', '/exceptions', undefined), + getMockObject(SecurityPageName.exceptions, '/exceptions', undefined), getSecuritySolutionUrl, false ); @@ -334,7 +333,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules', undefined), + getMockObject(SecurityPageName.rules, '/rules', undefined), getSecuritySolutionUrl, false ); @@ -349,7 +348,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules/create', undefined), + getMockObject(SecurityPageName.rules, '/rules/create', undefined), getSecuritySolutionUrl, false ); @@ -368,7 +367,7 @@ describe('Navigation Breadcrumbs', () => { const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}`, undefined), detailName: mockDetailName, state: { ruleName: mockRuleName, @@ -392,7 +391,7 @@ describe('Navigation Breadcrumbs', () => { const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}/edit`, undefined), detailName: mockDetailName, state: { ruleName: mockRuleName, @@ -417,7 +416,7 @@ describe('Navigation Breadcrumbs', () => { test('should return null breadcrumbs when supplied Cases pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('cases', '/', undefined), + getMockObject(SecurityPageName.case, '/', undefined), getSecuritySolutionUrl, false ); @@ -431,7 +430,7 @@ describe('Navigation Breadcrumbs', () => { }; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + ...getMockObject(SecurityPageName.case, `/${sampleCase.id}`, sampleCase.id), state: { caseTitle: sampleCase.name }, }, getSecuritySolutionUrl, @@ -442,7 +441,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Admin breadcrumbs when supplied endpoints pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('administration', '/endpoints', undefined), + getMockObject(SecurityPageName.administration, '/endpoints', undefined), getSecuritySolutionUrl, false ); @@ -461,22 +460,26 @@ describe('Navigation Breadcrumbs', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { const navigateToUrlMock = jest.fn(); const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); - result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + result.current( + getMockObject(SecurityPageName.hosts, '/', hostName), + chromeMock, + navigateToUrlMock + ); expect(setBreadcrumbsMock).toBeCalledWith([ expect.objectContaining({ text: 'Security', - href: "securitySolutionUI/get_started?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + href: "securitySolutionUI/get_started?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", onClick: expect.any(Function), }), expect.objectContaining({ text: 'Hosts', - href: "securitySolutionUI/hosts?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + href: "securitySolutionUI/hosts?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", onClick: expect.any(Function), }), expect.objectContaining({ text: 'siem-kibana', - href: "securitySolutionUI/hosts/siem-kibana?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + href: "securitySolutionUI/hosts/siem-kibana?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", onClick: expect.any(Function), }), { @@ -496,7 +499,7 @@ describe('Navigation Breadcrumbs', () => { describe('getBreadcrumbsForRoute', () => { test('should return Overview breadcrumbs when supplied overview pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('overview', '/', undefined), + getMockObject(SecurityPageName.overview, '/', undefined), getSecuritySolutionUrl, true ); @@ -515,7 +518,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Host breadcrumbs when supplied hosts pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', undefined), + getMockObject(SecurityPageName.hosts, '/', undefined), getSecuritySolutionUrl, true ); @@ -532,7 +535,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Network breadcrumbs when supplied network pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', undefined), + getMockObject(SecurityPageName.network, '/', undefined), getSecuritySolutionUrl, true ); @@ -549,7 +552,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Timelines breadcrumbs when supplied timelines pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('timelines', '/', undefined), + getMockObject(SecurityPageName.timelines, '/', undefined), getSecuritySolutionUrl, true ); @@ -564,7 +567,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Host Details breadcrumbs when supplied a pathname with hostName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('hosts', '/', hostName), + getMockObject(SecurityPageName.hosts, '/', hostName), getSecuritySolutionUrl, true ); @@ -582,7 +585,7 @@ describe('Navigation Breadcrumbs', () => { test('should return IP Details breadcrumbs when supplied pathname with ipv4', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv4), + getMockObject(SecurityPageName.network, '/', ipv4), getSecuritySolutionUrl, true ); @@ -600,7 +603,7 @@ describe('Navigation Breadcrumbs', () => { test('should return IP Details breadcrumbs when supplied pathname with ipv6', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('network', '/', ipv6Encoded), + getMockObject(SecurityPageName.network, '/', ipv6Encoded), getSecuritySolutionUrl, true ); @@ -618,7 +621,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Alerts breadcrumbs when supplied alerts pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('alerts', '/alerts', undefined), + getMockObject(SecurityPageName.alerts, '/alerts', undefined), getSecuritySolutionUrl, true ); @@ -633,7 +636,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Exceptions breadcrumbs when supplied exceptions pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('exceptions', '/exceptions', undefined), + getMockObject(SecurityPageName.exceptions, '/exceptions', undefined), getSecuritySolutionUrl, true ); @@ -649,7 +652,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules', undefined), + getMockObject(SecurityPageName.rules, '/rules', undefined), getSecuritySolutionUrl, true ); @@ -665,7 +668,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Rules breadcrumbs when supplied rules Creation pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('rules', '/rules/create', undefined), + getMockObject(SecurityPageName.rules, '/rules/create', undefined), getSecuritySolutionUrl, true ); @@ -685,7 +688,7 @@ describe('Navigation Breadcrumbs', () => { const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('rules', `/rules/id/${mockDetailName}`, undefined), + ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}`, undefined), detailName: mockDetailName, state: { ruleName: mockRuleName, @@ -710,7 +713,7 @@ describe('Navigation Breadcrumbs', () => { const mockRuleName = 'ALERT_RULE_NAME'; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('rules', `/rules/id/${mockDetailName}/edit`, undefined), + ...getMockObject(SecurityPageName.rules, `/rules/id/${mockDetailName}/edit`, undefined), detailName: mockDetailName, state: { ruleName: mockRuleName, @@ -736,7 +739,7 @@ describe('Navigation Breadcrumbs', () => { test('should return null breadcrumbs when supplied Cases pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('cases', '/', undefined), + getMockObject(SecurityPageName.case, '/', undefined), getSecuritySolutionUrl, true ); @@ -750,7 +753,7 @@ describe('Navigation Breadcrumbs', () => { }; const breadcrumbs = getBreadcrumbsForRoute( { - ...getMockObject('cases', `/${sampleCase.id}`, sampleCase.id), + ...getMockObject(SecurityPageName.case, `/${sampleCase.id}`, sampleCase.id), state: { caseTitle: sampleCase.name }, }, getSecuritySolutionUrl, @@ -761,7 +764,7 @@ describe('Navigation Breadcrumbs', () => { test('should return Admin breadcrumbs when supplied endpoints pageName', () => { const breadcrumbs = getBreadcrumbsForRoute( - getMockObject('administration', '/endpoints', undefined), + getMockObject(SecurityPageName.administration, '/endpoints', undefined), getSecuritySolutionUrl, true ); @@ -781,9 +784,13 @@ describe('Navigation Breadcrumbs', () => { test('should call chrome breadcrumb service with correct breadcrumbs', () => { const navigateToUrlMock = jest.fn(); const { result } = renderHook(() => useSetBreadcrumbs(), { wrapper: TestProviders }); - result.current(getMockObject('hosts', '/', hostName), chromeMock, navigateToUrlMock); + result.current( + getMockObject(SecurityPageName.hosts, '/', hostName), + chromeMock, + navigateToUrlMock + ); const searchString = - "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"; + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))"; expect(setBreadcrumbsMock).toBeCalledWith([ expect.objectContaining({ diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index 1449ff66b3317..7ca61bf9a8e83 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -84,7 +84,7 @@ export const getBreadcrumbsForRoute = ( } const newMenuLeadingBreadcrumbs = getLeadingBreadcrumbsForSecurityPage( - spyState.pageName as SecurityPageName, + spyState.pageName, getSecuritySolutionUrl, object.navTabs, isGroupedNavigationEnabled diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts index b2d91492b3ae1..16bf3a074d884 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/helpers.ts @@ -20,28 +20,31 @@ import { } from '../url_state/helpers'; import { SearchNavTab } from './types'; -import { SourcererUrlState } from '../../store/sourcerer/model'; import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { useUiSetting$ } from '../../lib/kibana'; import { ENABLE_GROUPED_NAVIGATION } from '../../../../common/constants'; -export const getSearch = (tab: SearchNavTab, urlState: UrlState): string => { +export const getSearch = ( + tab: SearchNavTab, + urlState: UrlState, + globalQueryString: string +): string => { if (tab && tab.urlKey != null && !isAdministration(tab.urlKey)) { - return getUrlStateSearch(urlState); + // TODO: Temporary code while we are migrating all query strings to global_query_string_manager + if (globalQueryString.length > 0) { + return `${getUrlStateSearch(urlState)}&${globalQueryString}`; + } else { + return getUrlStateSearch(urlState); + } } + return ''; }; export const getUrlStateSearch = (urlState: UrlState): string => ALL_URL_STATE_KEYS.reduce( (myLocation: Location, urlKey: KeyUrlState) => { - let urlStateToReplace: - | Filter[] - | Query - | SourcererUrlState - | TimelineUrl - | UrlInputsModel - | string = ''; + let urlStateToReplace: Filter[] | Query | TimelineUrl | UrlInputsModel | string = ''; if (urlKey === CONSTANTS.appQuery && urlState.query != null) { if (urlState.query.query === '') { @@ -57,8 +60,6 @@ export const getUrlStateSearch = (urlState: UrlState): string => } } else if (urlKey === CONSTANTS.timerange) { urlStateToReplace = urlState[CONSTANTS.timerange]; - } else if (urlKey === CONSTANTS.sourcerer) { - urlStateToReplace = urlState[CONSTANTS.sourcerer]; } else if (urlKey === CONSTANTS.timeline && urlState[CONSTANTS.timeline] != null) { const timeline = urlState[CONSTANTS.timeline]; if (timeline.id === '') { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx index f70b77b15dc8c..ef2c0a36437b5 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.test.tsx @@ -15,6 +15,7 @@ import { HostsTableType } from '../../../hosts/store/model'; import { RouteSpyState } from '../../utils/route/types'; import { TabNavigationComponentProps, SecuritySolutionTabNavigationProps } from './types'; import { TimelineTabs } from '../../../../common/types/timeline'; +import { SecurityPageName } from '../../../app/types'; jest.mock('react-router-dom', () => { const original = jest.requireActual('react-router-dom'); @@ -61,7 +62,7 @@ describe('SIEM Navigation', () => { const mockProps: TabNavigationComponentProps & SecuritySolutionTabNavigationProps & RouteSpyState = { - pageName: 'hosts', + pageName: SecurityPageName.hosts, pathName: '/', detailName: undefined, search: '', @@ -92,7 +93,6 @@ describe('SIEM Navigation', () => { }, [CONSTANTS.appQuery]: { query: '', language: 'kuery' }, [CONSTANTS.filters]: [], - [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { activeTab: TimelineTabs.query, id: '', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx index 8491171e65bca..ed296b70b31b0 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/index.tsx @@ -84,7 +84,6 @@ export const TabNavigationComponent: React.FC< navTabs={navTabs} pageName={pageName} pathName={pathName} - sourcerer={urlState.sourcerer} savedQuery={urlState.savedQuery} tabName={tabName} timeline={urlState.timeline} diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx index 8db77bae34a80..9ca8b1cf64bda 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/security_side_nav/security_side_nav.tsx @@ -130,7 +130,7 @@ const useSideNavItems = () => { const useSelectedId = (): SecurityPageName => { const [{ pageName }] = useRouteSpy(); const selectedId = useMemo(() => { - const [rootLinkInfo] = getAncestorLinksInfo(pageName as SecurityPageName); + const [rootLinkInfo] = getAncestorLinksInfo(pageName); return rootLinkInfo?.id ?? ''; }, [pageName]); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx index 594ceb1485adc..f5a05cb20925c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/index.test.tsx @@ -15,6 +15,7 @@ import { RouteSpyState } from '../../../utils/route/types'; import { CONSTANTS } from '../../url_state/constants'; import { TabNavigationComponent } from '.'; import { TabNavigationProps } from './types'; +import { SecurityPageName } from '../../../../app/types'; jest.mock('../../link_to'); jest.mock('../../../lib/kibana/kibana_react', () => { @@ -54,7 +55,7 @@ describe('Table Navigation', () => { const mockRiskyHostEnabled = true; const mockProps: TabNavigationProps & RouteSpyState = { - pageName: 'hosts', + pageName: SecurityPageName.hosts, pathName: '/hosts', detailName: hostName, search: '', @@ -89,7 +90,6 @@ describe('Table Navigation', () => { }, [CONSTANTS.appQuery]: { query: 'host.name:"siem-es"', language: 'kuery' }, [CONSTANTS.filters]: [], - [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { activeTab: TimelineTabs.query, id: '', diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts index 5630978bae87a..b1bf150f9e1c8 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/tab_navigation/types.ts @@ -8,7 +8,6 @@ import type { Filter, Query } from '@kbn/es-query'; import { UrlInputsModel } from '../../../store/inputs/model'; import { CONSTANTS } from '../../url_state/constants'; -import { SourcererUrlState } from '../../../store/sourcerer/model'; import { TimelineUrl } from '../../../../timelines/store/timeline/model'; import { SecuritySolutionTabNavigationProps } from '../types'; @@ -21,7 +20,6 @@ export interface TabNavigationProps extends SecuritySolutionTabNavigationProps { [CONSTANTS.appQuery]?: Query; [CONSTANTS.filters]?: Filter[]; [CONSTANTS.savedQuery]?: string; - [CONSTANTS.sourcerer]: SourcererUrlState; [CONSTANTS.timerange]: UrlInputsModel; [CONSTANTS.timeline]: TimelineUrl; } diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx index 9ec86ee2b24ef..4de646f860498 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_get_url_search.tsx @@ -8,6 +8,7 @@ import { useCallback, useMemo } from 'react'; import { useDeepEqualSelector } from '../../hooks/use_selector'; +import { useGlobalQueryString } from '../../utils/global_query_string'; import { makeMapStateToProps } from '../url_state/helpers'; import { getSearch, getUrlStateSearch } from './helpers'; import { SearchNavTab } from './types'; @@ -15,13 +16,27 @@ import { SearchNavTab } from './types'; export const useGetUrlSearch = (tab?: SearchNavTab) => { const mapState = makeMapStateToProps(); const { urlState } = useDeepEqualSelector(mapState); - const urlSearch = useMemo(() => (tab ? getSearch(tab, urlState) : ''), [tab, urlState]); + const globalQueryString = useGlobalQueryString(); + const urlSearch = useMemo( + () => (tab ? getSearch(tab, urlState, globalQueryString) : ''), + [tab, urlState, globalQueryString] + ); + return urlSearch; }; export const useGetUrlStateQueryString = () => { const mapState = makeMapStateToProps(); const { urlState } = useDeepEqualSelector(mapState); - const getUrlStateQueryString = useCallback(() => getUrlStateSearch(urlState), [urlState]); + const globalQueryString = useGlobalQueryString(); + const getUrlStateQueryString = useCallback(() => { + // TODO: Temporary code while we are migrating all query strings to global_query_string_manager + if (globalQueryString.length > 0) { + return `${getUrlStateSearch(urlState)}&${globalQueryString}`; + } + + return getUrlStateSearch(urlState); + }, [urlState, globalQueryString]); + return getUrlStateQueryString; }; diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap index 0be5c860e22a2..293fddddf1ba2 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/__snapshots__/index.test.tsx.snap @@ -8,10 +8,10 @@ Object { "id": "main", "items": Array [ Object { - "data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-get_started", "disabled": false, - "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/get_started?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "get_started", "isSelected": false, "name": "Get started", @@ -24,20 +24,20 @@ Object { "id": "dashboards", "items": Array [ Object { - "data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-overview", "disabled": false, - "href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/overview?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "overview", "isSelected": false, "name": "Overview", "onClick": [Function], }, Object { - "data-href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-detection_response", "disabled": false, - "href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/detection_response?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "detection_response", "isSelected": false, "name": "Detection & Response", @@ -50,30 +50,30 @@ Object { "id": "detect", "items": Array [ Object { - "data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-alerts", "disabled": false, - "href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/alerts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "alerts", "isSelected": false, "name": "Alerts", "onClick": [Function], }, Object { - "data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-rules", "disabled": false, - "href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/rules?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "rules", "isSelected": false, "name": "Rules", "onClick": [Function], }, Object { - "data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-exceptions", "disabled": false, - "href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/exceptions?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "exceptions", "isSelected": false, "name": "Exception lists", @@ -86,30 +86,30 @@ Object { "id": "explore", "items": Array [ Object { - "data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-hosts", "disabled": false, - "href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/hosts?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "hosts", "isSelected": true, "name": "Hosts", "onClick": [Function], }, Object { - "data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-network", "disabled": false, - "href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/network?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "network", "isSelected": false, "name": "Network", "onClick": [Function], }, Object { - "data-href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-users", "disabled": false, - "href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/users?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "users", "isSelected": false, "name": "Users", @@ -122,10 +122,10 @@ Object { "id": "investigate", "items": Array [ Object { - "data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-timelines", "disabled": false, - "href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/timelines?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "timelines", "isSelected": false, "name": "Timelines", diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index c2e733edf66d5..db7d06f899b5c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -32,7 +32,6 @@ describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, [CONSTANTS.savedQuery]: '', - [CONSTANTS.sourcerer]: {}, [CONSTANTS.timeline]: { activeTab: TimelineTabs.query, id: '', @@ -165,10 +164,10 @@ describe('useSecuritySolutionNavigation', () => { ); expect(caseNavItem).toMatchInlineSnapshot(` Object { - "data-href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "data-href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "data-test-subj": "navigation-cases", "disabled": false, - "href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "href": "securitySolutionUI/cases?query=(language:kuery,query:'host.name:%22security-solution-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", "id": "cases", "isSelected": false, "name": "Cases", diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx index e8ea703fe7171..f0a51e0b8a39a 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.tsx @@ -76,7 +76,6 @@ export const useSecuritySolutionNavigation = () => { filters: urlState.filters, navTabs: enabledNavTabs, pageName, - sourcerer: urlState.sourcerer, savedQuery: urlState.savedQuery, tabName, timeline: urlState.timeline, diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index be131f168464a..f9a766ed3d875 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -18,6 +18,7 @@ import { NavTab, SecurityNavGroupKey } from '../types'; import { SecurityPageName } from '../../../../../common/constants'; import { useCanSeeHostIsolationExceptionsMenu } from '../../../../management/pages/host_isolation_exceptions/view/hooks'; import { useIsExperimentalFeatureEnabled } from '../../../hooks/use_experimental_features'; +import { useGlobalQueryString } from '../../../utils/global_query_string'; export const usePrimaryNavigationItems = ({ navTabs, @@ -25,11 +26,13 @@ export const usePrimaryNavigationItems = ({ ...urlStateProps }: PrimaryNavigationItemsProps): Array> => { const { navigateTo, getAppUrl } = useNavigation(); + const globalQueryString = useGlobalQueryString(); + const getSideNav = useCallback( (tab: NavTab) => { const { id, name, disabled } = tab; const isSelected = selectedTabId === id; - const urlSearch = getSearch(tab, urlStateProps); + const urlSearch = getSearch(tab, urlStateProps, globalQueryString); const handleClick = (ev: React.MouseEvent) => { ev.preventDefault(); @@ -49,7 +52,7 @@ export const usePrimaryNavigationItems = ({ onClick: handleClick, }; }, - [getAppUrl, navigateTo, selectedTabId, urlStateProps] + [getAppUrl, navigateTo, selectedTabId, urlStateProps, globalQueryString] ); const navItemsToDisplay = usePrimaryNavigationItemsToDisplay(navTabs); diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx index 1123fd50a53e6..888d9f2d8ee6b 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_primary_navigation.tsx @@ -24,7 +24,6 @@ export const usePrimaryNavigation = ({ navTabs, pageName, savedQuery, - sourcerer, tabName, timeline, timerange, @@ -53,7 +52,6 @@ export const usePrimaryNavigation = ({ filters, query, savedQuery, - sourcerer, timeline, timerange, }); diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx index dbc57bb30c77f..0220963110ce6 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.test.tsx @@ -54,6 +54,16 @@ jest.mock('@kbn/kibana-react-plugin/public', () => { }; }); +const mockUpdateUrlParam = jest.fn(); +jest.mock('../../utils/global_query_string', () => { + const original = jest.requireActual('../../utils/global_query_string'); + + return { + ...original, + useUpdateUrlParam: () => mockUpdateUrlParam, + }; +}); + const mockOptions = DEFAULT_INDEX_PATTERN.map((index) => ({ label: index, value: index })); const defaultProps = { @@ -411,6 +421,50 @@ describe('Sourcerer component', () => { }) ); }); + + it('onSave updates the URL param', () => { + store = createStore( + { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaDataViews: [ + mockGlobalState.sourcerer.defaultDataView, + { + ...mockGlobalState.sourcerer.defaultDataView, + id: '1234', + title: 'filebeat-*', + patternList: ['filebeat-*'], + }, + ], + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.default]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.default], + selectedDataViewId: id, + selectedPatterns: patternListNoSignals.slice(0, 2), + }, + }, + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + + const wrapper = mount( + + + + ); + wrapper.find(`[data-test-subj="sourcerer-trigger"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="comboBoxInput"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="sourcerer-combo-option"]`).first().simulate('click'); + wrapper.find(`[data-test-subj="sourcerer-save"]`).first().simulate('click'); + + expect(mockUpdateUrlParam).toHaveBeenCalledTimes(1); + }); + it('resets to default index pattern', async () => { const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx index ad3b11a74e81d..0255bb6b58065 100644 --- a/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/sourcerer/index.tsx @@ -20,7 +20,7 @@ import { useDispatch } from 'react-redux'; import * as i18n from './translations'; import { sourcererActions, sourcererModel, sourcererSelectors } from '../../store/sourcerer'; import { useDeepEqualSelector } from '../../hooks/use_selector'; -import { SourcererScopeName } from '../../store/sourcerer/model'; +import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model'; import { usePickIndexPatterns } from './use_pick_index_patterns'; import { FormRow, PopoverContent, StyledButton, StyledFormRow } from './helpers'; import { TemporarySourcerer } from './temporary'; @@ -29,6 +29,8 @@ import { useUpdateDataView } from './use_update_data_view'; import { Trigger } from './trigger'; import { AlertsCheckbox, SaveButtons, SourcererCallout } from './sub_components'; import { useSignalHelpers } from '../../containers/sourcerer/use_signal_helpers'; +import { useUpdateUrlParam } from '../../utils/global_query_string'; +import { CONSTANTS } from '../url_state/constants'; export interface SourcererComponentProps { scope: sourcererModel.SourcererScopeName; @@ -38,6 +40,8 @@ export const Sourcerer = React.memo(({ scope: scopeId } const dispatch = useDispatch(); const isDetectionsSourcerer = scopeId === SourcererScopeName.detections; const isTimelineSourcerer = scopeId === SourcererScopeName.timeline; + const isDefaultSourcerer = scopeId === SourcererScopeName.default; + const updateUrlParam = useUpdateUrlParam(CONSTANTS.sourcerer); const sourcererScopeSelector = useMemo(() => sourcererSelectors.getSourcererScopeSelector(), []); const { @@ -144,8 +148,17 @@ export const Sourcerer = React.memo(({ scope: scopeId } shouldValidateSelectedPatterns, }) ); + + if (isDefaultSourcerer) { + updateUrlParam({ + [SourcererScopeName.default]: { + id: newSelectedDataView, + selectedPatterns: newSelectedPatterns, + }, + }); + } }, - [dispatch, scopeId] + [dispatch, scopeId, isDefaultSourcerer, updateUrlParam] ); const onChangeDataView = useCallback( 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 ba806da195461..f8df77e8ff624 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 @@ -8,6 +8,7 @@ import { navTabs } from '../../../app/home/home_navigations'; import { getTitle, isQueryStateEmpty } from './helpers'; import { CONSTANTS } from './constants'; +import { ValueUrlState } from './types'; describe('Helpers Url_State', () => { describe('getTitle', () => { @@ -45,7 +46,7 @@ describe('Helpers Url_State', () => { }); test('returns true if url key is "query" and queryState is empty string', () => { - const result = isQueryStateEmpty({}, CONSTANTS.appQuery); + const result = isQueryStateEmpty('', CONSTANTS.appQuery); expect(result).toBeTruthy(); }); @@ -72,7 +73,7 @@ describe('Helpers Url_State', () => { // TODO: Is this a bug, or intended? test('returns false if url key is "timeline" and queryState is empty', () => { - const result = isQueryStateEmpty({}, CONSTANTS.timeline); + const result = isQueryStateEmpty({} as ValueUrlState, 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 71b6852943ebf..681045be404e0 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 @@ -24,8 +24,6 @@ import { formatDate } from '../super_date_picker'; import { NavTab } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; import { ReplaceStateInLocation, KeyUrlState, ValueUrlState } from './types'; -import { sourcererSelectors } from '../../store/sourcerer'; -import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model'; export const isDetectionsPages = (pageName: string) => pageName === SecurityPageName.alerts || @@ -49,7 +47,10 @@ export const encodeRisonUrlState = (state: any) => encode(state); export const getQueryStringFromLocation = (search: string) => search.substring(1); -export const getParamFromQueryString = (queryString: string, key: string) => { +export const getParamFromQueryString = ( + queryString: string, + key: string +): string | undefined | null => { const parsedQueryString = parse(queryString, { sort: false }); const queryParam = parsedQueryString[key]; @@ -128,7 +129,6 @@ export const makeMapStateToProps = () => { const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); const getGlobalSavedQuerySelector = inputsSelectors.globalSavedQuerySelector(); const getTimeline = timelineSelectors.getTimelineByIdSelector(); - const getSourcererScopes = sourcererSelectors.scopesSelector(); const mapStateToProps = (state: State) => { const inputState = getInputsSelector(state); const { linkTo: globalLinkTo, timerange: globalTimerange } = inputState.global; @@ -159,25 +159,10 @@ export const makeMapStateToProps = () => { [CONSTANTS.savedQuery]: savedQuery.id, }; } - const sourcerer = getSourcererScopes(state); - const activeScopes: SourcererScopeName[] = Object.keys(sourcerer) as SourcererScopeName[]; - const selectedPatterns: SourcererUrlState = activeScopes - .filter((scope) => scope === SourcererScopeName.default) - .reduce( - (acc, scope) => ({ - ...acc, - [scope]: { - id: sourcerer[scope]?.selectedDataViewId, - selectedPatterns: sourcerer[scope]?.selectedPatterns, - }, - }), - {} - ); return { urlState: { ...searchAttr, - [CONSTANTS.sourcerer]: selectedPatterns, [CONSTANTS.timerange]: { global: { [CONSTANTS.timerange]: globalTimerange, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx index 02fefd46f031d..50072d96fe96f 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index.test.tsx @@ -125,6 +125,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -159,6 +160,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -189,6 +191,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -218,6 +221,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -227,7 +231,7 @@ describe('UrlStateContainer', () => { ).toEqual({ hash: '', pathname: examplePath, - search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, + search: `?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))`, state: '', }); } @@ -246,6 +250,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: 'out of sync path', + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -265,6 +270,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -284,6 +290,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -309,6 +316,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -334,6 +342,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -356,6 +365,7 @@ describe('UrlStateContainer', () => { (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx index 4e87be0fb5316..1162a449acdc6 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/index_mocked.test.tsx @@ -88,6 +88,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -126,7 +127,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2020-07-07T08:20:18.966Z',fromStr:now-24h,kind:relative,to:'2020-07-08T08:20:18.966Z',toStr:now)))", state: '', }); }); @@ -142,6 +143,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -160,7 +162,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); }); @@ -176,6 +178,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -195,42 +198,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: '/network', search: - "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", - state: '', - }); - }); - - test('sourcerer redux state updates the url', () => { - mockProps = getMockPropsObj({ - page: CONSTANTS.networkPage, - examplePath: '/network', - namespaceLower: 'network', - pageName: SecurityPageName.network, - detailName: undefined, - }).noSearch.undefinedQuery; - - (useLocation as jest.Mock).mockReturnValue({ - pathname: mockProps.pathName, - }); - - const wrapper = mount( - useUrlStateHooks(args)} /> - ); - const newUrlState = { - ...mockProps.urlState, - sourcerer: ['cool', 'patterns'], - }; - - wrapper.setProps({ - hookProps: { ...mockProps, urlState: newUrlState, isInitializing: false }, - }); - wrapper.update(); - - expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ - hash: '', - pathname: '/network', - search: - "?sourcerer=!(cool,patterns)&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))&timeline=(id:hello_timeline_id,isOpen:!t)", state: '', }); }); @@ -286,6 +254,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -297,6 +266,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: updatedMockProps.pathName, + search: mockProps.search, }); wrapper.setProps({ @@ -307,7 +277,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ hash: '', pathname: MANAGEMENT_PATH, - search: '?', + search: mockProps.search, state: '', }); }); @@ -363,6 +333,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -374,6 +345,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: updatedMockProps.pathName, + search: mockProps.search, }); wrapper.setProps({ @@ -384,7 +356,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => expect(mockHistory.replace.mock.calls[1][0]).toStrictEqual({ hash: '', pathname: DASHBOARDS_PATH, - search: '?', + search: mockProps.search, state: '', }); }); @@ -401,6 +373,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); mount( useUrlStateHooks(args)} />); @@ -409,7 +382,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => hash: '', pathname: examplePath, search: - "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))", state: '', }); } @@ -433,6 +406,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -440,11 +414,12 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => ); expect(mockHistory.replace.mock.calls[0][0].search).toEqual( - "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); (useLocation as jest.Mock).mockReturnValue({ pathname: updatedProps.pathName, + search: mockProps.search, }); wrapper.setProps({ hookProps: updatedProps }); @@ -452,7 +427,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => wrapper.update(); expect(mockHistory.replace.mock.calls[1][0].search).toEqual( - "?query=(language:kuery,query:'host.name:%22siem-es%22')&sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" + "?query=(language:kuery,query:'host.name:%22siem-es%22')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); }); @@ -478,6 +453,7 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => (useLocation as jest.Mock).mockReturnValue({ pathname: mockProps.pathName, + search: mockProps.search, }); const wrapper = mount( @@ -485,11 +461,12 @@ describe('UrlStateContainer - lodash.throttle mocked to test update url', () => ); expect(mockHistory.replace.mock.calls[0][0].search).toEqual( - "?sourcerer=()&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" + "?timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)),timeline:(linkTo:!(global),timerange:(from:'2019-05-16T23:10:43.696Z',fromStr:now-24h,kind:relative,to:'2019-05-17T23:10:43.697Z',toStr:now)))" ); (useLocation as jest.Mock).mockReturnValue({ pathname: updatedMockProps.pathName, + search: mockProps.search, }); wrapper.setProps({ hookProps: updatedMockProps }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx index 0f7e93f1befca..a417ad7c5950f 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/initialize_redux_by_url.tsx @@ -11,7 +11,7 @@ import { Dispatch } from 'redux'; import { useCallback, useMemo } from 'react'; import { useDispatch } from 'react-redux'; import type { Filter, Query } from '@kbn/es-query'; -import { inputsActions, sourcererActions } from '../../store/actions'; +import { inputsActions } from '../../store/actions'; import { InputsModelId, TimeRangeKinds } from '../../store/inputs/constants'; import { UrlInputsModel, @@ -21,14 +21,13 @@ import { } from '../../store/inputs/model'; import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { CONSTANTS } from './constants'; -import { decodeRisonUrlState, isDetectionsPages } from './helpers'; +import { decodeRisonUrlState } from './helpers'; import { normalizeTimeRange } from './normalize_time_range'; import { SetInitialStateFromUrl } from './types'; import { queryTimelineById, dispatchUpdateTimeline, } from '../../../timelines/components/open_timeline/helpers'; -import { SourcererScopeName, SourcererUrlState } from '../../store/sourcerer/model'; import { timelineActions } from '../../../timelines/store/timeline'; export const useSetInitialStateFromUrl = () => { @@ -54,23 +53,6 @@ export const useSetInitialStateFromUrl = () => { if (urlKey === CONSTANTS.timerange) { updateTimerange(newUrlStateString, dispatch); } - if (urlKey === CONSTANTS.sourcerer) { - const sourcererState = decodeRisonUrlState(newUrlStateString); - if (sourcererState != null) { - const activeScopes: SourcererScopeName[] = Object.keys(sourcererState).filter( - (key) => !(key === SourcererScopeName.default && isDetectionsPages(pageName)) - ) as SourcererScopeName[]; - activeScopes.forEach((scope) => - dispatch( - sourcererActions.setSelectedDataView({ - id: scope, - selectedDataViewId: sourcererState[scope]?.id ?? null, - selectedPatterns: sourcererState[scope]?.selectedPatterns ?? [], - }) - ) - ); - } - } if (urlKey === CONSTANTS.appQuery && indexPattern != null) { const appQuery = decodeRisonUrlState(newUrlStateString); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts index 03ec5b0dcf940..6b8c3e3fe252b 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/test_dependencies.ts @@ -120,7 +120,6 @@ export const defaultProps: UrlStateContainerPropTypes = { id: '', isOpen: false, }, - [CONSTANTS.sourcerer]: {}, }, history: { ...mockHistory, @@ -132,7 +131,7 @@ export const getMockProps = ( location = defaultLocation, kqlQueryKey = CONSTANTS.networkPage, kqlQueryValue: Query | null, - pageName: string, + pageName: SecurityPageName, detailName: string | undefined ): UrlStateContainerPropTypes => ({ ...defaultProps, @@ -154,7 +153,7 @@ interface GetMockPropsObj { examplePath: string; namespaceLower: string; page: LocationTypes; - pageName: string; + pageName: SecurityPageName; detailName: string | undefined; } @@ -270,7 +269,7 @@ export const getMockPropsObj = ({ page, examplePath, pageName, detailName }: Get // silly that this needs to be an array and not an object // https://jestjs.io/docs/en/api#testeachtable-name-fn-timeout export const testCases: Array< - [LocationTypes, string, string, string, string | null, string, undefined | string] + [LocationTypes, string, string, string, string | null, SecurityPageName, undefined | string] > = [ [ /* page */ CONSTANTS.networkPage, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts index 8633987b7c1c5..caf2ad77a7245 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/types.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/types.ts @@ -13,13 +13,11 @@ import { RouteSpyState } from '../../utils/route/types'; import { SecurityNav } from '../navigation/types'; import { CONSTANTS, UrlStateType } from './constants'; -import { SourcererUrlState } from '../../store/sourcerer/model'; export const ALL_URL_STATE_KEYS: KeyUrlState[] = [ CONSTANTS.appQuery, CONSTANTS.filters, CONSTANTS.savedQuery, - CONSTANTS.sourcerer, CONSTANTS.timerange, CONSTANTS.timeline, ]; @@ -43,7 +41,6 @@ export interface UrlState { [CONSTANTS.appQuery]?: Query; [CONSTANTS.filters]?: Filter[]; [CONSTANTS.savedQuery]?: string; - [CONSTANTS.sourcerer]: SourcererUrlState; [CONSTANTS.timerange]: UrlInputsModel; [CONSTANTS.timeline]: TimelineUrl; } diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx index e787b3a750e91..7a5706aec578f 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx +++ b/x-pack/plugins/security_solution/public/common/components/url_state/use_url_state.tsx @@ -41,7 +41,6 @@ import { TimelineUrl } from '../../../timelines/store/timeline/model'; import { UrlInputsModel } from '../../store/inputs/model'; import { queryTimelineByIdOnUrlChange } from './query_timeline_by_id_on_url_change'; import { getLinkInfo } from '../../links'; -import { SecurityPageName } from '../../../app/types'; import { useIsGroupedNavigationEnabled } from '../navigation/helpers'; function usePrevious(value: PreviousLocationUrlState) { @@ -57,17 +56,16 @@ export const useUrlStateHooks = ({ navTabs, pageName, urlState, - search, pathName, history, }: UrlStateContainerPropTypes) => { const [isFirstPageLoad, setIsFirstPageLoad] = useState(true); const { filterManager, savedQueries } = useKibana().services.data.query; - const { pathname: browserPathName } = useLocation(); + const { pathname: browserPathName, search } = useLocation(); const prevProps = usePrevious({ pathName, pageName, urlState, search }); const isGroupedNavEnabled = useIsGroupedNavigationEnabled(); - const linkInfo = pageName ? getLinkInfo(pageName as SecurityPageName) : undefined; + const linkInfo = pageName ? getLinkInfo(pageName) : undefined; const { setInitialStateFromUrl, updateTimeline, updateTimelineIsLoading } = useSetInitialStateFromUrl(); diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx index c577f45508e8a..737f4cf628765 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.test.tsx @@ -13,7 +13,11 @@ import { Provider } from 'react-redux'; import { getScopeFromPath, useInitSourcerer, useSourcererDataView } from '.'; import { mockPatterns } from './mocks'; import { RouteSpyState } from '../../utils/route/types'; -import { DEFAULT_INDEX_PATTERN, SecurityPageName } from '../../../../common/constants'; +import { + DEFAULT_DATA_VIEW_ID, + DEFAULT_INDEX_PATTERN, + SecurityPageName, +} from '../../../../common/constants'; import { createStore } from '../../store'; import { useUserInfo, @@ -25,9 +29,12 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, mockSourcererState, + TestProviders, } from '../../mock'; import { SelectedDataView, SourcererScopeName } from '../../store/sourcerer/model'; import { postSourcererDataView } from './api'; +import { sourcererActions } from '../../store/sourcerer'; +import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string'; const mockRouteSpy: RouteSpyState = { pageName: SecurityPageName.overview, @@ -40,6 +47,7 @@ const mockDispatch = jest.fn(); const mockUseUserInfo = useUserInfo as jest.Mock; jest.mock('../../../detections/components/user_info'); jest.mock('./api'); +jest.mock('../../utils/global_query_string'); jest.mock('react-redux', () => { const original = jest.requireActual('react-redux'); @@ -52,6 +60,8 @@ jest.mock('../../utils/route/use_route_spy', () => ({ useRouteSpy: () => [mockRouteSpy], })); +(useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) => onInitialize({})); + const mockSearch = jest.fn(); const mockAddWarning = jest.fn(); @@ -188,6 +198,50 @@ describe('Sourcerer Hooks', () => { }); }); + it('initilizes dataview with data from query string', async () => { + const selectedPatterns = ['testPattern-*']; + const selectedDataViewId = 'security-solution-default'; + (useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) => + onInitialize({ + [SourcererScopeName.default]: { + id: selectedDataViewId, + selectedPatterns, + }, + }) + ); + + renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + + expect(mockDispatch).toHaveBeenCalledWith( + sourcererActions.setSelectedDataView({ + id: SourcererScopeName.default, + selectedDataViewId, + selectedPatterns, + }) + ); + }); + + it('sets default selected patterns to the URL when there is no sorcerer URL param in the query string', async () => { + const updateUrlParam = jest.fn(); + (useUpdateUrlParam as jest.Mock).mockReturnValue(updateUrlParam); + (useInitializeUrlParam as jest.Mock).mockImplementation((_, onInitialize) => + onInitialize(null) + ); + + renderHook(() => useInitSourcerer(), { + wrapper: ({ children }) => {children}, + }); + + expect(updateUrlParam).toHaveBeenCalledWith({ + [SourcererScopeName.default]: { + id: DEFAULT_DATA_VIEW_ID, + selectedPatterns: DEFAULT_INDEX_PATTERN, + }, + }); + }); + it('calls addWarning if defaultDataView has an error', async () => { store = createStore( { diff --git a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx index 140f00e63a0a3..b29379790dd5b 100644 --- a/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/sourcerer/index.tsx @@ -14,17 +14,18 @@ import { SelectedDataView, SourcererDataView, SourcererScopeName, + SourcererUrlState, } from '../../store/sourcerer/model'; import { useUserInfo } from '../../../detections/components/user_info'; import { timelineSelectors } from '../../../timelines/store/timeline'; import { ALERTS_PATH, - CASES_PATH, HOSTS_PATH, USERS_PATH, NETWORK_PATH, OVERVIEW_PATH, RULES_PATH, + CASES_PATH, } from '../../../../common/constants'; import { TimelineId } from '../../../../common/types'; import { useDeepEqualSelector } from '../../hooks/use_selector'; @@ -37,6 +38,8 @@ import { useAppToasts } from '../../hooks/use_app_toasts'; import { postSourcererDataView } from './api'; import { useDataView } from '../source/use_data_view'; import { useFetchIndex } from '../source'; +import { useInitializeUrlParam, useUpdateUrlParam } from '../../utils/global_query_string'; +import { CONSTANTS } from '../../components/url_state/constants'; export const useInitSourcerer = ( scopeId: SourcererScopeName.default | SourcererScopeName.detections = SourcererScopeName.default @@ -46,6 +49,7 @@ export const useInitSourcerer = ( const initialTimelineSourcerer = useRef(true); const initialDetectionSourcerer = useRef(true); const { loading: loadingSignalIndex, isSignalIndexExists, signalIndexName } = useUserInfo(); + const updateUrlParam = useUpdateUrlParam(CONSTANTS.sourcerer); const getDataViewsSelector = useMemo( () => sourcererSelectors.getSourcererDataViewsSelector(), @@ -88,6 +92,41 @@ export const useInitSourcerer = ( } = useDeepEqualSelector((state) => scopeIdSelector(state, SourcererScopeName.timeline)); const { indexFieldsSearch } = useDataView(); + const onInitializeUrlParam = useCallback( + (initialState: SourcererUrlState | null) => { + // Initialize the store with value from UrlParam. + if (initialState != null) { + (Object.keys(initialState) as SourcererScopeName[]).forEach((scope) => { + if ( + !(scope === SourcererScopeName.default && scopeId === SourcererScopeName.detections) + ) { + dispatch( + sourcererActions.setSelectedDataView({ + id: scope, + selectedDataViewId: initialState[scope]?.id ?? null, + selectedPatterns: initialState[scope]?.selectedPatterns ?? [], + }) + ); + } + }); + } else { + // Initialize the UrlParam with values from the store. + // It isn't strictly necessary but I am keeping it for compatibility with the previous implementation. + if (scopeDataViewId) { + updateUrlParam({ + [SourcererScopeName.default]: { + id: scopeDataViewId, + selectedPatterns, + }, + }); + } + } + }, + [dispatch, scopeDataViewId, scopeId, selectedPatterns, updateUrlParam] + ); + + useInitializeUrlParam(CONSTANTS.sourcerer, onInitializeUrlParam); + /* * Note for future engineer: * we changed the logic to not fetch all the index fields for every data view on the loading of the app diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx index 5095a60215135..480ecdb3674ff 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.test.tsx @@ -37,16 +37,22 @@ describe('useGlobalTime', () => { expect(result1.to).toBe(0); }); - test('clear all queries at unmount', () => { - const { rerender } = renderHook(() => useGlobalTime()); - act(() => rerender()); + test('clear all queries at unmount when clearAllQuery is set to true', () => { + const { unmount } = renderHook(() => useGlobalTime()); + unmount(); expect(mockDispatch.mock.calls[0][0].type).toEqual( 'x-pack/security_solution/local/inputs/DELETE_ALL_QUERY' ); }); - test('do NOT clear all queries at unmount', () => { - const { rerender } = renderHook(() => useGlobalTime(false)); + test('do NOT clear all queries at unmount when clearAllQuery is set to false.', () => { + const { unmount } = renderHook(() => useGlobalTime(false)); + unmount(); + expect(mockDispatch.mock.calls.length).toBe(0); + }); + + test('do NOT clear all queries when setting state and clearAllQuery is set to true', () => { + const { rerender } = renderHook(() => useGlobalTime()); act(() => rerender()); expect(mockDispatch.mock.calls.length).toBe(0); }); diff --git a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx index 885b3cfdcac3f..960a23e7898f6 100644 --- a/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx +++ b/x-pack/plugins/security_solution/public/common/containers/use_global_time/index.tsx @@ -33,15 +33,17 @@ export const useGlobalTime = (clearAllQuery: boolean = true) => { ); useEffect(() => { - if (isInitializing) { - setIsInitializing(false); - } + setIsInitializing(false); + }, []); + + // This effect must not have any mutable dependencies. Otherwise, the cleanup function gets called before the component unmounts. + useEffect(() => { return () => { if (clearAllQuery) { dispatch(inputsActions.deleteAllQuery({ id: 'global' })); } }; - }, [clearAllQuery, dispatch, isInitializing]); + }, [dispatch, clearAllQuery]); const memoizedReturn = useMemo( () => ({ diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index d252553586a8b..89f4dab31c750 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -388,6 +388,7 @@ export const mockGlobalState: State = { }, }, }, + globalUrlParam: {}, /** * These state's are wrapped in `Immutable`, but for compatibility with the overall app architecture, * they are cast to mutable versions here. diff --git a/x-pack/plugins/security_solution/public/common/store/global_url_param/actions.ts b/x-pack/plugins/security_solution/public/common/store/global_url_param/actions.ts new file mode 100644 index 0000000000000..0b3c90e158151 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/global_url_param/actions.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 actionCreatorFactory from 'typescript-fsa'; + +const actionCreator = actionCreatorFactory('x-pack/security_solution/local/global_url_param'); + +export const registerUrlParam = actionCreator<{ key: string; initialValue: string | null }>( + 'REGISTER_URL_PARAM' +); + +export const deregisterUrlParam = actionCreator<{ key: string }>('DEREGISTER_URL_PARAM'); + +export const updateUrlParam = actionCreator<{ key: string; value: string | null }>( + 'UPDATE_URL_PARAM' +); diff --git a/x-pack/plugins/security_solution/public/common/store/global_url_param/index.ts b/x-pack/plugins/security_solution/public/common/store/global_url_param/index.ts new file mode 100644 index 0000000000000..1ee5a84c1653d --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/global_url_param/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as globalUrlParamActions from './actions'; +import * as globalUrlParamSelectors from './selectors'; + +export { globalUrlParamActions, globalUrlParamSelectors }; + +export * from './reducer'; diff --git a/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.test.ts b/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.test.ts new file mode 100644 index 0000000000000..dbe7dbeb05eab --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.test.ts @@ -0,0 +1,89 @@ +/* + * 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 { deregisterUrlParam, registerUrlParam, updateUrlParam } from './actions'; +import { globalUrlParamReducer, initialGlobalUrlParam } from './reducer'; + +const error = jest.spyOn(console, 'error').mockImplementation(() => {}); + +describe('globalUrlParamReducer', () => { + describe('#registerUrlParam', () => { + it('registers the URL param', () => { + const key = 'testKey'; + const initialValue = 'testValue'; + const state = globalUrlParamReducer( + initialGlobalUrlParam, + registerUrlParam({ key, initialValue }) + ); + + expect(state).toEqual({ [key]: initialValue }); + }); + + it('throws exception when a key is register twice', () => { + const key = 'testKey'; + const initialValue = 'testValue'; + const newState = globalUrlParamReducer( + initialGlobalUrlParam, + registerUrlParam({ key, initialValue }) + ); + + globalUrlParamReducer(newState, registerUrlParam({ key, initialValue })); + + expect(error).toHaveBeenCalledWith("Url param key 'testKey' is already being used."); + }); + }); + + describe('#deregisterUrlParam', () => { + it('deregisters the URL param', () => { + const key = 'testKey'; + const initialValue = 'testValue'; + let state = globalUrlParamReducer( + initialGlobalUrlParam, + registerUrlParam({ key, initialValue }) + ); + + expect(state).toEqual({ [key]: initialValue }); + + state = globalUrlParamReducer(initialGlobalUrlParam, deregisterUrlParam({ key })); + + expect(state).toEqual({}); + }); + }); + + describe('#updateUrlParam', () => { + it('updates URL param', () => { + const key = 'testKey'; + const value = 'new test value'; + + const state = globalUrlParamReducer( + { [key]: 'old test value' }, + updateUrlParam({ key, value }) + ); + + expect(state).toEqual({ [key]: value }); + }); + + it("doesn't update the URL param if key isn't registered", () => { + const key = 'testKey'; + const value = 'testValue'; + + const state = globalUrlParamReducer(initialGlobalUrlParam, updateUrlParam({ key, value })); + + expect(state).toEqual(initialGlobalUrlParam); + }); + + it("doesn't update the state if new value is equal to store value", () => { + const key = 'testKey'; + const value = 'testValue'; + const intialState = { [key]: value }; + + const state = globalUrlParamReducer(intialState, updateUrlParam({ key, value })); + + expect(state).toBe(intialState); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.ts b/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.ts new file mode 100644 index 0000000000000..5e7d91515e9b6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/global_url_param/reducer.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 { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { registerUrlParam, updateUrlParam, deregisterUrlParam } from './actions'; + +export type GlobalUrlParam = Record; + +export const initialGlobalUrlParam: GlobalUrlParam = {}; + +export const globalUrlParamReducer = reducerWithInitialState(initialGlobalUrlParam) + .case(registerUrlParam, (state, { key, initialValue }) => { + // It doesn't allow the query param to be used twice + if (state[key] !== undefined) { + // eslint-disable-next-line no-console + console.error(`Url param key '${key}' is already being used.`); + return state; + } + + return { + ...state, + [key]: initialValue, + }; + }) + .case(deregisterUrlParam, (state, { key }) => { + const nextState = { ...state }; + + delete nextState[key]; + + return nextState; + }) + .case(updateUrlParam, (state, { key, value }) => { + // Only update the URL after the query param is registered and if the current value is different than the previous value + if (state[key] === undefined || state[key] === value) { + return state; + } + + return { + ...state, + [key]: value, + }; + }) + .build(); diff --git a/x-pack/plugins/security_solution/public/common/store/global_url_param/selectors.ts b/x-pack/plugins/security_solution/public/common/store/global_url_param/selectors.ts new file mode 100644 index 0000000000000..0bff4f0b12d88 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/store/global_url_param/selectors.ts @@ -0,0 +1,11 @@ +/* + * 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 { GlobalUrlParam } from '.'; +import { State } from '../types'; + +export const selectGlobalUrlParam = (state: State): GlobalUrlParam => state.globalUrlParam; diff --git a/x-pack/plugins/security_solution/public/common/store/reducer.ts b/x-pack/plugins/security_solution/public/common/store/reducer.ts index 538657064be90..23da338112fc1 100644 --- a/x-pack/plugins/security_solution/public/common/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/common/store/reducer.ts @@ -24,6 +24,7 @@ import { AppAction } from './actions'; import { initDataView, SourcererModel, SourcererScopeName } from './sourcerer/model'; import { ExperimentalFeatures } from '../../../common/experimental_features'; import { getScopePatternListSelection } from './sourcerer/helpers'; +import { globalUrlParamReducer, initialGlobalUrlParam } from './global_url_param'; export type SubPluginsInitReducer = HostsPluginReducer & UsersPluginReducer & @@ -36,7 +37,7 @@ export type SubPluginsInitReducer = HostsPluginReducer & export const createInitialState = ( pluginsInitState: Omit< SecuritySubPlugins['store']['initialState'], - 'app' | 'dragAndDrop' | 'inputs' | 'sourcerer' + 'app' | 'dragAndDrop' | 'inputs' | 'sourcerer' | 'globalUrlParam' >, { defaultDataView, @@ -100,6 +101,7 @@ export const createInitialState = ( kibanaDataViews: kibanaDataViews.map((dataView) => ({ ...initDataView, ...dataView })), signalIndexName, }, + globalUrlParam: initialGlobalUrlParam, }; return preloadedState; @@ -116,5 +118,6 @@ export const createReducer: ( dragAndDrop: dragAndDropReducer, inputs: inputsReducer, sourcerer: sourcererReducer, + globalUrlParam: globalUrlParamReducer, ...pluginsReducer, }); diff --git a/x-pack/plugins/security_solution/public/common/store/types.ts b/x-pack/plugins/security_solution/public/common/store/types.ts index 226ab872ca2ee..491877830034e 100644 --- a/x-pack/plugins/security_solution/public/common/store/types.ts +++ b/x-pack/plugins/security_solution/public/common/store/types.ts @@ -20,6 +20,7 @@ import { TimelinePluginState } from '../../timelines/store/timeline'; import { NetworkPluginState } from '../../network/store'; import { ManagementPluginState } from '../../management'; import { UsersPluginState } from '../../users/store'; +import { GlobalUrlParam } from './global_url_param'; export type StoreState = HostsPluginState & UsersPluginState & @@ -31,6 +32,7 @@ export type StoreState = HostsPluginState & dragAndDrop: DragAndDropState; inputs: InputsState; sourcerer: SourcererState; + globalUrlParam: GlobalUrlParam; }; /** * The redux `State` type for the Security App. diff --git a/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.test.tsx b/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.test.tsx new file mode 100644 index 0000000000000..8ba1a78d98fcf --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.test.tsx @@ -0,0 +1,303 @@ +/* + * 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 { renderHook } from '@testing-library/react-hooks'; +import { + useInitializeUrlParam, + useGlobalQueryString, + useSyncGlobalQueryString, + useUpdateUrlParam, +} from '.'; +import { GlobalUrlParam, globalUrlParamActions } from '../../store/global_url_param'; +import { mockHistory } from '../route/mocks'; +import { + createSecuritySolutionStorageMock, + kibanaObservable, + mockGlobalState, + SUB_PLUGINS_REDUCER, + TestProviders, +} from '../../mock'; +import { createStore } from '../../store'; +import { LinkInfo } from '../../links'; +import { SecurityPageName } from '../../../app/types'; + +const mockDispatch = jest.fn(); + +jest.mock('react-redux', () => { + const original = jest.requireActual('react-redux'); + return { + ...original, + useDispatch: () => mockDispatch, + }; +}); + +const mockLocation = jest.fn(); + +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: () => mockHistory, + useLocation: () => mockLocation(), +})); + +const defaultLinkInfo: LinkInfo = { + id: SecurityPageName.alerts, + path: '/test', + title: 'test title', + skipUrlState: false, +}; + +const mockLinkInfo = jest.fn().mockResolvedValue(defaultLinkInfo); + +jest.mock('../../links', () => ({ + ...jest.requireActual('../../links'), + getLinkInfo: () => mockLinkInfo(), +})); + +describe('global query string', () => { + const { storage } = createSecuritySolutionStorageMock(); + + const makeStore = (globalUrlParam: GlobalUrlParam) => + createStore( + { + ...mockGlobalState, + globalUrlParam, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + + const makeWrapper = (globalUrlParam?: GlobalUrlParam) => { + const wrapper = ({ children }: { children: React.ReactElement }) => ( + {children} + ); + return wrapper; + }; + + beforeAll(() => { + // allow window.location.search to be redefined + Object.defineProperty(window, 'location', { + value: { + search: '?', + }, + }); + }); + beforeEach(() => { + jest.clearAllMocks(); + window.location.search = '?'; + }); + describe('useInitializeUrlParam', () => { + it('calls onInitialize with decoded URL param value', () => { + const urlParamKey = 'testKey'; + mockLocation.mockReturnValue({ search: '?testKey=(test:(value:123))' }); + + const onInitialize = jest.fn(); + + renderHook(() => useInitializeUrlParam(urlParamKey, onInitialize), { + wrapper: makeWrapper(), + }); + + expect(onInitialize).toHaveBeenCalledWith({ test: { value: 123 } }); + }); + + it('deregister during unmount', () => { + const urlParamKey = 'testKey'; + mockLocation.mockReturnValue({ search: "?testKey='123'" }); + + const { unmount } = renderHook(() => useInitializeUrlParam(urlParamKey, () => {}), { + wrapper: makeWrapper(), + }); + unmount(); + + expect(mockDispatch).toBeCalledWith( + globalUrlParamActions.deregisterUrlParam({ + key: urlParamKey, + }) + ); + }); + + it('calls registerUrlParam global URL param action', () => { + const urlParamKey = 'testKey'; + const initialValue = 123; + mockLocation.mockReturnValue({ search: `?testKey=${initialValue}` }); + + renderHook(() => useInitializeUrlParam(urlParamKey, () => {}), { + wrapper: makeWrapper(), + }); + + expect(mockDispatch).toBeCalledWith( + globalUrlParamActions.registerUrlParam({ + key: urlParamKey, + initialValue: initialValue.toString(), + }) + ); + }); + }); + + describe('updateUrlParam', () => { + it('dispatch updateUrlParam action', () => { + const urlParamKey = 'testKey'; + const value = { test: 123 }; + const encodedVaue = '(test:123)'; + + const globalUrlParam = { + [urlParamKey]: 'oldValue', + }; + + const { + result: { current: updateUrlParam }, + } = renderHook(() => useUpdateUrlParam(urlParamKey), { + wrapper: makeWrapper(globalUrlParam), + }); + updateUrlParam(value); + + expect(mockDispatch).toBeCalledWith( + globalUrlParamActions.updateUrlParam({ + key: urlParamKey, + value: encodedVaue, + }) + ); + }); + + it('dispatch updateUrlParam action with null value', () => { + const urlParamKey = 'testKey'; + + const { + result: { current: updateUrlParam }, + } = renderHook(() => useUpdateUrlParam(urlParamKey), { + wrapper: makeWrapper(), + }); + updateUrlParam(null); + + expect(mockDispatch).toBeCalledWith( + globalUrlParamActions.updateUrlParam({ + key: urlParamKey, + value: null, + }) + ); + }); + }); + + describe('useGlobalQueryString', () => { + it('returns global query string', () => { + const store = createStore( + { + ...mockGlobalState, + globalUrlParam: { + testNumber: '123', + testObject: '(test:321)', + testNull: null, + testEmpty: '', + }, + }, + SUB_PLUGINS_REDUCER, + kibanaObservable, + storage + ); + const wrapper = ({ children }: { children: React.ReactElement }) => ( + {children} + ); + + const { result } = renderHook(() => useGlobalQueryString(), { wrapper }); + + expect(result.current).toEqual('testNumber=123&testObject=(test:321)'); + }); + }); + + describe('useSyncGlobalQueryString', () => { + it("doesn't delete other URL params when updating one", async () => { + const urlParamKey = 'testKey'; + const value = '123'; + const globalUrlParam = { + [urlParamKey]: value, + }; + window.location.search = `?firstKey=111&${urlParamKey}=oldValue&lastKey=999`; + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).toHaveBeenCalledWith({ + search: `firstKey=111&${urlParamKey}=${value}&lastKey=999`, + }); + }); + + it('updates URL params', async () => { + const urlParamKey1 = 'testKey1'; + const value1 = '1111'; + const urlParamKey2 = 'testKey2'; + const value2 = '2222'; + const globalUrlParam = { + [urlParamKey1]: value1, + [urlParamKey2]: value2, + }; + window.location.search = `?`; + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).toHaveBeenCalledWith({ + search: `${urlParamKey1}=${value1}&${urlParamKey2}=${value2}`, + }); + }); + + it('deletes URL param when value is null', async () => { + const urlParamKey = 'testKey'; + const globalUrlParam = { + [urlParamKey]: null, + }; + window.location.search = `?${urlParamKey}=oldValue`; + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).toHaveBeenCalledWith({ + search: '', + }); + }); + + it('deletes URL param when page has skipUrlState=true', async () => { + const urlParamKey = 'testKey'; + const value = 'testValue'; + const globalUrlParam = { + [urlParamKey]: value, + }; + window.location.search = `?${urlParamKey}=${value}`; + mockLinkInfo.mockReturnValue({ ...defaultLinkInfo, skipUrlState: true }); + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).toHaveBeenCalledWith({ + search: '', + }); + }); + + it('does not replace URL param when the value does not change', async () => { + const urlParamKey = 'testKey'; + const value = 'testValue'; + const globalUrlParam = { + [urlParamKey]: value, + }; + window.location.search = `?${urlParamKey}=${value}`; + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).not.toHaveBeenCalledWith(); + }); + + it('does not replace URL param when the page doe not exist', async () => { + const urlParamKey = 'testKey'; + const value = 'testValue'; + const globalUrlParam = { + [urlParamKey]: value, + }; + window.location.search = `?${urlParamKey}=oldValue`; + mockLinkInfo.mockReturnValue(undefined); + + renderHook(() => useSyncGlobalQueryString(), { wrapper: makeWrapper(globalUrlParam) }); + + expect(mockHistory.replace).not.toHaveBeenCalledWith(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.ts b/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.ts new file mode 100644 index 0000000000000..4d2b266f9622a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/utils/global_query_string/index.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as H from 'history'; +import { parse, ParsedQuery, stringify } from 'query-string'; +import { useCallback, useEffect, useMemo } from 'react'; + +import { url } from '@kbn/kibana-utils-plugin/public'; +import { isEmpty, pickBy } from 'lodash/fp'; +import { useHistory, useLocation } from 'react-router-dom'; +import { useDispatch } from 'react-redux'; +import { + decodeRisonUrlState, + encodeRisonUrlState, + getParamFromQueryString, + getQueryStringFromLocation, +} from '../../components/url_state/helpers'; +import { useShallowEqualSelector } from '../../hooks/use_selector'; +import { globalUrlParamActions, globalUrlParamSelectors } from '../../store/global_url_param'; +import { useRouteSpy } from '../route/use_route_spy'; +import { getLinkInfo } from '../../links'; + +/** + * Adds urlParamKey and the initial value to redux store. + * + * Please call this hook at the highest possible level of the rendering tree. + * So it is only called when the application starts instead of on every page. + * + * @param urlParamKey Must not change. + * @param onInitialize Called once when initializing. + */ +export const useInitializeUrlParam = ( + urlParamKey: string, + /** + * @param state Decoded URL param value. + */ + onInitialize: (state: State | null) => void +) => { + const dispatch = useDispatch(); + const { search } = useLocation(); + + useEffect(() => { + const initialValue = getParamFromQueryString(getQueryStringFromLocation(search), urlParamKey); + + dispatch( + globalUrlParamActions.registerUrlParam({ + key: urlParamKey, + initialValue: initialValue ?? null, + }) + ); + + // execute consumer initialization + onInitialize(decodeRisonUrlState(initialValue ?? undefined)); + + return () => { + dispatch(globalUrlParamActions.deregisterUrlParam({ key: urlParamKey })); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps -- It must run only once when the application is initializing. + }, []); +}; + +/** + * Updates URL parameters in the url. + * + * Make sure to call `useInitializeUrlParam` before calling this function. + */ +export const useUpdateUrlParam = (urlParamKey: string) => { + const dispatch = useDispatch(); + + const updateUrlParam = useCallback( + (value: State | null) => { + const encodedValue = value !== null ? encodeRisonUrlState(value) : null; + dispatch(globalUrlParamActions.updateUrlParam({ key: urlParamKey, value: encodedValue })); + }, + [dispatch, urlParamKey] + ); + + return updateUrlParam; +}; + +export const useGlobalQueryString = (): string => { + const globalUrlParam = useShallowEqualSelector(globalUrlParamSelectors.selectGlobalUrlParam); + + const globalQueryString = useMemo( + () => encodeQueryString(pickBy((value) => !isEmpty(value), globalUrlParam)), + [globalUrlParam] + ); + + return globalQueryString; +}; + +/** + * - It hides / shows the global query depending on the page. + * - It updates the URL when globalUrlParam store updates. + */ +export const useSyncGlobalQueryString = () => { + const history = useHistory(); + const [{ pageName }] = useRouteSpy(); + const globalUrlParam = useShallowEqualSelector(globalUrlParamSelectors.selectGlobalUrlParam); + + useEffect(() => { + const linkInfo = getLinkInfo(pageName) ?? { skipUrlState: true }; + const params = Object.entries(globalUrlParam).map(([key, value]) => ({ + key, + value: linkInfo.skipUrlState ? null : value, + })); + + if (params.length > 0) { + // window.location.search provides the most updated representation of the url search. + // It prevents unnecessary re-renders which useLocation would create because 'replaceUrlParams' does update the location. + // window.location.search also guarantees that we don't overwrite URL param managed outside react-router. + replaceUrlParams(params, history, window.location.search); + } + }, [globalUrlParam, pageName, history]); +}; + +const encodeQueryString = (urlParams: ParsedQuery): string => + stringify(url.encodeQuery(urlParams), { sort: false, encode: false }); + +const replaceUrlParams = ( + params: Array<{ key: string; value: string | null }>, + history: H.History, + search: string +) => { + const urlParams = parse(search, { sort: false }); + + params.forEach(({ key, value }) => { + if (value == null || value === '') { + delete urlParams[key]; + } else { + urlParams[key] = value; + } + }); + + const newSearch = encodeQueryString(urlParams); + + if (getQueryStringFromLocation(search) !== newSearch) { + history.replace({ search: newSearch }); + } +}; diff --git a/x-pack/plugins/security_solution/public/common/utils/route/helpers.ts b/x-pack/plugins/security_solution/public/common/utils/route/helpers.ts index 1066680621242..9e4740b6f4adf 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/helpers.ts @@ -7,11 +7,12 @@ import { noop } from 'lodash/fp'; import { createContext, Dispatch } from 'react'; +import { SecurityPageName } from '../../../app/types'; import { RouteSpyState, RouteSpyAction } from './types'; export const initRouteSpy: RouteSpyState = { - pageName: '', + pageName: SecurityPageName.noPage, detailName: undefined, tabName: undefined, search: '', diff --git a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx index f450b7d26d2f7..03ccabf5ecce9 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/index.test.tsx @@ -13,6 +13,7 @@ import { ManageRoutesSpy } from './manage_spy_routes'; import { SpyRouteComponent } from './spy_routes'; import { useRouteSpy } from './use_route_spy'; import { generateHistoryMock, generateRoutesMock } from './mocks'; +import { SecurityPageName } from '../../../app/types'; const mockUseRouteSpy: jest.Mock = useRouteSpy as jest.Mock; jest.mock('./use_route_spy', () => ({ @@ -81,7 +82,7 @@ describe('Spy Routes', () => { flowTarget: undefined, }, }} - pageName="hosts" + pageName={SecurityPageName.hosts} /> ); @@ -127,7 +128,7 @@ describe('Spy Routes', () => { flowTarget: undefined, }, }} - pageName="hosts" + pageName={SecurityPageName.hosts} /> ); @@ -146,7 +147,7 @@ describe('Spy Routes', () => { path: newPathname, url: newPathname, params: { - pageName: 'hosts', + pageName: SecurityPageName.hosts, detailName: undefined, tabName: HostsTableType.authentications, search: '', @@ -162,7 +163,7 @@ describe('Spy Routes', () => { route: { detailName: undefined, history: mockHistoryValue, - pageName: 'hosts', + pageName: SecurityPageName.hosts, pathName: newPathname, tabName: HostsTableType.authentications, search: '?updated="true"', diff --git a/x-pack/plugins/security_solution/public/common/utils/route/mocks.ts b/x-pack/plugins/security_solution/public/common/utils/route/mocks.ts index 19b9e93fa5476..a904c4485aaa3 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/mocks.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/mocks.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { SecurityPageName } from '../../../app/types'; import { RouteSpyState } from './types'; type Action = 'PUSH' | 'POP' | 'REPLACE'; @@ -35,7 +36,7 @@ export const generateHistoryMock = () => ({ export const mockHistory = generateHistoryMock(); export const generateRoutesMock = (): RouteSpyState => ({ - pageName: '', + pageName: SecurityPageName.noPage, detailName: undefined, tabName: undefined, search: '', diff --git a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx index ab48ec0b6e006..12ae848a59efa 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx +++ b/x-pack/plugins/security_solution/public/common/utils/route/spy_routes.tsx @@ -15,7 +15,7 @@ import { useRouteSpy } from './use_route_spy'; import { SecurityPageName } from '../../../../common/constants'; export const SpyRouteComponent = memo< - SpyRouteProps & { location: H.Location; pageName: string | undefined } + SpyRouteProps & { location: H.Location; pageName: SecurityPageName | undefined } >( ({ location: { pathname, search }, diff --git a/x-pack/plugins/security_solution/public/common/utils/route/types.ts b/x-pack/plugins/security_solution/public/common/utils/route/types.ts index 67f59e6eec0d9..7cab113eadf0b 100644 --- a/x-pack/plugins/security_solution/public/common/utils/route/types.ts +++ b/x-pack/plugins/security_solution/public/common/utils/route/types.ts @@ -16,6 +16,7 @@ import { NetworkRouteType } from '../../../network/pages/navigation/types'; import { AdministrationSubTab as AdministrationType } from '../../../management/types'; import { FlowTarget } from '../../../../common/search_strategy'; import { UsersTableType } from '../../../users/store/model'; +import { SecurityPageName } from '../../../app/types'; export type SiemRouteType = | HostsTableType @@ -24,7 +25,7 @@ export type SiemRouteType = | AdministrationType | UsersTableType; export interface RouteSpyState { - pageName: string; + pageName: SecurityPageName; detailName: string | undefined; tabName: SiemRouteType | undefined; search: string; diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx index 37c48487912e0..30f027aa9a619 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.test.tsx @@ -18,12 +18,17 @@ import { const dateNow = new Date('2022-04-08T12:00:00.000Z').valueOf(); const mockDateNow = jest.fn().mockReturnValue(dateNow); Date.now = jest.fn(() => mockDateNow()) as unknown as DateConstructor['now']; +const mockSetQuery = jest.fn(); +const mockDeleteQuery = jest.fn(); jest.mock('../../../../common/containers/use_global_time', () => { return { - useGlobalTime: jest - .fn() - .mockReturnValue({ from: '2022-04-05T12:00:00.000Z', to: '2022-04-08T12:00:00.000Z' }), + useGlobalTime: jest.fn().mockReturnValue({ + from: '2022-04-05T12:00:00.000Z', + to: '2022-04-08T12:00:00.000Z', + setQuery: () => mockSetQuery(), + deleteQuery: () => mockDeleteQuery(), + }), }; }); jest.mock('../../../../common/lib/kibana'); @@ -101,6 +106,36 @@ describe('useCasesByStatus', () => { }); }); + test('it should call setQuery when fetching', async () => { + await act(async () => { + const { waitForNextUpdate } = renderHook( + () => useCasesByStatus({ skip: false }), + { + wrapper: TestProviders, + } + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(mockSetQuery).toHaveBeenCalled(); + }); + }); + + test('it should call deleteQuery when unmounting', async () => { + await act(async () => { + const { waitForNextUpdate, unmount } = renderHook< + UseCasesByStatusProps, + UseCasesByStatusResults + >(() => useCasesByStatus({ skip: false }), { + wrapper: TestProviders, + }); + await waitForNextUpdate(); + + unmount(); + + expect(mockDeleteQuery).toHaveBeenCalled(); + }); + }); + test('skip', async () => { const abortSpy = jest.spyOn(AbortController.prototype, 'abort'); await act(async () => { diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx index 580f91dea6d7d..3f903e1e73803 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_by_status/use_cases_by_status.tsx @@ -6,7 +6,8 @@ */ import { CasesStatus } from '@kbn/cases-plugin/common/ui'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; +import uuid from 'uuid'; import { APP_ID } from '../../../../../common/constants'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useKibana } from '../../../../common/lib/kibana'; @@ -28,8 +29,9 @@ export const useCasesByStatus = ({ skip = false }) => { const { services: { cases }, } = useKibana(); - const { to, from } = useGlobalTime(); - + const { to, from, setQuery, deleteQuery } = useGlobalTime(); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `useCaseItems-${uuid.v4()}`, []); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [isLoading, setIsLoading] = useState(true); const [casesCounts, setCasesCounts] = useState(null); @@ -64,6 +66,12 @@ export const useCasesByStatus = ({ skip = false }) => { if (!skip) { fetchCases(); + setQuery({ + id: uniqueQueryId, + inspect: null, + loading: false, + refetch: fetchCases, + }); } if (skip) { @@ -75,8 +83,9 @@ export const useCasesByStatus = ({ skip = false }) => { return () => { isSubscribed = false; abortCtrl.abort(); + deleteQuery({ id: uniqueQueryId }); }; - }, [cases.api.cases, from, skip, to]); + }, [cases.api.cases, from, skip, to, setQuery, deleteQuery, uniqueQueryId]); return { closed: casesCounts?.countClosedCases ?? 0, diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts index d112aaac7d5ad..2e2578b0d7294 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.test.ts @@ -38,8 +38,12 @@ jest.mock('../../../../common/lib/kibana', () => ({ const from = '2020-07-07T08:20:18.966Z'; const to = '2020-07-08T08:20:18.966Z'; +const mockSetQuery = jest.fn(); +const mockDeleteQuery = jest.fn(); -const mockUseGlobalTime = jest.fn().mockReturnValue({ from, to }); +const mockUseGlobalTime = jest + .fn() + .mockReturnValue({ from, to, setQuery: mockSetQuery, deleteQuery: mockDeleteQuery }); jest.mock('../../../../common/containers/use_global_time', () => { return { useGlobalTime: (...props: unknown[]) => mockUseGlobalTime(...props), @@ -100,6 +104,31 @@ describe('useCaseItems', () => { }); }); + test('it should call setQuery when fetching', async () => { + mockCasesApi.mockReturnValue(mockCasesResult); + await act(async () => { + const { waitForNextUpdate } = renderUseCaseItems(); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(mockSetQuery).toHaveBeenCalled(); + }); + }); + + test('it should call deleteQuery when unmounting', async () => { + await act(async () => { + const { waitForNextUpdate, unmount } = renderUseCaseItems(); + + await waitForNextUpdate(); + await waitForNextUpdate(); + + unmount(); + + expect(mockDeleteQuery).toHaveBeenCalled(); + }); + }); + it('should return new updatedAt', async () => { const newDateNow = new Date('2022-04-08T14:00:00.000Z').valueOf(); mockDateNow.mockReturnValue(newDateNow); diff --git a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts index c0645f263c770..709fe8c125655 100644 --- a/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts +++ b/x-pack/plugins/security_solution/public/overview/components/detection_response/cases_table/use_case_items.ts @@ -5,11 +5,12 @@ * 2.0. */ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { CaseStatuses } from '@kbn/cases-plugin/common'; import { Cases } from '@kbn/cases-plugin/common/ui'; +import uuid from 'uuid'; import { APP_ID } from '../../../../../common/constants'; import { useGlobalTime } from '../../../../common/containers/use_global_time'; import { useKibana } from '../../../../common/lib/kibana'; @@ -40,10 +41,12 @@ export const useCaseItems: UseCaseItems = ({ skip }) => { const { services: { cases }, } = useKibana(); - const { to, from } = useGlobalTime(); + const { to, from, setQuery, deleteQuery } = useGlobalTime(); const [isLoading, setIsLoading] = useState(true); const [updatedAt, setUpdatedAt] = useState(Date.now()); const [items, setItems] = useState([]); + // create a unique, but stable (across re-renders) query id + const uniqueQueryId = useMemo(() => `useCaseItems-${uuid.v4()}`, []); useEffect(() => { let isSubscribed = true; @@ -75,6 +78,13 @@ export const useCaseItems: UseCaseItems = ({ skip }) => { if (!skip) { fetchCases(); + + setQuery({ + id: uniqueQueryId, + inspect: null, + loading: false, + refetch: fetchCases, + }); } if (skip) { @@ -86,8 +96,9 @@ export const useCaseItems: UseCaseItems = ({ skip }) => { return () => { isSubscribed = false; abortCtrl.abort(); + deleteQuery({ id: uniqueQueryId }); }; - }, [cases.api.cases, from, skip, to]); + }, [cases.api.cases, from, skip, to, setQuery, deleteQuery, uniqueQueryId]); return { items, isLoading, updatedAt }; }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap index 923509ccfccab..db8bf464ce17e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/__snapshots__/field_renderers.test.tsx.snap @@ -146,7 +146,7 @@ exports[`Field Renderers #hostIdRenderer it renders correctly against snapshot 1 raspberrypi @@ -201,7 +201,7 @@ exports[`Field Renderers #hostNameRenderer it renders correctly against snapshot raspberrypi diff --git a/x-pack/plugins/synthetics/e2e/config.ts b/x-pack/plugins/synthetics/e2e/config.ts index 8f55b497e61ca..ed71896a8fffe 100644 --- a/x-pack/plugins/synthetics/e2e/config.ts +++ b/x-pack/plugins/synthetics/e2e/config.ts @@ -52,11 +52,11 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, `--elasticsearch.ignoreVersionMismatch=${process.env.CI ? 'false' : 'true'}`, - `--uiSettings.overrides.theme:darkMode=true`, `--elasticsearch.username=kibana_system`, `--elasticsearch.password=changeme`, '--xpack.reporting.enabled=false', `--xpack.uptime.service.manifestUrl=${manifestUrl}`, + `--xpack.uptime.service.showExperimentalLocations=true`, `--xpack.uptime.service.username=${ process.env.SYNTHETICS_REMOTE_ENABLED ? serviceUsername diff --git a/x-pack/plugins/synthetics/e2e/journeys/index.ts b/x-pack/plugins/synthetics/e2e/journeys/index.ts index 1d9fe3e8f33d3..47d77dcafd544 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/index.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/index.ts @@ -5,7 +5,7 @@ * 2.0. */ export * from './alerts'; -// export * from './synthetics'; // TODO: Enable these in a follow up PR +export * from './synthetics'; export * from './data_view_permissions'; export * from './uptime.journey'; export * from './step_duration.journey'; diff --git a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts index acef9e96e7f2d..40035e9c70923 100644 --- a/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts +++ b/x-pack/plugins/synthetics/e2e/journeys/synthetics/getting_started.journey.ts @@ -7,7 +7,6 @@ import { journey, step, expect, before, Page } from '@elastic/synthetics'; import { syntheticsAppPageProvider } from '../../page_objects/synthetics_app'; -import { byTestId } from '../utils'; journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: any }) => { const syntheticsApp = syntheticsAppPageProvider({ page, kibanaUrl: params.kibanaUrl }); @@ -16,7 +15,6 @@ journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: a await syntheticsApp.fillFirstMonitorDetails({ url: 'https://www.elastic.co', locations: ['us_central'], - apmServiceName: 'synthetics', }); }; @@ -34,9 +32,13 @@ journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: a expect(await invalid.isVisible()).toBeFalsy(); }); - step('shows validation error on touch', async () => { - await page.click(byTestId('urls-input')); - await page.click(byTestId('comboBoxInput')); + step('enable monitor management', async () => { + await syntheticsApp.enableMonitorManagement(true); + }); + + step('shows validation error on submit', async () => { + await page.click('text=Create monitor'); + expect(await page.isVisible('text=URL is required')).toBeTruthy(); }); @@ -47,6 +49,5 @@ journey(`Getting Started Page`, async ({ page, params }: { page: Page; params: a step('it navigates to details page after saving', async () => { await page.click('text=Dismiss'); - expect(await page.isVisible('text=My first monitor')).toBeTruthy(); }); }); diff --git a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx index 1444e4282012f..13ef33a726163 100644 --- a/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx +++ b/x-pack/plugins/synthetics/e2e/page_objects/synthetics_app.tsx @@ -8,14 +8,19 @@ import { Page } from '@elastic/synthetics'; import { loginPageProvider } from './login'; import { utilsPageProvider } from './utils'; +const SIXTY_SEC_TIMEOUT = { + timeout: 60 * 1000, +}; + export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kibanaUrl: string }) { const remoteKibanaUrl = process.env.SYNTHETICS_REMOTE_KIBANA_URL; const remoteUsername = process.env.SYNTHETICS_REMOTE_KIBANA_USERNAME; const remotePassword = process.env.SYNTHETICS_REMOTE_KIBANA_PASSWORD; const isRemote = Boolean(process.env.SYNTHETICS_REMOTE_ENABLED); const basePath = isRemote ? remoteKibanaUrl : kibanaUrl; - const monitorManagement = `${basePath}/app/synthetics/manage-monitors`; + const monitorManagement = `${basePath}/app/synthetics/monitors`; const addMonitor = `${basePath}/app/uptime/add-monitor`; + return { ...loginPageProvider({ page, @@ -40,7 +45,7 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib }, async getAddMonitorButton() { - return await this.findByTestSubj('syntheticsAddMonitorBtn'); + return await this.findByText('Create monitor'); }, async navigateToAddMonitor() { @@ -65,23 +70,46 @@ export function syntheticsAppPageProvider({ page, kibanaUrl }: { page: Page; kib async selectLocations({ locations }: { locations: string[] }) { for (let i = 0; i < locations.length; i++) { - await page.click(this.byTestId(`syntheticsServiceLocation--${locations[i]}`)); + await page.click( + this.byTestId(`syntheticsServiceLocation--${locations[i]}`), + SIXTY_SEC_TIMEOUT + ); } }, - async fillFirstMonitorDetails({ - url, - apmServiceName, - locations, - }: { - url: string; - apmServiceName: string; - locations: string[]; - }) { + async fillFirstMonitorDetails({ url, locations }: { url: string; locations: string[] }) { await this.fillByTestSubj('urls-input', url); await page.click(this.byTestId('comboBoxInput')); await this.selectLocations({ locations }); await page.click(this.byTestId('urls-input')); }, + + async enableMonitorManagement(shouldEnable: boolean = true) { + const isEnabled = await this.checkIsEnabled(); + if (isEnabled === shouldEnable) { + return; + } + const [toggle, button] = await Promise.all([ + page.$(this.byTestId('syntheticsEnableSwitch')), + page.$(this.byTestId('syntheticsEnableButton')), + ]); + + if (toggle === null && button === null) { + return null; + } + if (toggle) { + if (isEnabled !== shouldEnable) { + await toggle.click(); + } + } else { + await button?.click(); + } + }, + async checkIsEnabled() { + await page.waitForTimeout(5 * 1000); + const addMonitorBtn = await this.getAddMonitorButton(); + const isDisabled = await addMonitorBtn.isDisabled(); + return !isDisabled; + }, }; } diff --git a/x-pack/plugins/synthetics/e2e/parse_args_params.ts b/x-pack/plugins/synthetics/e2e/parse_args_params.ts index 9e7819bee5d2e..a69cae912dfee 100644 --- a/x-pack/plugins/synthetics/e2e/parse_args_params.ts +++ b/x-pack/plugins/synthetics/e2e/parse_args_params.ts @@ -13,7 +13,7 @@ const { argv } = yargs(process.argv.slice(2)) type: 'boolean', description: 'Start in headless mode', }) - .option('pauseOnError', { + .option('bail', { default: false, type: 'boolean', description: 'Pause on error', diff --git a/x-pack/plugins/synthetics/e2e/synthetics_run.ts b/x-pack/plugins/synthetics/e2e/synthetics_run.ts index 345e153a8c86c..64f4e2a8ba2f6 100644 --- a/x-pack/plugins/synthetics/e2e/synthetics_run.ts +++ b/x-pack/plugins/synthetics/e2e/synthetics_run.ts @@ -10,7 +10,7 @@ import { SyntheticsRunner } from './synthetics_start'; import { argv } from './parse_args_params'; -const { headless, grep, pauseOnError } = argv; +const { headless, grep, bail: pauseOnError } = argv; async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) { const kibanaConfig = await readConfigFile(require.resolve('./config.ts')); diff --git a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx index 8dbeaa74d618d..7ac40ae361ae6 100644 --- a/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx +++ b/x-pack/plugins/synthetics/public/apps/synthetics/components/monitors_page/management/page_header/monitors_page_header.tsx @@ -9,6 +9,7 @@ import React, { useContext } from 'react'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiBetaBadge, EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; +import { useEnablement } from '../../../../hooks'; import { MONITOR_ADD_ROUTE } from '../../../../../../../common/constants'; import { SyntheticsSettingsContext } from '../../../../contexts/synthetics_settings_context'; @@ -18,6 +19,10 @@ import { BETA_TOOLTIP_MESSAGE } from '../labels'; export const MonitorsPageHeader = () => { const { basePath } = useContext(SyntheticsSettingsContext); + const { + enablement: { isEnabled }, + } = useEnablement(); + return ( @@ -38,6 +43,8 @@ export const MonitorsPageHeader = () => { iconSide="left" iconType="plusInCircleFilled" href={`${basePath}/app/uptime${MONITOR_ADD_ROUTE}`} + isDisabled={!isEnabled} + data-test-subj="syntheticsAddMonitorBtn" > { + describe('Welcome interstitial', () => { before(async () => { // Need to navigate to page first to clear storage before test can be run await PageObjects.common.navigateToUrl('home', undefined); @@ -30,11 +29,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('is displayed on a fresh install with Fleet setup executed', async () => { // Setup Fleet and verify the metrics index pattern was created await kibanaServer.request({ path: '/api/fleet/setup', method: 'POST' }); - const metricsIndexPattern = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - expect(metricsIndexPattern?.attributes.title).to.eql('metrics-*'); // Reload the home screen and verify the interstitial is displayed await PageObjects.common.navigateToUrl('home', undefined, { disableWelcomePrompt: false });