From 12895d8bd040afb930c5b8c55261ba15c59386a4 Mon Sep 17 00:00:00 2001 From: Alexey Antonov Date: Wed, 23 Jun 2021 18:48:22 +0300 Subject: [PATCH 01/62] [TSVB] fix tooltip on annotations with 's are not displayed correctly (#102892) * [TSVB] tooltip on annotations with 's are not displayed correctly Closes: #102631 * 'handlebars/dist/handlebars' -> 'handlebars' --- .../components/lib/{reorder.js => reorder.ts} | 2 +- ...lace_vars.test.js => replace_vars.test.ts} | 0 .../lib/{replace_vars.js => replace_vars.ts} | 24 +++++++++++++------ .../components/lib/tick_formatter.js | 2 +- .../components/vis_types/timeseries/vis.js | 4 +++- 5 files changed, 22 insertions(+), 10 deletions(-) rename src/plugins/vis_type_timeseries/public/application/components/lib/{reorder.js => reorder.ts} (85%) rename src/plugins/vis_type_timeseries/public/application/components/lib/{replace_vars.test.js => replace_vars.test.ts} (100%) rename src/plugins/vis_type_timeseries/public/application/components/lib/{replace_vars.js => replace_vars.ts} (77%) diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts similarity index 85% rename from src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts index 15c21e19af2a5..a026b5bb2051e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/reorder.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export const reorder = (list, startIndex, endIndex) => { +export const reorder = (list: unknown[], startIndex: number, endIndex: number) => { const result = Array.from(list); const [removed] = result.splice(startIndex, 1); result.splice(endIndex, 0, removed); diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts similarity index 100% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.test.ts diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts similarity index 77% rename from src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js rename to src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts index 458866f2098a0..2862fe933bfb7 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/replace_vars.ts @@ -6,20 +6,30 @@ * Side Public License, v 1. */ -import _ from 'lodash'; -import handlebars from 'handlebars/dist/handlebars'; -import { emptyLabel } from '../../../../common/empty_label'; +import handlebars from 'handlebars'; import { i18n } from '@kbn/i18n'; +import { emptyLabel } from '../../../../common/empty_label'; + +type CompileOptions = Parameters[1]; -export function replaceVars(str, args = {}, vars = {}) { +export function replaceVars( + str: string, + args: Record = {}, + vars: Record = {}, + compileOptions: Partial = {} +) { try { - // we need add '[]' for emptyLabel because this value contains special characters. (https://handlebarsjs.com/guide/expressions.html#literal-segments) + /** we need add '[]' for emptyLabel because this value contains special characters. + * @see (https://handlebarsjs.com/guide/expressions.html#literal-segments) **/ const template = handlebars.compile(str.split(emptyLabel).join(`[${emptyLabel}]`), { strict: true, knownHelpersOnly: true, + ...compileOptions, + }); + const string = template({ + ...vars, + args, }); - - const string = template(_.assign({}, vars, { args })); return string; } catch (e) { diff --git a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js index 70529be78567d..c1d82a182e509 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js +++ b/src/plugins/vis_type_timeseries/public/application/components/lib/tick_formatter.js @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import handlebars from 'handlebars/dist/handlebars'; import { isNumber } from 'lodash'; +import handlebars from 'handlebars'; import { isEmptyValue, DISPLAY_EMPTY_VALUE } from '../../../../common/last_value_utils'; import { inputFormats, outputFormats, isDuration } from '../lib/durations'; import { getFieldFormats } from '../../../services'; diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js index 8e59e8e1bb628..097b0a7b5e332 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/timeseries/vis.js @@ -51,7 +51,9 @@ class TimeseriesVisualization extends Component { }; applyDocTo = (template) => (doc) => { - const vars = replaceVars(template, null, doc); + const vars = replaceVars(template, null, doc, { + noEscape: true, + }); if (vars instanceof Error) { this.showToastNotification = vars.error.caused_by; From 5b0d325d7e441e605dba92a1ef8305d6be777d28 Mon Sep 17 00:00:00 2001 From: Pablo Machado Date: Wed, 23 Jun 2021 18:27:55 +0200 Subject: [PATCH 02/62] Fix breadcrumbs path reopens timeline when timeline modal is open (#101568) --- .../navigation/breadcrumbs/index.test.ts | 23 +++++++++++++++++++ .../navigation/breadcrumbs/index.ts | 8 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) 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 dffc7becaf42a..c869df6ad388e 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 @@ -306,6 +306,29 @@ describe('Navigation Breadcrumbs', () => { }, ]); }); + + test('should set "timeline.isOpen" to false when timeline is open', () => { + const breadcrumbs = getBreadcrumbsForRoute( + { + ...getMockObject('timelines', '/', undefined), + timeline: { + activeTab: TimelineTabs.query, + id: 'TIMELINE_ID', + isOpen: true, + graphEventId: 'GRAPH_EVENT_ID', + }, + }, + getUrlForAppMock + ); + expect(breadcrumbs).toEqual([ + { text: 'Security', href: 'securitySolutionoverview' }, + { + text: 'Timelines', + href: + "securitySolution:timelines?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=(activeTab:query,graphEventId:GRAPH_EVENT_ID,id:TIMELINE_ID,isOpen:!f)", + }, + ]); + }); }); describe('setBreadcrumbs()', () => { 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 605478900d066..a09945f705c58 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 @@ -61,10 +61,14 @@ const isAdminRoutes = (spyState: RouteSpyState): spyState is AdministrationRoute // eslint-disable-next-line complexity export const getBreadcrumbsForRoute = ( - object: RouteSpyState & TabNavigationProps, + objectParam: RouteSpyState & TabNavigationProps, getUrlForApp: GetUrlForApp ): ChromeBreadcrumb[] | null => { - const spyState: RouteSpyState = omit('navTabs', object); + const spyState: RouteSpyState = omit('navTabs', objectParam); + + // Sets `timeline.isOpen` to false in the state to avoid reopening the timeline on breadcrumb click. https://github.com/elastic/kibana/issues/100322 + const object = { ...objectParam, timeline: { ...objectParam.timeline, isOpen: false } }; + const overviewPath = getUrlForApp(APP_ID, { path: SecurityPageName.overview }); const siemRootBreadcrumb: ChromeBreadcrumb = { text: APP_NAME, From 28162810ad2aa90395c83e483f4cd4dc476b8c11 Mon Sep 17 00:00:00 2001 From: Jonathan Buttner <56361221+jonathan-buttner@users.noreply.github.com> Date: Wed, 23 Jun 2021 12:30:12 -0400 Subject: [PATCH 03/62] Fixing the generator to use bulk api to install endpoint package (#103094) --- .../endpoint/resolver_generator_script.ts | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts index 72f56f13eaddf..66ac744b3a50c 100644 --- a/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts +++ b/x-pack/plugins/security_solution/scripts/endpoint/resolver_generator_script.ts @@ -15,9 +15,16 @@ import { KbnClient } from '@kbn/test'; import { AxiosResponse } from 'axios'; import { indexHostsAndAlerts } from '../../common/endpoint/index_data'; import { ANCESTRY_LIMIT, EndpointDocGenerator } from '../../common/endpoint/generate_data'; -import { AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../../fleet/common/constants'; import { + AGENTS_SETUP_API_ROUTES, + EPM_API_ROUTES, + SETUP_API_ROUTE, +} from '../../../fleet/common/constants'; +import { + BulkInstallPackageInfo, + BulkInstallPackagesResponse, CreateFleetSetupResponse, + IBulkInstallPackageHTTPError, PostIngestSetupResponse, } from '../../../fleet/common/types/rest_spec'; import { KbnClientWithApiKeySupport } from './kbn_client_with_api_key_support'; @@ -44,6 +51,12 @@ async function deleteIndices(indices: string[], client: Client) { } } +function isFleetBulkInstallError( + installResponse: BulkInstallPackageInfo | IBulkInstallPackageHTTPError +): installResponse is IBulkInstallPackageHTTPError { + return 'error' in installResponse && installResponse.error !== undefined; +} + async function doIngestSetup(kbnClient: KbnClient) { // Setup Ingest try { @@ -76,6 +89,35 @@ async function doIngestSetup(kbnClient: KbnClient) { console.error(error); throw error; } + + // Install/upgrade the endpoint package + try { + const installEndpointPackageResp = (await kbnClient.request({ + path: EPM_API_ROUTES.BULK_INSTALL_PATTERN, + method: 'POST', + body: { + packages: ['endpoint'], + }, + })) as AxiosResponse; + + const bulkResp = installEndpointPackageResp.data.response; + if (bulkResp.length <= 0) { + throw new Error('Installing the Endpoint package failed, response was empty, existing'); + } + + if (isFleetBulkInstallError(bulkResp[0])) { + if (bulkResp[0].error instanceof Error) { + throw new Error( + `Installing the Endpoint package failed: ${bulkResp[0].error.message}, exiting` + ); + } + + throw new Error(bulkResp[0].error); + } + } catch (error) { + console.error(error); + throw error; + } } async function main() { From 3dc59a30aad18bfc8a25c338ea7ae669a7e961dd Mon Sep 17 00:00:00 2001 From: Devon Thomson Date: Wed, 23 Jun 2021 12:32:11 -0400 Subject: [PATCH 04/62] Lens on Dashboard 7.12.1 Smoke Tests (#102667) * added smoke tests for lens by value panels on dashboard --- .../services/dashboard/panel_actions.ts | 60 +++++++------- .../test/functional/apps/dashboard/index.ts | 2 + ...ens_dashboard_migration_test_7_12_1.ndjson | 7 ++ .../lens_migration_smoke_test.ts | 83 +++++++++++++++++++ 4 files changed, 124 insertions(+), 28 deletions(-) create mode 100644 x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson create mode 100644 x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts diff --git a/test/functional/services/dashboard/panel_actions.ts b/test/functional/services/dashboard/panel_actions.ts index 9aca790b0b437..4340f16492a7c 100644 --- a/test/functional/services/dashboard/panel_actions.ts +++ b/test/functional/services/dashboard/panel_actions.ts @@ -211,36 +211,29 @@ export class DashboardPanelActionsService extends FtrService { await this.testSubjects.click('confirmSaveSavedObjectButton'); } - async expectExistsRemovePanelAction() { - this.log.debug('expectExistsRemovePanelAction'); - await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); - } - - async expectExistsPanelAction(testSubject: string) { + async expectExistsPanelAction(testSubject: string, title?: string) { this.log.debug('expectExistsPanelAction', testSubject); - await this.openContextMenu(); - if (await this.testSubjects.exists(CLONE_PANEL_DATA_TEST_SUBJ)) return; - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); + + const panelWrapper = title ? await this.getPanelHeading(title) : undefined; + await this.openContextMenu(panelWrapper); + + if (!(await this.testSubjects.exists(testSubject))) { + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + } + await this.testSubjects.existOrFail(testSubject); } - await this.testSubjects.existOrFail(CLONE_PANEL_DATA_TEST_SUBJ); - await this.toggleContextMenu(); + await this.toggleContextMenu(panelWrapper); } - async expectMissingPanelAction(testSubject: string) { - this.log.debug('expectMissingPanelAction', testSubject); - await this.openContextMenu(); - await this.testSubjects.missingOrFail(testSubject); - if (await this.hasContextMenuMoreItem()) { - await this.clickContextMenuMoreItem(); - await this.testSubjects.missingOrFail(testSubject); - } - await this.toggleContextMenu(); + async expectExistsRemovePanelAction() { + this.log.debug('expectExistsRemovePanelAction'); + await this.expectExistsPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsEditPanelAction() { + async expectExistsEditPanelAction(title?: string) { this.log.debug('expectExistsEditPanelAction'); - await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); + await this.expectExistsPanelAction(EDIT_PANEL_DATA_TEST_SUBJ, title); } async expectExistsReplacePanelAction() { @@ -253,6 +246,22 @@ export class DashboardPanelActionsService extends FtrService { await this.expectExistsPanelAction(CLONE_PANEL_DATA_TEST_SUBJ); } + async expectExistsToggleExpandAction() { + this.log.debug('expectExistsToggleExpandAction'); + await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); + } + + async expectMissingPanelAction(testSubject: string) { + this.log.debug('expectMissingPanelAction', testSubject); + await this.openContextMenu(); + await this.testSubjects.missingOrFail(testSubject); + if (await this.hasContextMenuMoreItem()) { + await this.clickContextMenuMoreItem(); + await this.testSubjects.missingOrFail(testSubject); + } + await this.toggleContextMenu(); + } + async expectMissingEditPanelAction() { this.log.debug('expectMissingEditPanelAction'); await this.expectMissingPanelAction(EDIT_PANEL_DATA_TEST_SUBJ); @@ -273,11 +282,6 @@ export class DashboardPanelActionsService extends FtrService { await this.expectMissingPanelAction(REMOVE_PANEL_DATA_TEST_SUBJ); } - async expectExistsToggleExpandAction() { - this.log.debug('expectExistsToggleExpandAction'); - await this.expectExistsPanelAction(TOGGLE_EXPAND_PANEL_DATA_TEST_SUBJ); - } - async getPanelHeading(title: string) { return await this.testSubjects.find(`embeddablePanelHeading-${title.replace(/\s/g, '')}`); } diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 1d046c7c18218..99f8c6ffedefc 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -19,5 +19,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./_async_dashboard')); loadTestFile(require.resolve('./dashboard_lens_by_value')); loadTestFile(require.resolve('./dashboard_maps_by_value')); + + loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); }); } diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson new file mode 100644 index 0000000000000..cdf6e94537ae6 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/lens_dashboard_migration_test_7_12_1.ndjson @@ -0,0 +1,7 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare"},"coreMigrationVersion":"7.12.2","id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"index-pattern":"7.11.0"},"references":[],"type":"index-pattern","updated_at":"2021-06-17T22:28:02.495Z","version":"WzEyLDJd"} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"hidePanelTitles\":false}","panelsJSON":"[]","timeRestore":false,"title":"Blank Destination Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[],"type":"dashboard","updated_at":"2021-06-17T22:43:39.414Z","version":"WzI1MiwyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"8faa1a43-2c03-4277-b19b-575da8b59561":{"columnOrder":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe"],"columns":{"20d61a13-4000-4df2-9d83-d9ec0c87b32a":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","type":"column"},"orderDirection":"desc","otherBucket":true,"size":20},"scale":"ordinal","sourceField":"speaker"},"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe":{"dataType":"number","isBucketed":false,"label":"Count of records","operationType":"count","scale":"ratio","sourceField":"Records"}},"incompleteColumns":{}}}}},"filters":[{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-0","key":"play_name","negate":false,"params":{"query":"Hamlet"},"type":"phrase"},"query":{"match_phrase":{"play_name":"Hamlet"}}},{"$state":{"store":"appState"},"meta":{"alias":null,"disabled":false,"indexRefName":"filter-index-pattern-1","key":"speaker","negate":true,"params":{"query":"HAMLET"},"type":"phrase"},"query":{"match_phrase":{"speaker":"HAMLET"}}}],"query":{"language":"kuery","query":""},"visualization":{"layers":[{"categoryDisplay":"default","groups":["20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a","20d61a13-4000-4df2-9d83-d9ec0c87b32a"],"layerId":"8faa1a43-2c03-4277-b19b-575da8b59561","legendDisplay":"default","metric":"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe","nestedLegend":false,"numberDisplay":"percent"}],"shape":"donut"}},"title":"Lens by Reference With Various Filters","visualizationType":"lnsPie"},"coreMigrationVersion":"7.12.2","id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:31:24.138Z","version":"WzgzLDJd"} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"b349d4ba-df44-415f-b0be-999b12f52213":{"columnOrder":["73bc446b-31e8-47a1-b7a1-9549bc81570a","45d911a5-6178-4d9a-a8b4-702a8377c859","89abee74-0f49-4e13-b0e1-2698af72c6f6"],"columns":{"45d911a5-6178-4d9a-a8b4-702a8377c859":{"dataType":"string","isBucketed":true,"label":"Top values of speaker","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"89abee74-0f49-4e13-b0e1-2698af72c6f6","type":"column"},"orderDirection":"desc","otherBucket":true,"size":3},"scale":"ordinal","sourceField":"speaker"},"73bc446b-31e8-47a1-b7a1-9549bc81570a":{"dataType":"string","isBucketed":true,"label":"Top values of play_name","operationType":"terms","params":{"missingBucket":false,"orderBy":{"type":"alphabetical"},"orderDirection":"asc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"play_name"},"89abee74-0f49-4e13-b0e1-2698af72c6f6":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["89abee74-0f49-4e13-b0e1-2698af72c6f6"],"layerId":"b349d4ba-df44-415f-b0be-999b12f52213","position":"top","seriesType":"bar_stacked","showGridlines":false,"splitAccessor":"45d911a5-6178-4d9a-a8b4-702a8377c859","xAccessor":"73bc446b-31e8-47a1-b7a1-9549bc81570a"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Average","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:33:28.820Z","version":"WzEyMywyXQ=="} +{"attributes":{"state":{"datasourceStates":{"indexpattern":{"layers":{"7d197461-9572-4437-b565-f3d7ec731753":{"columnOrder":["de0485bc-e55f-45d1-bf14-2a252ff718d0","a8fced94-076e-44ac-9e94-c0e3847e51b5"],"columns":{"a8fced94-076e-44ac-9e94-c0e3847e51b5":{"dataType":"number","isBucketed":false,"label":"Average of speech_number","operationType":"avg","scale":"ratio","sourceField":"speech_number"},"de0485bc-e55f-45d1-bf14-2a252ff718d0":{"dataType":"string","isBucketed":true,"label":"Top values of type.keyword","operationType":"terms","params":{"missingBucket":false,"orderBy":{"columnId":"a8fced94-076e-44ac-9e94-c0e3847e51b5","type":"column"},"orderDirection":"desc","otherBucket":true,"size":5},"scale":"ordinal","sourceField":"type.keyword"}},"incompleteColumns":{}}}}},"filters":[],"query":{"language":"kuery","query":""},"visualization":{"axisTitlesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"fittingFunction":"None","gridlinesVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"layers":[{"accessors":["a8fced94-076e-44ac-9e94-c0e3847e51b5"],"layerId":"7d197461-9572-4437-b565-f3d7ec731753","seriesType":"bar_stacked","xAccessor":"de0485bc-e55f-45d1-bf14-2a252ff718d0"}],"legend":{"isVisible":true,"position":"right"},"preferredSeriesType":"bar_stacked","tickLabelsVisibilitySettings":{"x":true,"yLeft":true,"yRight":true},"valueLabels":"hide"}},"title":"[7.12.1] Lens By Reference with Drilldown","visualizationType":"lnsXY"},"coreMigrationVersion":"7.12.2","id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"lens":"7.12.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"}],"type":"lens","updated_at":"2021-06-17T22:44:14.911Z","version":"WzI2OSwyXQ=="} +{"attributes":{"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"filter\":[]}"},"optionsJSON":"{\"hidePanelTitles\":false,\"useMargins\":true}","panelsJSON":"[{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":9,\"h\":15,\"i\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\"},\"panelIndex\":\"64bf6149-4bba-423e-91b4-9ff160f520e0\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsPie\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"8faa1a43-2c03-4277-b19b-575da8b59561\":{\"columns\":{\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":20,\"orderBy\":{\"type\":\"column\",\"columnId\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"shape\":\"donut\",\"layers\":[{\"layerId\":\"8faa1a43-2c03-4277-b19b-575da8b59561\",\"groups\":[\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\",\"20d61a13-4000-4df2-9d83-d9ec0c87b32a\"],\"metric\":\"6f1df118-ab3f-4f7a-b5ea-c38e0c11aefe\",\"numberDisplay\":\"percent\",\"categoryDisplay\":\"default\",\"legendDisplay\":\"default\",\"nestedLegend\":false}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[{\"meta\":{\"alias\":null,\"negate\":false,\"disabled\":false,\"type\":\"phrase\",\"key\":\"play_name\",\"params\":{\"query\":\"Hamlet\"},\"indexRefName\":\"filter-index-pattern-0\"},\"query\":{\"match_phrase\":{\"play_name\":\"Hamlet\"}},\"$state\":{\"store\":\"appState\"}},{\"meta\":{\"alias\":null,\"negate\":true,\"disabled\":false,\"type\":\"phrase\",\"key\":\"speaker\",\"params\":{\"query\":\"HAMLET\"},\"indexRefName\":\"filter-index-pattern-1\"},\"query\":{\"match_phrase\":{\"speaker\":\"HAMLET\"}},\"$state\":{\"store\":\"appState\"}}]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561\"},{\"name\":\"filter-index-pattern-0\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"},{\"name\":\"filter-index-pattern-1\",\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Value With Various Filters\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":9,\"y\":0,\"w\":24,\"h\":15,\"i\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\"},\"panelIndex\":\"6ecd96ef-70cc-4d80-a5e5-a2d5b43a2236\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"b349d4ba-df44-415f-b0be-999b12f52213\":{\"columns\":{\"73bc446b-31e8-47a1-b7a1-9549bc81570a\":{\"label\":\"Top values of play_name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"play_name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"alphabetical\"},\"orderDirection\":\"asc\",\"otherBucket\":true,\"missingBucket\":false}},\"89abee74-0f49-4e13-b0e1-2698af72c6f6\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"},\"45d911a5-6178-4d9a-a8b4-702a8377c859\":{\"label\":\"Top values of speaker\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"speaker\",\"isBucketed\":true,\"params\":{\"size\":3,\"orderBy\":{\"type\":\"column\",\"columnId\":\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}}},\"columnOrder\":[\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"45d911a5-6178-4d9a-a8b4-702a8377c859\",\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"b349d4ba-df44-415f-b0be-999b12f52213\",\"accessors\":[\"89abee74-0f49-4e13-b0e1-2698af72c6f6\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"xAccessor\":\"73bc446b-31e8-47a1-b7a1-9549bc81570a\",\"splitAccessor\":\"45d911a5-6178-4d9a-a8b4-702a8377c859\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213\"}]},\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens By Value Lens with Average\"},{\"version\":\"7.12.2\",\"type\":\"lens\",\"gridData\":{\"x\":33,\"y\":0,\"w\":15,\"h\":15,\"i\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\"},\"panelIndex\":\"fed39777-b755-45f8-9efb-1203b4b3d7cf\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"type\":\"lens\",\"visualizationType\":\"lnsXY\",\"state\":{\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"7d197461-9572-4437-b565-f3d7ec731753\":{\"columns\":{\"de0485bc-e55f-45d1-bf14-2a252ff718d0\":{\"label\":\"Top values of type.keyword\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"type.keyword\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"},\"orderDirection\":\"desc\",\"otherBucket\":true,\"missingBucket\":false}},\"a8fced94-076e-44ac-9e94-c0e3847e51b5\":{\"label\":\"Average of speech_number\",\"dataType\":\"number\",\"operationType\":\"avg\",\"sourceField\":\"speech_number\",\"isBucketed\":false,\"scale\":\"ratio\"}},\"columnOrder\":[\"de0485bc-e55f-45d1-bf14-2a252ff718d0\",\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"incompleteColumns\":{}}}}},\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"7d197461-9572-4437-b565-f3d7ec731753\",\"seriesType\":\"bar_stacked\",\"accessors\":[\"a8fced94-076e-44ac-9e94-c0e3847e51b5\"],\"xAccessor\":\"de0485bc-e55f-45d1-bf14-2a252ff718d0\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[]},\"references\":[{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-current-indexpattern\"},{\"type\":\"index-pattern\",\"id\":\"472d30b0-cfbb-11eb-984d-af3b44ed60a7\",\"name\":\"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753\"}]},\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"title\":\"[7.12.1] Lens By Value with Drilldown\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":0,\"y\":15,\"w\":9,\"h\":15,\"i\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\"},\"panelIndex\":\"eb826c7a-0ead-4c8d-99cf-d823388bb91d\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"title\":\"[7.12.1] Lens by Reference With Various Filters\",\"panelRefName\":\"panel_3\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":9,\"y\":15,\"w\":24,\"h\":15,\"i\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\"},\"panelIndex\":\"80a4927b-aa69-4c80-ad38-482c141d0b93\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{}},\"panelRefName\":\"panel_4\"},{\"version\":\"7.12.2\",\"gridData\":{\"x\":33,\"y\":15,\"w\":15,\"h\":15,\"i\":\"57a79145-1314-49f3-87e3-7c494cf55f64\"},\"panelIndex\":\"57a79145-1314-49f3-87e3-7c494cf55f64\",\"embeddableConfig\":{\"hidePanelTitles\":false,\"enhancements\":{\"dynamicActions\":{\"events\":[{\"eventId\":\"8934b2f2-b989-4b8c-8339-c95e387f4372\",\"triggers\":[\"FILTER_TRIGGER\"],\"action\":{\"name\":\"Test Drilldown\",\"config\":{\"useCurrentFilters\":true,\"useCurrentDateRange\":true},\"factoryId\":\"DASHBOARD_TO_DASHBOARD_DRILLDOWN\"}}]}}},\"panelRefName\":\"panel_5\"}]","timeRestore":false,"title":"[7.12.1] Lens By Value Test Dashboard","version":1},"coreMigrationVersion":"7.12.2","id":"60a5cfa0-cfbd-11eb-984d-af3b44ed60a7","migrationVersion":{"dashboard":"7.11.0"},"references":[{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-8faa1a43-2c03-4277-b19b-575da8b59561","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-0","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"filter-index-pattern-1","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-b349d4ba-df44-415f-b0be-999b12f52213","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"472d30b0-cfbb-11eb-984d-af3b44ed60a7","name":"indexpattern-datasource-layer-7d197461-9572-4437-b565-f3d7ec731753","type":"index-pattern"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"759faf20-cfbd-11eb-984d-af3b44ed60a7","name":"drilldown:DASHBOARD_TO_DASHBOARD_DRILLDOWN:8934b2f2-b989-4b8c-8339-c95e387f4372:dashboardId","type":"dashboard"},{"id":"bf5d7860-cfbb-11eb-984d-af3b44ed60a7","name":"panel_3","type":"lens"},{"id":"09ae9610-cfbc-11eb-984d-af3b44ed60a7","name":"panel_4","type":"lens"},{"id":"8ac83fc0-cfbd-11eb-984d-af3b44ed60a7","name":"panel_5","type":"lens"}],"type":"dashboard","updated_at":"2021-06-17T22:44:36.881Z","version":"WzI3NSwyXQ=="} +{"exportedCount":6,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts new file mode 100644 index 0000000000000..78b7ccfe7df08 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/lens_migration_smoke_test.ts @@ -0,0 +1,83 @@ +/* + * 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. + */ + +/* This test is importing saved objects from 7.13.0 to 8.0 and the backported version + * will import from 6.8.x to 7.x.x + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + const dashboardPanelActions = getService('dashboardPanelActions'); + + const PageObjects = getPageObjects(['common', 'settings', 'header', 'savedObjects', 'dashboard']); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await PageObjects.settings.navigateTo(); + await PageObjects.settings.clickKibanaSavedObjects(); + await PageObjects.savedObjects.importFile( + path.join(__dirname, 'exports', 'lens_dashboard_migration_test_7_12_1.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with various Lens panels from 7.12.1', async () => { + // this will catch cases where there is an error in the migrations. + await PageObjects.savedObjects.checkImportSucceeded(); + await PageObjects.savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await PageObjects.common.navigateToApp('dashboard'); + await PageObjects.dashboard.loadSavedDashboard('[7.12.1] Lens By Value Test Dashboard'); + + // dashboard should load properly + await PageObjects.dashboard.expectOnDashboard('[7.12.1] Lens By Value Test Dashboard'); + await PageObjects.dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('should show the edit action for all panels', async () => { + await PageObjects.dashboard.switchToEditMode(); + + // All panels should be editable. This will catch cases where an error does not create an error embeddable. + const panelTitles = await PageObjects.dashboard.getPanelTitles(); + for (const title of panelTitles) { + await dashboardPanelActions.expectExistsEditPanelAction(title); + } + }); + + it('should retain all panel drilldowns from 7.12.1', async () => { + // Both panels configured with drilldowns in 7.12.1 should still have drilldowns. + const totalPanels = await PageObjects.dashboard.getPanelCount(); + let panelsWithDrilldowns = 0; + for (let panelIndex = 0; panelIndex < totalPanels; panelIndex++) { + if ((await PageObjects.dashboard.getPanelDrilldownCount(panelIndex)) === 1) { + panelsWithDrilldowns++; + } + } + expect(panelsWithDrilldowns).to.be(2); + }); + }); +} From 81fe54109ef3f49480282ca708fb343734a4974c Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 23 Jun 2021 10:33:57 -0600 Subject: [PATCH 05/62] Mask timeslider (#102046) * [Maps] timeslider feature mask * create mask * include timeField in documntes pulled from es * fix edge case where timerange is different then timeslice * tslint * fix can_skip_fetch jest tests * simplify source mock * fix functional tests * fix docvalue_fields functional test * review feedback * do not add method to IVectorLayer interface * fix merge artifact * review feedback * review feedback Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../data_request_descriptor_types.ts | 4 + .../blended_vector_layer.ts | 34 ++---- .../layers/heatmap_layer/heatmap_layer.ts | 3 + .../maps/public/classes/layers/layer.tsx | 3 +- .../tiled_vector_layer/tiled_vector_layer.tsx | 7 +- .../classes/layers/vector_layer/utils.tsx | 20 ++- .../layers/vector_layer/vector_layer.tsx | 100 +++++++++++---- .../es_search_source/es_search_source.tsx | 105 ++++++++++++++-- .../mvt_single_layer_vector_source.tsx | 4 + .../maps/public/classes/sources/source.ts | 12 +- .../sources/vector_source/vector_source.tsx | 5 + .../classes/util/can_skip_fetch.test.js | 21 ++++ .../public/classes/util/can_skip_fetch.ts | 8 +- .../classes/util/mb_filter_expressions.ts | 115 ++++++++++++------ .../connected_components/mb_map/index.ts | 2 + .../connected_components/mb_map/mb_map.tsx | 49 +++----- .../maps/documents_source/docvalue_fields.js | 11 +- 17 files changed, 369 insertions(+), 134 deletions(-) diff --git a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts index 07de57d0ac832..d1690ddfff43d 100644 --- a/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/data_request_descriptor_types.ts @@ -66,6 +66,7 @@ export type VectorSourceRequestMeta = MapFilters & { applyGlobalTime: boolean; fieldNames: string[]; geogridPrecision?: number; + timesiceMaskField?: string; sourceQuery?: MapQuery; sourceMeta: VectorSourceSyncMeta; }; @@ -84,6 +85,9 @@ export type VectorStyleRequestMeta = MapFilters & { export type ESSearchSourceResponseMeta = { areResultsTrimmed?: boolean; resultsCount?: number; + // results time extent, either Kibana time range or timeslider time slice + timeExtent?: Timeslice; + isTimeExtentForTimeslice?: boolean; // top hits meta areEntitiesTrimmed?: boolean; diff --git a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts index 6dd454137be7d..9bfa74825c338 100644 --- a/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/blended_vector_layer/blended_vector_layer.ts @@ -22,7 +22,6 @@ import { LAYER_STYLE_TYPE, FIELD_ORIGIN, } from '../../../../common/constants'; -import { isTotalHitsGreaterThan, TotalHits } from '../../../../common/elasticsearch_util'; import { ESGeoGridSource } from '../../sources/es_geo_grid_source/es_geo_grid_source'; import { canSkipSourceUpdate } from '../../util/can_skip_fetch'; import { IESSource } from '../../sources/es_source'; @@ -35,6 +34,7 @@ import { DynamicStylePropertyOptions, StylePropertyOptions, LayerDescriptor, + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStylePropertiesDescriptor, @@ -46,10 +46,6 @@ import { isSearchSourceAbortError } from '../../sources/es_source/es_source'; const ACTIVE_COUNT_DATA_ID = 'ACTIVE_COUNT_DATA_ID'; -interface CountData { - isSyncClustered: boolean; -} - function getAggType( dynamicProperty: IDynamicStyleProperty ): AGG_TYPE.AVG | AGG_TYPE.TERMS { @@ -216,7 +212,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isClustered = false; const countDataRequest = this.getDataRequest(ACTIVE_COUNT_DATA_ID); if (countDataRequest) { - const requestData = countDataRequest.getData() as CountData; + const requestData = countDataRequest.getData() as { isSyncClustered: boolean }; if (requestData && requestData.isSyncClustered) { isClustered = true; } @@ -294,7 +290,7 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { async syncData(syncContext: DataRequestContext) { const dataRequestId = ACTIVE_COUNT_DATA_ID; const requestToken = Symbol(`layer-active-count:${this.getId()}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( syncContext.dataFilters, this.getSource(), this.getCurrentStyle() @@ -305,6 +301,9 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { prevDataRequest: this.getDataRequest(dataRequestId), nextMeta: searchFilters, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); let activeSource; @@ -322,22 +321,11 @@ export class BlendedVectorLayer extends VectorLayer implements IVectorLayer { let isSyncClustered; try { syncContext.startLoading(dataRequestId, requestToken, searchFilters); - const abortController = new AbortController(); - syncContext.registerCancelCallback(requestToken, () => abortController.abort()); - const maxResultWindow = await this._documentSource.getMaxResultWindow(); - const searchSource = await this._documentSource.makeSearchSource(searchFilters, 0); - searchSource.setField('trackTotalHits', maxResultWindow + 1); - const resp = await searchSource.fetch({ - abortSignal: abortController.signal, - sessionId: syncContext.dataFilters.searchSessionId, - legacyHitsTotal: false, - }); - isSyncClustered = isTotalHitsGreaterThan( - (resp.hits.total as unknown) as TotalHits, - maxResultWindow - ); - const countData = { isSyncClustered } as CountData; - syncContext.stopLoading(dataRequestId, requestToken, countData, searchFilters); + isSyncClustered = !(await this._documentSource.canLoadAllDocuments( + searchFilters, + syncContext.registerCancelCallback.bind(null, requestToken) + )); + syncContext.stopLoading(dataRequestId, requestToken, { isSyncClustered }, searchFilters); } catch (error) { if (!(error instanceof DataRequestAbortError) || !isSearchSourceAbortError(error)) { syncContext.onLoadError(dataRequestId, requestToken, error.message); diff --git a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts index 368ff8bebcdd1..d12c8432a4191 100644 --- a/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts +++ b/x-pack/plugins/maps/public/classes/layers/heatmap_layer/heatmap_layer.ts @@ -111,6 +111,9 @@ export class HeatmapLayer extends AbstractLayer { }, syncContext, source: this.getSource(), + getUpdateDueToTimeslice: () => { + return true; + }, }); } catch (error) { if (!(error instanceof DataRequestAbortError)) { diff --git a/x-pack/plugins/maps/public/classes/layers/layer.tsx b/x-pack/plugins/maps/public/classes/layers/layer.tsx index be113ab4cc2c9..ef41c157a2b17 100644 --- a/x-pack/plugins/maps/public/classes/layers/layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/layer.tsx @@ -36,6 +36,7 @@ import { LayerDescriptor, MapExtent, StyleDescriptor, + Timeslice, } from '../../../common/descriptor_types'; import { ImmutableSourceProperty, ISource, SourceEditorArgs } from '../sources/source'; import { DataRequestContext } from '../../actions'; @@ -78,7 +79,7 @@ export interface ILayer { getMbLayerIds(): string[]; ownsMbLayerId(mbLayerId: string): boolean; ownsMbSourceId(mbSourceId: string): boolean; - syncLayerWithMB(mbMap: MbMap): void; + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice): void; getLayerTypeIconName(): string; isInitialDataLoadComplete(): boolean; getIndexPatternIds(): string[]; diff --git a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx index 6dba935ccc87d..2ad6a5ef73c6d 100644 --- a/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/tiled_vector_layer/tiled_vector_layer.tsx @@ -21,6 +21,7 @@ import { VectorLayer, VectorLayerArguments } from '../vector_layer'; import { ITiledSingleLayerVectorSource } from '../../sources/tiled_single_layer_vector_source'; import { DataRequestContext } from '../../../actions'; import { + Timeslice, VectorLayerDescriptor, VectorSourceRequestMeta, } from '../../../../common/descriptor_types'; @@ -66,7 +67,7 @@ export class TiledVectorLayer extends VectorLayer { dataFilters, }: DataRequestContext) { const requestToken: symbol = Symbol(`layer-${this.getId()}-${SOURCE_DATA_REQUEST_ID}`); - const searchFilters: VectorSourceRequestMeta = this._getSearchFilters( + const searchFilters: VectorSourceRequestMeta = await this._getSearchFilters( dataFilters, this.getSource(), this._style as IVectorStyle @@ -84,6 +85,10 @@ export class TiledVectorLayer extends VectorLayer { source: this.getSource(), prevDataRequest, nextMeta: searchFilters, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + // TODO use meta features to determine if tiles already contain features for timeslice. + return true; + }, }); const canSkip = noChangesInSourceState && noChangesInSearchState; if (canSkip) { diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx index d305bb920b2ad..346e59f60af32 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/utils.tsx @@ -13,7 +13,13 @@ import { SOURCE_DATA_REQUEST_ID, VECTOR_SHAPE_TYPE, } from '../../../../common/constants'; -import { MapExtent, MapQuery, VectorSourceRequestMeta } from '../../../../common/descriptor_types'; +import { + DataMeta, + MapExtent, + MapQuery, + Timeslice, + VectorSourceRequestMeta, +} from '../../../../common/descriptor_types'; import { DataRequestContext } from '../../../actions'; import { IVectorSource } from '../../sources/vector_source'; import { DataRequestAbortError } from '../../util/data_request'; @@ -52,6 +58,7 @@ export async function syncVectorSource({ requestMeta, syncContext, source, + getUpdateDueToTimeslice, }: { layerId: string; layerName: string; @@ -59,6 +66,7 @@ export async function syncVectorSource({ requestMeta: VectorSourceRequestMeta; syncContext: DataRequestContext; source: IVectorSource; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise<{ refreshed: boolean; featureCollection: FeatureCollection }> { const { startLoading, @@ -76,6 +84,7 @@ export async function syncVectorSource({ prevDataRequest, nextMeta: requestMeta, extentAware: source.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); if (canSkipFetch) { return { @@ -104,7 +113,14 @@ export async function syncVectorSource({ ) { layerFeatureCollection.features.push(...getCentroidFeatures(layerFeatureCollection)); } - stopLoading(dataRequestId, requestToken, layerFeatureCollection, meta); + const responseMeta: DataMeta = meta ? { ...meta } : {}; + if (requestMeta.applyGlobalTime && (await source.isTimeAware())) { + const timesiceMaskField = await source.getTimesliceMaskFieldName(); + if (timesiceMaskField) { + responseMeta.timesiceMaskField = timesiceMaskField; + } + } + stopLoading(dataRequestId, requestToken, layerFeatureCollection, responseMeta); return { refreshed: true, featureCollection: layerFeatureCollection, diff --git a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx index 8b4d25f4612cc..49a0878ef80b2 100644 --- a/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx +++ b/x-pack/plugins/maps/public/classes/layers/vector_layer/vector_layer.tsx @@ -43,16 +43,19 @@ import { getFillFilterExpression, getLineFilterExpression, getPointFilterExpression, + TimesliceMaskConfig, } from '../../util/mb_filter_expressions'; import { DynamicStylePropertyOptions, MapFilters, MapQuery, + Timeslice, VectorJoinSourceRequestMeta, VectorLayerDescriptor, VectorSourceRequestMeta, VectorStyleRequestMeta, } from '../../../../common/descriptor_types'; +import { ISource } from '../../sources/source'; import { IVectorSource } from '../../sources/vector_source'; import { CustomIconAndTooltipContent, ILayer } from '../layer'; import { InnerJoin } from '../../joins/inner_join'; @@ -347,6 +350,9 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { prevDataRequest, nextMeta: searchFilters, extentAware: false, // join-sources are term-aggs that are spatially unaware (e.g. ESTermSource/TableSource). + getUpdateDueToTimeslice: () => { + return true; + }, }); if (canSkipFetch) { return { @@ -389,17 +395,22 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await Promise.all(joinSyncs); } - _getSearchFilters( + async _getSearchFilters( dataFilters: MapFilters, source: IVectorSource, style: IVectorStyle - ): VectorSourceRequestMeta { + ): Promise { const fieldNames = [ ...source.getFieldNames(), ...style.getSourceFieldNames(), ...this.getValidJoins().map((join) => join.getLeftField().getName()), ]; + const timesliceMaskFieldName = await source.getTimesliceMaskFieldName(); + if (timesliceMaskFieldName) { + fieldNames.push(timesliceMaskFieldName); + } + const sourceQuery = this.getQuery() as MapQuery; return { ...dataFilters, @@ -674,9 +685,12 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { layerId: this.getId(), layerName: await this.getDisplayName(source), prevDataRequest: this.getSourceDataRequest(), - requestMeta: this._getSearchFilters(syncContext.dataFilters, source, style), + requestMeta: await this._getSearchFilters(syncContext.dataFilters, source, style), syncContext, source, + getUpdateDueToTimeslice: (timeslice?: Timeslice) => { + return this._getUpdateDueToTimesliceFromSourceRequestMeta(source, timeslice); + }, }); await this._syncSupportsFeatureEditing({ syncContext, source }); if ( @@ -754,7 +768,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbPointsProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbPointsProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const pointLayerId = this._getMbPointLayerId(); const symbolLayerId = this._getMbSymbolLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -771,7 +789,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { if (symbolLayer) { mbMap.setLayoutProperty(symbolLayerId, 'visibility', 'none'); } - this._setMbCircleProperties(mbMap, mvtSourceLayer); + this._setMbCircleProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } else { markerLayerId = symbolLayerId; textLayerId = symbolLayerId; @@ -779,7 +797,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayoutProperty(pointLayerId, 'visibility', 'none'); mbMap.setLayoutProperty(this._getMbTextLayerId(), 'visibility', 'none'); } - this._setMbSymbolProperties(mbMap, mvtSourceLayer); + this._setMbSymbolProperties(mbMap, mvtSourceLayer, timesliceMaskConfig); } this.syncVisibilityWithMb(mbMap, markerLayerId); @@ -790,7 +808,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { } } - _setMbCircleProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCircleProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const pointLayerId = this._getMbPointLayerId(); const pointLayer = mbMap.getLayer(pointLayerId); @@ -822,7 +844,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(pointLayerId))) { mbMap.setFilter(pointLayerId, filterExpr); mbMap.setFilter(textLayerId, filterExpr); @@ -841,7 +863,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbSymbolProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbSymbolProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const symbolLayerId = this._getMbSymbolLayerId(); const symbolLayer = mbMap.getLayer(symbolLayerId); @@ -858,7 +884,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getPointFilterExpression(this.hasJoins()); + const filterExpr = getPointFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(symbolLayerId))) { mbMap.setFilter(symbolLayerId, filterExpr); } @@ -876,7 +902,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { }); } - _setMbLinePolygonProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbLinePolygonProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const sourceId = this.getId(); const fillLayerId = this._getMbPolygonLayerId(); const lineLayerId = this._getMbLineLayerId(); @@ -940,14 +970,14 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { this.syncVisibilityWithMb(mbMap, fillLayerId); mbMap.setLayerZoomRange(fillLayerId, this.getMinZoom(), this.getMaxZoom()); - const fillFilterExpr = getFillFilterExpression(hasJoins); + const fillFilterExpr = getFillFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(fillFilterExpr, mbMap.getFilter(fillLayerId))) { mbMap.setFilter(fillLayerId, fillFilterExpr); } this.syncVisibilityWithMb(mbMap, lineLayerId); mbMap.setLayerZoomRange(lineLayerId, this.getMinZoom(), this.getMaxZoom()); - const lineFilterExpr = getLineFilterExpression(hasJoins); + const lineFilterExpr = getLineFilterExpression(hasJoins, timesliceMaskConfig); if (!_.isEqual(lineFilterExpr, mbMap.getFilter(lineLayerId))) { mbMap.setFilter(lineLayerId, lineFilterExpr); } @@ -956,7 +986,11 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(tooManyFeaturesLayerId, this.getMinZoom(), this.getMaxZoom()); } - _setMbCentroidProperties(mbMap: MbMap, mvtSourceLayer?: string) { + _setMbCentroidProperties( + mbMap: MbMap, + mvtSourceLayer?: string, + timesliceMaskConfig?: TimesliceMaskConfig + ) { const centroidLayerId = this._getMbCentroidLayerId(); const centroidLayer = mbMap.getLayer(centroidLayerId); if (!centroidLayer) { @@ -971,7 +1005,7 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.addLayer(mbLayer); } - const filterExpr = getCentroidFilterExpression(this.hasJoins()); + const filterExpr = getCentroidFilterExpression(this.hasJoins(), timesliceMaskConfig); if (!_.isEqual(filterExpr, mbMap.getFilter(centroidLayerId))) { mbMap.setFilter(centroidLayerId, filterExpr); } @@ -986,17 +1020,32 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { mbMap.setLayerZoomRange(centroidLayerId, this.getMinZoom(), this.getMaxZoom()); } - _syncStylePropertiesWithMb(mbMap: MbMap) { - this._setMbPointsProperties(mbMap); - this._setMbLinePolygonProperties(mbMap); + _syncStylePropertiesWithMb(mbMap: MbMap, timeslice?: Timeslice) { + const timesliceMaskConfig = this._getTimesliceMaskConfig(timeslice); + this._setMbPointsProperties(mbMap, undefined, timesliceMaskConfig); + this._setMbLinePolygonProperties(mbMap, undefined, timesliceMaskConfig); // centroid layers added after polygon layers to ensure they are on top of polygon layers - this._setMbCentroidProperties(mbMap); + this._setMbCentroidProperties(mbMap, undefined, timesliceMaskConfig); } - syncLayerWithMB(mbMap: MbMap) { + _getTimesliceMaskConfig(timeslice?: Timeslice): TimesliceMaskConfig | undefined { + if (!timeslice || this.hasJoins()) { + return; + } + + const prevMeta = this.getSourceDataRequest()?.getMeta(); + return prevMeta !== undefined && prevMeta.timesiceMaskField !== undefined + ? { + timesiceMaskField: prevMeta.timesiceMaskField, + timeslice, + } + : undefined; + } + + syncLayerWithMB(mbMap: MbMap, timeslice?: Timeslice) { addGeoJsonMbSource(this._getMbSourceId(), this.getMbLayerIds(), mbMap); this._syncFeatureCollectionWithMb(mbMap); - this._syncStylePropertiesWithMb(mbMap); + this._syncStylePropertiesWithMb(mbMap, timeslice); } _getMbPointLayerId() { @@ -1094,6 +1143,15 @@ export class VectorLayer extends AbstractLayer implements IVectorLayer { return await this._source.getLicensedFeatures(); } + _getUpdateDueToTimesliceFromSourceRequestMeta(source: ISource, timeslice?: Timeslice) { + const prevDataRequest = this.getSourceDataRequest(); + const prevMeta = prevDataRequest?.getMeta(); + if (!prevMeta) { + return true; + } + return source.getUpdateDueToTimeslice(prevMeta, timeslice); + } + async addFeature(geometry: Geometry | Position[]) { const layerSource = this.getSource(); await layerSource.addFeature(geometry); diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index a51e291574b70..9f7bd1260ca22 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -12,13 +12,19 @@ import { i18n } from '@kbn/i18n'; import { IFieldType, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { AbstractESSource } from '../es_source'; -import { getHttp, getMapAppConfig, getSearchService } from '../../../kibana_services'; +import { + getHttp, + getMapAppConfig, + getSearchService, + getTimeFilter, +} from '../../../kibana_services'; import { addFieldToDSL, getField, hitsToGeoJson, isTotalHitsGreaterThan, PreIndexedShape, + TotalHits, } from '../../../../common/elasticsearch_util'; // @ts-expect-error import { UpdateSourceEditor } from './update_source_editor'; @@ -41,11 +47,14 @@ import { DEFAULT_FILTER_BY_MAP_BOUNDS } from './constants'; import { ESDocField } from '../../fields/es_doc_field'; import { registerSource } from '../source_registry'; import { + DataMeta, ESSearchSourceDescriptor, + Timeslice, VectorSourceRequestMeta, VectorSourceSyncMeta, } from '../../../../common/descriptor_types'; import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; +import { TimeRange } from '../../../../../../../src/plugins/data/common'; import { ImmutableSourceProperty, SourceEditorArgs } from '../source'; import { IField } from '../../fields/field'; import { GeoJsonWithMeta, SourceTooltipConfig } from '../vector_source'; @@ -59,6 +68,16 @@ import { getDocValueAndSourceFields, ScriptField } from './util/get_docvalue_sou import { ITiledSingleLayerMvtParams } from '../tiled_single_layer_vector_source/tiled_single_layer_vector_source'; import { addFeatureToIndex, getMatchingIndexes } from './util/feature_edit'; +export function timerangeToTimeextent(timerange: TimeRange): Timeslice | undefined { + const timeRangeBounds = getTimeFilter().calculateBounds(timerange); + return timeRangeBounds.min !== undefined && timeRangeBounds.max !== undefined + ? { + from: timeRangeBounds.min.valueOf(), + to: timeRangeBounds.max.valueOf(), + } + : undefined; +} + export const sourceTitle = i18n.translate('xpack.maps.source.esSearchTitle', { defaultMessage: 'Documents', }); @@ -338,7 +357,6 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye async _getSearchHits( layerName: string, searchFilters: VectorSourceRequestMeta, - maxResultWindow: number, registerCancelCallback: (callback: () => void) => void ) { const indexPattern = await this.getIndexPattern(); @@ -350,8 +368,18 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ); const initialSearchContext = { docvalue_fields: docValueFields }; // Request fields in docvalue_fields insted of _source + + // Use Kibana global time extent instead of timeslice extent when all documents for global time extent can be loaded + // to allow for client-side masking of timeslice + const searchFiltersWithoutTimeslice = { ...searchFilters }; + delete searchFiltersWithoutTimeslice.timeslice; + const useSearchFiltersWithoutTimeslice = + searchFilters.timeslice !== undefined && + (await this.canLoadAllDocuments(searchFiltersWithoutTimeslice, registerCancelCallback)); + + const maxResultWindow = await this.getMaxResultWindow(); const searchSource = await this.makeSearchSource( - searchFilters, + useSearchFiltersWithoutTimeslice ? searchFiltersWithoutTimeslice : searchFilters, maxResultWindow, initialSearchContext ); @@ -375,11 +403,17 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye searchSessionId: searchFilters.searchSessionId, }); + const isTimeExtentForTimeslice = + searchFilters.timeslice !== undefined && !useSearchFiltersWithoutTimeslice; return { hits: resp.hits.hits.reverse(), // Reverse hits so top documents by sort are drawn on top meta: { resultsCount: resp.hits.hits.length, areResultsTrimmed: isTotalHitsGreaterThan(resp.hits.total, resp.hits.hits.length), + timeExtent: isTimeExtentForTimeslice + ? searchFilters.timeslice + : timerangeToTimeextent(searchFilters.timeFilters), + isTimeExtentForTimeslice, }, }; } @@ -424,16 +458,9 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye ): Promise { const indexPattern = await this.getIndexPattern(); - const indexSettings = await loadIndexSettings(indexPattern.title); - const { hits, meta } = this._isTopHits() ? await this._getTopHits(layerName, searchFilters, registerCancelCallback) - : await this._getSearchHits( - layerName, - searchFilters, - indexSettings.maxResultWindow, - registerCancelCallback - ); + : await this._getSearchHits(layerName, searchFilters, registerCancelCallback); const unusedMetaFields = indexPattern.metaFields.filter((metaField) => { return !['_id', '_index'].includes(metaField); @@ -743,6 +770,62 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye : urlTemplate, }; } + + async getTimesliceMaskFieldName(): Promise { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return null; + } + + const indexPattern = await this.getIndexPattern(); + return indexPattern.timeFieldName ? indexPattern.timeFieldName : null; + } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + if (this._isTopHits() || this._descriptor.scalingType === SCALING_TYPES.MVT) { + return true; + } + + if ( + prevMeta.timeExtent === undefined || + prevMeta.areResultsTrimmed === undefined || + prevMeta.areResultsTrimmed + ) { + return true; + } + + const isTimeExtentForTimeslice = + prevMeta.isTimeExtentForTimeslice !== undefined ? prevMeta.isTimeExtentForTimeslice : false; + if (!timeslice) { + return isTimeExtentForTimeslice + ? // Previous request only covers timeslice extent. Will need to re-fetch data to cover global time extent + true + : // Previous request covers global time extent. + // No need to re-fetch data since previous request already has data for the entire global time extent. + false; + } + + const isWithin = isTimeExtentForTimeslice + ? timeslice.from >= prevMeta.timeExtent.from && timeslice.to <= prevMeta.timeExtent.to + : true; + return !isWithin; + } + + async canLoadAllDocuments( + searchFilters: VectorSourceRequestMeta, + registerCancelCallback: (callback: () => void) => void + ) { + const abortController = new AbortController(); + registerCancelCallback(() => abortController.abort()); + const maxResultWindow = await this.getMaxResultWindow(); + const searchSource = await this.makeSearchSource(searchFilters, 0); + searchSource.setField('trackTotalHits', maxResultWindow + 1); + const resp = await searchSource.fetch({ + abortSignal: abortController.signal, + sessionId: searchFilters.searchSessionId, + legacyHitsTotal: false, + }); + return !isTotalHitsGreaterThan((resp.hits.total as unknown) as TotalHits, maxResultWindow); + } } registerSource({ diff --git a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx index d58e71db2a9ab..5bf7a2e47cc66 100644 --- a/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/mvt_single_layer_vector_source/mvt_single_layer_vector_source.tsx @@ -228,6 +228,10 @@ export class MVTSingleLayerVectorSource return tooltips; } + async getTimesliceMaskFieldName() { + return null; + } + async supportsFeatureEditing(): Promise { return false; } diff --git a/x-pack/plugins/maps/public/classes/sources/source.ts b/x-pack/plugins/maps/public/classes/sources/source.ts index 7a8fca337fd2e..0ecbde06cf3e2 100644 --- a/x-pack/plugins/maps/public/classes/sources/source.ts +++ b/x-pack/plugins/maps/public/classes/sources/source.ts @@ -13,7 +13,12 @@ import { GeoJsonProperties } from 'geojson'; import { copyPersistentState } from '../../reducers/copy_persistent_state'; import { IField } from '../fields/field'; import { FieldFormatter, LAYER_TYPE, MAX_ZOOM, MIN_ZOOM } from '../../../common/constants'; -import { AbstractSourceDescriptor, Attribution } from '../../../common/descriptor_types'; +import { + AbstractSourceDescriptor, + Attribution, + DataMeta, + Timeslice, +} from '../../../common/descriptor_types'; import { LICENSED_FEATURES } from '../../licensed_features'; import { PreIndexedShape } from '../../../common/elasticsearch_util'; @@ -64,6 +69,7 @@ export interface ISource { getMinZoom(): number; getMaxZoom(): number; getLicensedFeatures(): Promise; + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean; } export class AbstractSource implements ISource { @@ -194,4 +200,8 @@ export class AbstractSource implements ISource { async getLicensedFeatures(): Promise { return []; } + + getUpdateDueToTimeslice(prevMeta: DataMeta, timeslice?: Timeslice): boolean { + return true; + } } diff --git a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx index 1194d571e344b..8f93de705e365 100644 --- a/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/vector_source/vector_source.tsx @@ -66,6 +66,7 @@ export interface IVectorSource extends ISource { getSupportedShapeTypes(): Promise; isBoundsAware(): boolean; getSourceTooltipContent(sourceDataRequest?: DataRequest): SourceTooltipConfig; + getTimesliceMaskFieldName(): Promise; supportsFeatureEditing(): Promise; addFeature(geometry: Geometry | Position[]): Promise; } @@ -156,6 +157,10 @@ export class AbstractVectorSource extends AbstractSource implements IVectorSourc return null; } + async getTimesliceMaskFieldName(): Promise { + return null; + } + async addFeature(geometry: Geometry | Position[]) { throw new Error('Should implement VectorSource#addFeature'); } diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js index c13b2fd441cad..da3cbb9055d43 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.test.js @@ -82,6 +82,9 @@ describe('updateDueToExtent', () => { describe('canSkipSourceUpdate', () => { const SOURCE_DATA_REQUEST_ID = 'foo'; + const getUpdateDueToTimeslice = () => { + return true; + }; describe('isQueryAware', () => { const queryAwareSourceMock = { @@ -136,6 +139,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -156,6 +160,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -176,6 +181,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -193,6 +199,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -224,6 +231,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -244,6 +252,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -264,6 +273,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -281,6 +291,7 @@ describe('canSkipSourceUpdate', () => { prevDataRequest, nextMeta, extentAware: queryAwareSourceMock.isFilterByMapBounds(), + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -327,6 +338,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: false, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -346,6 +358,7 @@ describe('canSkipSourceUpdate', () => { applyGlobalTime: true, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -375,6 +388,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -402,6 +416,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -429,6 +444,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -463,6 +479,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -498,6 +515,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -529,6 +547,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(false); @@ -564,6 +583,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); @@ -599,6 +619,7 @@ describe('canSkipSourceUpdate', () => { }, }, extentAware: false, + getUpdateDueToTimeslice, }); expect(canSkipUpdate).toBe(true); diff --git a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts index 1f2678f40eecd..b6f03ef3d1c63 100644 --- a/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts +++ b/x-pack/plugins/maps/public/classes/util/can_skip_fetch.ts @@ -10,7 +10,7 @@ import turfBboxPolygon from '@turf/bbox-polygon'; import turfBooleanContains from '@turf/boolean-contains'; import { isRefreshOnlyQuery } from './is_refresh_only_query'; import { ISource } from '../sources/source'; -import { DataMeta } from '../../../common/descriptor_types'; +import { DataMeta, Timeslice } from '../../../common/descriptor_types'; import { DataRequest } from './data_request'; const SOURCE_UPDATE_REQUIRED = true; @@ -56,11 +56,13 @@ export async function canSkipSourceUpdate({ prevDataRequest, nextMeta, extentAware, + getUpdateDueToTimeslice, }: { source: ISource; prevDataRequest: DataRequest | undefined; nextMeta: DataMeta; extentAware: boolean; + getUpdateDueToTimeslice: (timeslice?: Timeslice) => boolean; }): Promise { const timeAware = await source.isTimeAware(); const refreshTimerAware = await source.isRefreshTimerAware(); @@ -94,7 +96,9 @@ export async function canSkipSourceUpdate({ updateDueToApplyGlobalTime = prevMeta.applyGlobalTime !== nextMeta.applyGlobalTime; if (nextMeta.applyGlobalTime) { updateDueToTime = !_.isEqual(prevMeta.timeFilters, nextMeta.timeFilters); - updateDueToTimeslice = !_.isEqual(prevMeta.timeslice, nextMeta.timeslice); + if (!_.isEqual(prevMeta.timeslice, nextMeta.timeslice)) { + updateDueToTimeslice = getUpdateDueToTimeslice(nextMeta.timeslice); + } } } diff --git a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts index f5df741759cb3..6a193216c7c1e 100644 --- a/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts +++ b/x-pack/plugins/maps/public/classes/util/mb_filter_expressions.ts @@ -12,67 +12,110 @@ import { KBN_TOO_MANY_FEATURES_PROPERTY, } from '../../../common/constants'; +import { Timeslice } from '../../../common/descriptor_types'; + +export interface TimesliceMaskConfig { + timesiceMaskField: string; + timeslice: Timeslice; +} + export const EXCLUDE_TOO_MANY_FEATURES_BOX = ['!=', ['get', KBN_TOO_MANY_FEATURES_PROPERTY], true]; const EXCLUDE_CENTROID_FEATURES = ['!=', ['get', KBN_IS_CENTROID_FEATURE], true]; -function getFilterExpression(geometryFilter: unknown[], hasJoins: boolean) { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - EXCLUDE_CENTROID_FEATURES, - geometryFilter, - ]; +function getFilterExpression( + filters: unknown[], + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +) { + const allFilters: unknown[] = [...filters]; if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); + allFilters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); } - return ['all', ...filters]; + if (timesliceMaskConfig) { + allFilters.push(['has', timesliceMaskConfig.timesiceMaskField]); + allFilters.push([ + '>=', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.from, + ]); + allFilters.push([ + '<', + ['get', timesliceMaskConfig.timesiceMaskField], + timesliceMaskConfig.timeslice.to, + ]); + } + + return ['all', ...allFilters]; } -export function getFillFilterExpression(hasJoins: boolean): unknown[] { +export function getFillFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getLineFilterExpression(hasJoins: boolean): unknown[] { +export function getLineFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], - ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POLYGON], + ['==', ['geometry-type'], GEO_JSON_TYPE.LINE_STRING], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_LINE_STRING], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getPointFilterExpression(hasJoins: boolean): unknown[] { +export function getPointFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { return getFilterExpression( [ - 'any', - ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], - ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + EXCLUDE_TOO_MANY_FEATURES_BOX, + EXCLUDE_CENTROID_FEATURES, + [ + 'any', + ['==', ['geometry-type'], GEO_JSON_TYPE.POINT], + ['==', ['geometry-type'], GEO_JSON_TYPE.MULTI_POINT], + ], ], - hasJoins + hasJoins, + timesliceMaskConfig ); } -export function getCentroidFilterExpression(hasJoins: boolean): unknown[] { - const filters: unknown[] = [ - EXCLUDE_TOO_MANY_FEATURES_BOX, - ['==', ['get', KBN_IS_CENTROID_FEATURE], true], - ]; - - if (hasJoins) { - filters.push(['==', ['get', FEATURE_VISIBLE_PROPERTY_NAME], true]); - } - - return ['all', ...filters]; +export function getCentroidFilterExpression( + hasJoins: boolean, + timesliceMaskConfig?: TimesliceMaskConfig +): unknown[] { + return getFilterExpression( + [EXCLUDE_TOO_MANY_FEATURES_BOX, ['==', ['get', KBN_IS_CENTROID_FEATURE], true]], + hasJoins, + timesliceMaskConfig + ); } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 4f94cbc7b7458..b9b4b184318f5 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -27,6 +27,7 @@ import { getMapSettings, getScrollZoom, getSpatialFiltersLayer, + getTimeslice, } from '../../selectors/map_selectors'; import { getDrawMode, getIsFullScreen } from '../../selectors/ui_selectors'; import { getInspectorAdapters } from '../../reducers/non_serializable_instances'; @@ -43,6 +44,7 @@ function mapStateToProps(state: MapStoreState) { inspectorAdapters: getInspectorAdapters(state), scrollZoom: getScrollZoom(state), isFullScreen: getIsFullScreen(state), + timeslice: getTimeslice(state), featureModeActive: getDrawMode(state) === DRAW_MODE.DRAW_SHAPES || getDrawMode(state) === DRAW_MODE.DRAW_POINTS, filterModeActive: getDrawMode(state) === DRAW_MODE.DRAW_FILTERS, diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index 96ff7b7dcf882..2ce4e2d98ce5f 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -25,7 +25,7 @@ import { getInitialView } from './get_initial_view'; import { getPreserveDrawingBuffer } from '../../kibana_services'; import { ILayer } from '../../classes/layers/layer'; import { MapSettings } from '../../reducers/map'; -import { Goto, MapCenterAndZoom } from '../../../common/descriptor_types'; +import { Goto, MapCenterAndZoom, Timeslice } from '../../../common/descriptor_types'; import { DECIMAL_DEGREES_PRECISION, KBN_TOO_MANY_FEATURES_IMAGE_ID, @@ -68,13 +68,12 @@ export interface Props { onSingleValueTrigger?: (actionId: string, key: string, value: RawValue) => void; renderTooltipContent?: RenderToolTipContent; setAreTilesLoaded: (layerId: string, areTilesLoaded: boolean) => void; + timeslice?: Timeslice; featureModeActive: boolean; filterModeActive: boolean; } interface State { - prevLayerList: ILayer[] | undefined; - hasSyncedLayerList: boolean; mbMap: MapboxMap | undefined; } @@ -83,38 +82,23 @@ export class MBMap extends Component { private _isMounted: boolean = false; private _containerRef: HTMLDivElement | null = null; private _prevDisableInteractive?: boolean; + private _prevLayerList?: ILayer[]; + private _prevTimeslice?: Timeslice; private _navigationControl = new mapboxgl.NavigationControl({ showCompass: false }); private _tileStatusTracker?: TileStatusTracker; state: State = { - prevLayerList: undefined, - hasSyncedLayerList: false, mbMap: undefined, }; - static getDerivedStateFromProps(nextProps: Props, prevState: State) { - const nextLayerList = nextProps.layerList; - if (nextLayerList !== prevState.prevLayerList) { - return { - prevLayerList: nextLayerList, - hasSyncedLayerList: false, - }; - } - - return null; - } - componentDidMount() { this._initializeMap(); this._isMounted = true; } componentDidUpdate() { - if (this.state.mbMap) { - // do not debounce syncing of map-state - this._syncMbMapWithMapState(); - this._debouncedSync(); - } + this._syncMbMapWithMapState(); // do not debounce syncing of map-state + this._debouncedSync(); } componentWillUnmount() { @@ -134,16 +118,13 @@ export class MBMap extends Component { _debouncedSync = _.debounce(() => { if (this._isMounted && this.props.isMapReady && this.state.mbMap) { - if (!this.state.hasSyncedLayerList) { - this.setState( - { - hasSyncedLayerList: true, - }, - () => { - this._syncMbMapWithLayerList(); - this._syncMbMapWithInspector(); - } - ); + const hasLayerListChanged = this._prevLayerList !== this.props.layerList; // Comparing re-select memoized instance so no deep equals needed + const hasTimesliceChanged = !_.isEqual(this._prevTimeslice, this.props.timeslice); + if (hasLayerListChanged || hasTimesliceChanged) { + this._prevLayerList = this.props.layerList; + this._prevTimeslice = this.props.timeslice; + this._syncMbMapWithLayerList(); + this._syncMbMapWithInspector(); } this.props.spatialFiltersLayer.syncLayerWithMB(this.state.mbMap); this._syncSettings(); @@ -346,7 +327,9 @@ export class MBMap extends Component { this.props.layerList, this.props.spatialFiltersLayer ); - this.props.layerList.forEach((layer) => layer.syncLayerWithMB(this.state.mbMap!)); + this.props.layerList.forEach((layer) => + layer.syncLayerWithMB(this.state.mbMap!, this.props.timeslice) + ); syncLayerOrder(this.state.mbMap, this.props.spatialFiltersLayer, this.props.layerList); }; diff --git a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js index fb0fdcf333cf2..3479f292374d2 100644 --- a/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js +++ b/x-pack/test/functional/apps/maps/documents_source/docvalue_fields.js @@ -21,12 +21,12 @@ export default function ({ getPageObjects, getService }) { await security.testUser.restoreDefaults(); }); - it('should only fetch geo_point field and nothing else when source does not have data driven styling', async () => { + it('should only fetch geo_point field and time field and nothing else when source does not have data driven styling', async () => { await PageObjects.maps.loadSavedMap('document example'); const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['geo.coordinates']); + expect(firstHit.fields).to.only.have.keys(['@timestamp', 'geo.coordinates']); }); it('should only fetch geo_point field and data driven styling fields', async () => { @@ -34,7 +34,12 @@ export default function ({ getPageObjects, getService }) { const { rawResponse: response } = await PageObjects.maps.getResponse(); const firstHit = response.hits.hits[0]; expect(firstHit).to.only.have.keys(['_id', '_index', '_score', 'fields']); - expect(firstHit.fields).to.only.have.keys(['bytes', 'geo.coordinates', 'hour_of_day']); + expect(firstHit.fields).to.only.have.keys([ + '@timestamp', + 'bytes', + 'geo.coordinates', + 'hour_of_day', + ]); }); it('should format date fields as epoch_millis when data driven styling is applied to a date field', async () => { From 3c780a85053d858c9116ba433e9bb44eee901dd8 Mon Sep 17 00:00:00 2001 From: Thomas Neirynck Date: Wed, 23 Jun 2021 18:56:15 +0200 Subject: [PATCH 06/62] [Maps] Use id-values from client-manifest to suggest layers (#102788) --- .../keyword_content.tsx | 8 - .../ems_autosuggest/ems_autosuggest.test.ts | 119 ++++------- .../public/ems_autosuggest/ems_autosuggest.ts | 187 +++++++----------- .../ml/common/constants/embeddable_map.ts | 13 -- .../application/explorer/anomalies_map.tsx | 2 - 5 files changed, 113 insertions(+), 216 deletions(-) delete mode 100644 x-pack/plugins/ml/common/constants/embeddable_map.ts diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx index 22fe8244ef760..1baea4b3f2f7c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/stats_table/components/field_data_expanded_row/keyword_content.tsx @@ -14,13 +14,6 @@ import { DocumentStatsTable } from './document_stats'; import { ExpandedRowContent } from './expanded_row_content'; import { ChoroplethMap } from './choropleth_map'; -const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; - export const KeywordContent: FC = ({ config }) => { const [EMSSuggestion, setEMSSuggestion] = useState(); const { stats, fieldName } = config; @@ -32,7 +25,6 @@ export const KeywordContent: FC = ({ config }) => { const loadEMSTermSuggestions = useCallback(async () => { if (!mapsPlugin) return; const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.isArray(stats?.topValues) ? stats?.topValues.map((value) => value.key) : [], diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts index eff49c1b1242e..cc0ed19db0b40 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.test.ts @@ -6,40 +6,22 @@ */ import { suggestEMSTermJoinConfig } from './ems_autosuggest'; -import { FeatureCollection } from 'geojson'; class MockFileLayer { - private readonly _url: string; private readonly _id: string; private readonly _fields: Array<{ id: string }>; - constructor(url: string, fields: Array<{ id: string }>) { - this._url = url; - this._id = url; + constructor(id: string, fields: Array<{ id: string; alias?: string[]; values?: string[] }>) { + this._id = id; this._fields = fields; } - getFields() { - return this._fields; + getId() { + return this._id; } - getGeoJson() { - if (this._url === 'world_countries') { - return ({ - type: 'FeatureCollection', - features: [ - { properties: { iso2: 'CA', iso3: 'CAN' } }, - { properties: { iso2: 'US', iso3: 'USA' } }, - ], - } as unknown) as FeatureCollection; - } else if (this._url === 'zips') { - return ({ - type: 'FeatureCollection', - features: [{ properties: { zip: '40204' } }, { properties: { zip: '40205' } }], - } as unknown) as FeatureCollection; - } else { - throw new Error(`unrecognized mock url ${this._url}`); - } + getFields() { + return this._fields; } hasId(id: string) { @@ -51,31 +33,31 @@ jest.mock('../util', () => { return { async getEmsFileLayers() { return [ - new MockFileLayer('world_countries', [{ id: 'iso2' }, { id: 'iso3' }]), - new MockFileLayer('zips', [{ id: 'zip' }]), + new MockFileLayer('world_countries', [ + { + id: 'iso2', + alias: ['(geo\\.){0,}country_iso_code$', '(country|countries)'], + values: ['CA', 'US'], + }, + { id: 'iso3', values: ['CAN', 'USA'] }, + { id: 'name', alias: ['(country|countries)'] }, + ]), + new MockFileLayer('usa_zip_codes', [ + { id: 'zip', alias: ['zip'], values: ['40204', '40205'] }, + ]), ]; }, }; }); describe('suggestEMSTermJoinConfig', () => { - test('no info provided', async () => { + test('Should not validate when no info provided', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({}); expect(termJoinConfig).toBe(null); }); - describe('validate common column names', () => { - test('ecs region', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'destination.geo.region_iso_code', - }); - expect(termJoinConfig).toEqual({ - layerId: 'administrative_regions_lvl2', - field: 'region_iso_code', - }); - }); - - test('ecs country', async () => { + describe('With common column names', () => { + test('should match first match', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ sampleValuesColumnName: 'country_iso_code', }); @@ -85,78 +67,61 @@ describe('suggestEMSTermJoinConfig', () => { }); }); - test('country', async () => { + test('When sampleValues are provided, should reject match if no sampleValues for a layer, even though the name matches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'Country_name', - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'name', + sampleValuesColumnName: 'country_iso_code', + sampleValues: ['FO', 'US', 'CA'], }); + expect(termJoinConfig).toEqual(null); }); - test('unknown name', async () => { + test('should reject match if sampleValues not in id-list', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValuesColumnName: 'cntry', + sampleValuesColumnName: 'zip', + sampleValues: ['90201', '40205'], }); expect(termJoinConfig).toEqual(null); }); - }); - describe('validate well known formats', () => { - test('5-digit zip code', async () => { + test('should return first match (regex matches both iso2 and name)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 40204], + sampleValuesColumnName: 'Country_name', }); expect(termJoinConfig).toEqual({ - layerId: 'usa_zip_codes', - field: 'zip', + layerId: 'world_countries', + field: 'iso2', }); }); - test('mismatch', async () => { + test('unknown name', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['90201', 'foobar'], + sampleValuesColumnName: 'cntry', }); expect(termJoinConfig).toEqual(null); }); }); - describe('validate based on EMS data', () => { - test('Should validate with zip codes layer', async () => { + describe('validate well known formats (using id-values in manifest)', () => { + test('Should validate known zipcodes', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', 40205], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['40205', 40204], }); expect(termJoinConfig).toEqual({ - layerId: 'zips', + layerId: 'usa_zip_codes', field: 'zip', }); }); - test('Should not validate with faulty zip codes', async () => { + test('Should not validate unknown zipcode (in this case, 90201)', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['40204', '00000'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 40204], }); expect(termJoinConfig).toEqual(null); }); - test('Should validate against countries', async () => { + test('Should not validate mismatches', async () => { const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'USA', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], - }); - expect(termJoinConfig).toEqual({ - layerId: 'world_countries', - field: 'iso3', - }); - }); - - test('Should not validate against missing countries', async () => { - const termJoinConfig = await suggestEMSTermJoinConfig({ - sampleValues: ['USA', 'BEL', 'CAN'], - emsLayerIds: ['world_countries', 'zips'], + sampleValues: ['90201', 'foobar'], }); expect(termJoinConfig).toEqual(null); }); diff --git a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts index 952e48a71a9dc..66fcbd805f53e 100644 --- a/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts +++ b/x-pack/plugins/maps/public/ems_autosuggest/ems_autosuggest.ts @@ -7,10 +7,8 @@ import type { FileLayer } from '@elastic/ems-client'; import { getEmsFileLayers } from '../util'; -import { emsWorldLayerId, emsRegionLayerId, emsUsaZipLayerId } from '../../common'; export interface SampleValuesConfig { - emsLayerIds?: string[]; sampleValues?: Array; sampleValuesColumnName?: string; } @@ -20,44 +18,16 @@ export interface EMSTermJoinConfig { field: string; } -const wellKnownColumnNames = [ - { - regex: /(geo\.){0,}country_iso_code$/i, // ECS postfix for country - emsConfig: { - layerId: emsWorldLayerId, - field: 'iso2', - }, - }, - { - regex: /(geo\.){0,}region_iso_code$/i, // ECS postfixn for region - emsConfig: { - layerId: emsRegionLayerId, - field: 'region_iso_code', - }, - }, - { - regex: /^country/i, // anything starting with country - emsConfig: { - layerId: emsWorldLayerId, - field: 'name', - }, - }, -]; - -const wellKnownColumnFormats = [ - { - regex: /(^\d{5}$)/i, // 5-digit zipcode - emsConfig: { - layerId: emsUsaZipLayerId, - field: 'zip', - }, - }, -]; - interface UniqueMatch { - config: { layerId: string; field: string }; + config: EMSTermJoinConfig; count: number; } +interface FileLayerFieldShim { + id: string; + values?: string[]; + regex?: string; + alias?: string[]; +} export async function suggestEMSTermJoinConfig( sampleValuesConfig: SampleValuesConfig @@ -65,20 +35,17 @@ export async function suggestEMSTermJoinConfig( const matches: EMSTermJoinConfig[] = []; if (sampleValuesConfig.sampleValuesColumnName) { - matches.push(...suggestByName(sampleValuesConfig.sampleValuesColumnName)); + const matchesBasedOnColumnName = await suggestByName( + sampleValuesConfig.sampleValuesColumnName, + sampleValuesConfig.sampleValues + ); + matches.push(...matchesBasedOnColumnName); } if (sampleValuesConfig.sampleValues && sampleValuesConfig.sampleValues.length) { - if (sampleValuesConfig.emsLayerIds && sampleValuesConfig.emsLayerIds.length) { - matches.push( - ...(await suggestByEMSLayerIds( - sampleValuesConfig.emsLayerIds, - sampleValuesConfig.sampleValues - )) - ); - } else { - matches.push(...suggestByValues(sampleValuesConfig.sampleValues)); - } + // Only looks at id-values in main manifest + const matchesBasedOnIds = await suggestByIdValues(sampleValuesConfig.sampleValues); + matches.push(...matchesBasedOnIds); } const uniqMatches: UniqueMatch[] = matches.reduce((accum: UniqueMatch[], match) => { @@ -105,92 +72,80 @@ export async function suggestEMSTermJoinConfig( return uniqMatches.length ? uniqMatches[0].config : null; } -function suggestByName(columnName: string): EMSTermJoinConfig[] { - const matches = wellKnownColumnNames.filter((wellknown) => { - return columnName.match(wellknown.regex); - }); - - return matches.map((m) => { - return m.emsConfig; - }); -} +async function suggestByName( + columnName: string, + sampleValues?: Array +): Promise { + const fileLayers = await getEmsFileLayers(); -function suggestByValues(values: Array): EMSTermJoinConfig[] { - const matches = wellKnownColumnFormats.filter((wellknown) => { - for (let i = 0; i < values.length; i++) { - const value = values[i].toString(); - if (!value.match(wellknown.regex)) { - return false; + const matches: EMSTermJoinConfig[] = []; + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.alias || !emsField.alias.length) { + return; } - } - return true; - }); - return matches.map((m) => { - return m.emsConfig; + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + emsField.alias.forEach((alias: string) => { + const regex = new RegExp(alias, 'i'); + const nameMatchesAlias = !!columnName.match(regex); + // Check if this violates any known id-values. + + let isMatch: boolean; + if (sampleValues) { + if (emsField.values && emsField.values.length) { + isMatch = nameMatchesAlias && allSamplesMatch(sampleValues, emsField.values); + } else { + // requires validation against sample-values but EMS provides no meta to do so. + isMatch = false; + } + } else { + isMatch = nameMatchesAlias; + } + + if (isMatch) { + matches.push(emsConfig); + } + }); + }); }); -} -function existsInEMS(emsJson: any, emsFieldId: string, sampleValue: string): boolean { - for (let i = 0; i < emsJson.features.length; i++) { - const emsFieldValue = emsJson.features[i].properties[emsFieldId].toString(); - if (emsFieldValue.toString() === sampleValue) { - return true; - } - } - return false; + return matches; } -function matchesEmsField(emsJson: any, emsFieldId: string, sampleValues: Array) { +function allSamplesMatch(sampleValues: Array, ids: string[]) { for (let j = 0; j < sampleValues.length; j++) { const sampleValue = sampleValues[j].toString(); - if (!existsInEMS(emsJson, emsFieldId, sampleValue)) { + if (!ids.includes(sampleValue)) { return false; } } return true; } -async function getMatchesForEMSLayer( - emsLayerId: string, +async function suggestByIdValues( sampleValues: Array ): Promise { + const matches: EMSTermJoinConfig[] = []; const fileLayers: FileLayer[] = await getEmsFileLayers(); - const emsFileLayer: FileLayer | undefined = fileLayers.find((fl: FileLayer) => - fl.hasId(emsLayerId) - ); - - if (!emsFileLayer) { - return []; - } - - const emsFields = emsFileLayer.getFields(); - - try { - const emsJson = await emsFileLayer.getGeoJson(); - const matches: EMSTermJoinConfig[] = []; - for (let f = 0; f < emsFields.length; f++) { - if (matchesEmsField(emsJson, emsFields[f].id, sampleValues)) { - matches.push({ - layerId: emsLayerId, - field: emsFields[f].id, - }); + fileLayers.forEach((fileLayer) => { + const emsFields: FileLayerFieldShim[] = fileLayer.getFields(); + emsFields.forEach((emsField: FileLayerFieldShim) => { + if (!emsField.values || !emsField.values.length) { + return; } - } - return matches; - } catch (e) { - return []; - } -} - -async function suggestByEMSLayerIds( - emsLayerIds: string[], - values: Array -): Promise { - const matches = []; - for (const emsLayerId of emsLayerIds) { - const layerIdMathes = await getMatchesForEMSLayer(emsLayerId, values); - matches.push(...layerIdMathes); - } + const emsConfig = { + layerId: fileLayer.getId(), + field: emsField.id, + }; + if (allSamplesMatch(sampleValues, emsField.values)) { + matches.push(emsConfig); + } + }); + }); return matches; } diff --git a/x-pack/plugins/ml/common/constants/embeddable_map.ts b/x-pack/plugins/ml/common/constants/embeddable_map.ts deleted file mode 100644 index 6cb345bae630e..0000000000000 --- a/x-pack/plugins/ml/common/constants/embeddable_map.ts +++ /dev/null @@ -1,13 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export const COMMON_EMS_LAYER_IDS = [ - 'world_countries', - 'administrative_regions_lvl2', - 'usa_zip_codes', - 'usa_states', -]; diff --git a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx index 73a6a9d64b60e..fe43bd659131f 100644 --- a/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx +++ b/x-pack/plugins/ml/public/application/explorer/anomalies_map.tsx @@ -28,7 +28,6 @@ import { isDefined } from '../../../common/types/guards'; import { MlEmbeddedMapComponent } from '../components/ml_embedded_map'; import { EMSTermJoinConfig } from '../../../../maps/public'; import { AnomaliesTableRecord } from '../../../common/types/anomalies'; -import { COMMON_EMS_LAYER_IDS } from '../../../common/constants/embeddable_map'; const MAX_ENTITY_VALUES = 3; @@ -177,7 +176,6 @@ export const AnomaliesMap: FC = ({ anomalies, jobIds }) => { } const suggestion: EMSTermJoinConfig | null = await mapsPlugin.suggestEMSTermJoinConfig({ - emsLayerIds: COMMON_EMS_LAYER_IDS, sampleValues: Array.from(entityValues), sampleValuesColumnName: entityName || '', }); From 52d5b9d51df349eccf471c6da4f90f32b73bbd1d Mon Sep 17 00:00:00 2001 From: Tyler Smalley Date: Wed, 23 Jun 2021 09:58:51 -0700 Subject: [PATCH 07/62] [docker] Removes setting with hyphen (#103085) This setting is causing an error to be throw as it's being used in an environment variable. Created https://github.com/elastic/kibana/issues/103084 Signed-off-by: Tyler Smalley --- .../docker_generator/resources/base/bin/kibana-docker | 1 - 1 file changed, 1 deletion(-) diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index a9b2dd6aefdda..9ea6e8960e373 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -69,7 +69,6 @@ kibana_vars=( logging.appenders logging.appenders.console logging.appenders.file - logging.appenders.rolling-file logging.dest logging.json logging.loggers From 293dc95f8a54d234877a836191af4fac4e04649c Mon Sep 17 00:00:00 2001 From: Shahzad Date: Wed, 23 Jun 2021 19:07:57 +0200 Subject: [PATCH 08/62] [Exploratory view] Refactor code for multi series (#101157) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../app/RumDashboard/ActionMenu/index.tsx | 5 +- .../PageLoadDistribution/index.tsx | 2 +- .../app/RumDashboard/PageViewsTrend/index.tsx | 2 +- .../components/empty_view.tsx | 8 +- .../components/filter_label.test.tsx | 4 + .../components/filter_label.tsx | 6 +- .../configurations/constants/constants.ts | 22 +- .../configurations/default_configs.ts | 14 +- .../configurations/lens_attributes.test.ts | 303 +++++----- .../configurations/lens_attributes.ts | 530 +++++++++++------- .../mobile/device_distribution_config.ts | 2 +- .../synthetics/data_distribution_config.ts | 4 +- .../test_data/sample_attribute.ts | 28 +- .../exploratory_view/configurations/utils.ts | 40 +- .../exploratory_view.test.tsx | 3 +- .../exploratory_view/exploratory_view.tsx | 105 +++- .../exploratory_view/header/header.test.tsx | 2 +- .../shared/exploratory_view/header/header.tsx | 7 +- .../hooks/use_app_index_pattern.tsx | 35 +- .../hooks/use_lens_attributes.ts | 79 ++- .../hooks/use_series_storage.tsx | 29 +- .../shared/exploratory_view/index.tsx | 4 +- .../shared/exploratory_view/rtl_helpers.tsx | 11 +- .../series_builder/columns/chart_types.tsx | 8 +- .../columns/data_types_col.test.tsx | 11 +- .../series_builder/columns/data_types_col.tsx | 6 +- .../columns/date_picker_col.tsx | 11 +- .../columns/operation_type_select.test.tsx | 8 +- .../columns/report_breakdowns.test.tsx | 6 +- .../columns/report_definition_col.test.tsx | 6 +- .../columns/report_definition_col.tsx | 32 +- .../columns/report_definition_field.tsx | 26 +- .../columns/report_filters.test.tsx | 2 +- .../columns/report_types_col.test.tsx | 11 +- .../columns/report_types_col.tsx | 28 +- .../series_builder/last_updated.tsx | 37 ++ .../series_builder/series_builder.tsx | 285 +++++++--- .../series_date_picker/date_range_picker.tsx | 113 ++++ .../series_date_picker/index.tsx | 2 +- .../series_date_picker.test.tsx | 10 +- .../series_editor/columns/breakdowns.test.tsx | 4 +- .../series_editor/columns/date_picker_col.tsx | 11 +- .../series_editor/columns/filter_expanded.tsx | 8 +- .../columns/filter_value_btn.test.tsx | 4 +- .../columns/filter_value_btn.tsx | 4 +- .../series_editor/columns/remove_series.tsx | 4 +- .../series_editor/columns/series_actions.tsx | 92 ++- .../series_editor/selected_filters.test.tsx | 2 +- .../series_editor/selected_filters.tsx | 5 +- .../series_editor/series_editor.tsx | 128 ++--- .../shared/exploratory_view/types.ts | 7 +- .../utils/stringify_kueries.test.ts | 148 +++++ .../utils/stringify_kueries.ts | 37 ++ .../observability/public/routes/index.tsx | 14 + .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - .../common/charts/ping_histogram.tsx | 2 +- .../common/header/action_menu_content.tsx | 5 +- .../monitor_duration_container.tsx | 2 +- 59 files changed, 1556 insertions(+), 770 deletions(-) create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/last_updated.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/series_date_picker/date_range_picker.tsx create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.test.ts create mode 100644 x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx index 20d930d28599f..63ba7047696ca 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ActionMenu/index.tsx @@ -47,10 +47,11 @@ export function UXActionMenu({ const uxExploratoryViewLink = createExploratoryViewUrl( { - 'ux-series': { + 'ux-series': ({ dataType: 'ux', + isNew: true, time: { from: rangeFrom, to: rangeTo }, - } as SeriesUrl, + } as unknown) as SeriesUrl, }, http?.basePath.get() ); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx index e0486af6cd6ef..5c63cc24b6fdf 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageLoadDistribution/index.tsx @@ -89,7 +89,7 @@ export function PageLoadDistribution() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx index c45637e5d3c82..667d0b5e4b4db 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/PageViewsTrend/index.tsx @@ -64,7 +64,7 @@ export function PageViewsTrend() { { [`${serviceName}-page-views`]: { dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: rangeFrom!, to: rangeTo! }, reportDefinitions: { 'service.name': serviceName as string[], diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx index ea69a371cedae..3566835b1701c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/empty_view.tsx @@ -38,6 +38,12 @@ export function EmptyView({ emptyMessage = SELECTED_DATA_TYPE_FOR_REPORT; } + if (!series) { + emptyMessage = i18n.translate('xpack.observability.expView.seriesEditor.notFound', { + defaultMessage: 'No series found. Please add a series.', + }); + } + return ( {loading && ( @@ -77,7 +83,7 @@ export const EMPTY_LABEL = i18n.translate('xpack.observability.expView.seriesBui export const CHOOSE_REPORT_DEFINITION = i18n.translate( 'xpack.observability.expView.seriesBuilder.emptyReportDefinition', { - defaultMessage: 'Select a report type to create a visualization.', + defaultMessage: 'Select a report definition to create a visualization.', } ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx index af64e74bca89c..fe2953edd36d6 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.test.tsx @@ -29,6 +29,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); @@ -52,6 +53,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -74,6 +76,7 @@ describe('FilterLabel', function () { negate={false} seriesId={'kpi-over-time'} removeFilter={removeFilter} + indexPattern={mockIndexPattern} /> ); @@ -99,6 +102,7 @@ describe('FilterLabel', function () { negate={true} seriesId={'kpi-over-time'} removeFilter={jest.fn()} + indexPattern={mockIndexPattern} /> ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx index 3d4ba6dc08c37..a08e777c5ea71 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/components/filter_label.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { useAppIndexPatternContext } from '../hooks/use_app_index_pattern'; +import { IndexPattern } from '../../../../../../../../src/plugins/data/public'; import { useSeriesFilters } from '../hooks/use_series_filters'; import { FilterValueLabel } from '../../filter_value_label/filter_value_label'; @@ -17,6 +17,7 @@ interface Props { seriesId: string; negate: boolean; definitionFilter?: boolean; + indexPattern: IndexPattern; removeFilter: (field: string, value: string, notVal: boolean) => void; } @@ -26,11 +27,10 @@ export function FilterLabel({ field, value, negate, + indexPattern, removeFilter, definitionFilter, }: Props) { - const { indexPattern } = useAppIndexPatternContext(); - const { invertFilter } = useSeriesFilters({ seriesId }); return indexPattern ? ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts index e119507860c5c..01e8d023ae96b 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/constants/constants.ts @@ -5,8 +5,15 @@ * 2.0. */ -import { ReportViewTypeId } from '../../types'; -import { CLS_FIELD, FCP_FIELD, FID_FIELD, LCP_FIELD, TBT_FIELD } from './elasticsearch_fieldnames'; +import { ReportViewType } from '../../types'; +import { + CLS_FIELD, + FCP_FIELD, + FID_FIELD, + LCP_FIELD, + TBT_FIELD, + TRANSACTION_TIME_TO_FIRST_BYTE, +} from './elasticsearch_fieldnames'; import { AGENT_HOST_LABEL, BROWSER_FAMILY_LABEL, @@ -58,6 +65,7 @@ export const FieldLabels: Record = { [TBT_FIELD]: TBT_LABEL, [FID_FIELD]: FID_LABEL, [CLS_FIELD]: CLS_LABEL, + [TRANSACTION_TIME_TO_FIRST_BYTE]: 'Page load time', 'monitor.id': MONITOR_ID_LABEL, 'monitor.status': MONITOR_STATUS_LABEL, @@ -77,11 +85,11 @@ export const FieldLabels: Record = { 'http.request.method': REQUEST_METHOD, }; -export const DataViewLabels: Record = { - dist: PERF_DIST_LABEL, - kpi: KPI_OVER_TIME_LABEL, - cwv: CORE_WEB_VITALS_LABEL, - mdd: DEVICE_DISTRIBUTION_LABEL, +export const DataViewLabels: Record = { + 'data-distribution': PERF_DIST_LABEL, + 'kpi-over-time': KPI_OVER_TIME_LABEL, + 'core-web-vitals': CORE_WEB_VITALS_LABEL, + 'device-data-distribution': DEVICE_DISTRIBUTION_LABEL, }; export const USE_BREAK_DOWN_COLUMN = 'USE_BREAK_DOWN_COLUMN'; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts index 07342d976cbea..574a9f6a2bc10 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/default_configs.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AppDataType, ReportViewTypes } from '../types'; +import { AppDataType, ReportViewType } from '../types'; import { getRumDistributionConfig } from './rum/data_distribution_config'; import { getSyntheticsDistributionConfig } from './synthetics/data_distribution_config'; import { getSyntheticsKPIConfig } from './synthetics/kpi_over_time_config'; @@ -17,7 +17,7 @@ import { getMobileKPIDistributionConfig } from './mobile/distribution_config'; import { getMobileDeviceDistributionConfig } from './mobile/device_distribution_config'; interface Props { - reportType: keyof typeof ReportViewTypes; + reportType: ReportViewType; indexPattern: IndexPattern; dataType: AppDataType; } @@ -25,23 +25,23 @@ interface Props { export const getDefaultConfigs = ({ reportType, dataType, indexPattern }: Props) => { switch (dataType) { case 'ux': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getRumDistributionConfig({ indexPattern }); } - if (reportType === 'cwv') { + if (reportType === 'core-web-vitals') { return getCoreWebVitalsConfig({ indexPattern }); } return getKPITrendsLensConfig({ indexPattern }); case 'synthetics': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getSyntheticsDistributionConfig({ indexPattern }); } return getSyntheticsKPIConfig({ indexPattern }); case 'mobile': - if (reportType === 'dist') { + if (reportType === 'data-distribution') { return getMobileKPIDistributionConfig({ indexPattern }); } - if (reportType === 'mdd') { + if (reportType === 'device-data-distribution') { return getMobileDeviceDistributionConfig({ indexPattern }); } return getMobileKPIConfig({ indexPattern }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts index 8b21df64a3c91..5189a529bda8f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.test.ts @@ -5,25 +5,37 @@ * 2.0. */ -import { LensAttributes } from './lens_attributes'; +import { LayerConfig, LensAttributes } from './lens_attributes'; import { mockAppIndexPattern, mockIndexPattern } from '../rtl_helpers'; import { getDefaultConfigs } from './default_configs'; import { sampleAttribute } from './test_data/sample_attribute'; -import { LCP_FIELD, SERVICE_NAME, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { LCP_FIELD, USER_AGENT_NAME } from './constants/elasticsearch_fieldnames'; +import { buildExistsFilter, buildPhrasesFilter } from './utils'; describe('Lens Attribute', () => { mockAppIndexPattern(); const reportViewConfig = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); + reportViewConfig.filters?.push(...buildExistsFilter('transaction.type', mockIndexPattern)); + let lnsAttr: LensAttributes; + const layerConfig: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: {}, + time: { from: 'now-15m', to: 'now' }, + }; + beforeEach(() => { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {}); + lnsAttr = new LensAttributes([layerConfig]); }); it('should return expected json', function () { @@ -31,7 +43,7 @@ describe('Lens Attribute', () => { }); it('should return main y axis', function () { - expect(lnsAttr.getMainYAxis()).toEqual({ + expect(lnsAttr.getMainYAxis(layerConfig)).toEqual({ dataType: 'number', isBucketed: false, label: 'Pages loaded', @@ -42,7 +54,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -60,7 +72,7 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with default value', function () { - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -79,11 +91,18 @@ describe('Lens Attribute', () => { }); it('should return expected field type for custom field with passed value', function () { - lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', { - 'performance.metric': [LCP_FIELD], - }); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual( + lnsAttr = new LensAttributes([layerConfig1]); + + expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric', layerConfig1))).toEqual( JSON.stringify({ fieldMeta: { count: 0, @@ -102,7 +121,7 @@ describe('Lens Attribute', () => { }); it('should return expected number range column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -124,7 +143,7 @@ describe('Lens Attribute', () => { }); it('should return expected number operation column', function () { - expect(lnsAttr.getNumberRangeColumn('transaction.duration.us')).toEqual({ + expect(lnsAttr.getNumberRangeColumn('transaction.duration.us', reportViewConfig)).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -160,7 +179,7 @@ describe('Lens Attribute', () => { }); it('should return main x axis', function () { - expect(lnsAttr.getXAxis()).toEqual({ + expect(lnsAttr.getXAxis(layerConfig, 'layer0')).toEqual({ dataType: 'number', isBucketed: true, label: 'Page load time', @@ -182,38 +201,45 @@ describe('Lens Attribute', () => { }); it('should return first layer', function () { - expect(lnsAttr.getLayer()).toEqual({ - columnOrder: ['x-axis-column', 'y-axis-column'], - columns: { - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [ - { - from: 0, - label: '', - to: 1000, - }, - ], - type: 'histogram', + expect(lnsAttr.getLayers()).toEqual({ + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], + columns: { + 'x-axis-column-layer0': { + dataType: 'number', + isBucketed: true, + label: 'Page load time', + operationType: 'range', + params: { + maxBars: 'auto', + ranges: [ + { + from: 0, + label: '', + to: 1000, + }, + ], + type: 'histogram', + }, + scale: 'interval', + sourceField: 'transaction.duration.us', + }, + 'y-axis-column-layer0': { + dataType: 'number', + isBucketed: false, + label: 'Pages loaded', + operationType: 'count', + scale: 'ratio', + sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', }, + incompleteColumns: {}, }, - incompleteColumns: {}, }); }); @@ -225,12 +251,12 @@ describe('Lens Attribute', () => { gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ], legend: { isVisible: true, position: 'right' }, @@ -240,108 +266,52 @@ describe('Lens Attribute', () => { }); }); - describe('ParseFilters function', function () { - it('should parse default filters', function () { - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ]); - }); - - it('should parse default and ui filters', function () { - lnsAttr = new LensAttributes( - mockIndexPattern, - reportViewConfig, - 'line', - [ - { field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] }, - { field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] }, - ], - 'count', - {} - ); + describe('Layer breakdowns', function () { + it('should return breakdown column', function () { + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + breakdown: USER_AGENT_NAME, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.parseFilters()).toEqual([ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - { - meta: { - index: 'apm-*', - key: 'service.name', - params: ['elastic-co', 'kibana-front'], - type: 'phrases', - value: 'elastic-co, kibana-front', - }, - query: { - bool: { - minimum_should_match: 1, - should: [ - { - match_phrase: { - 'service.name': 'elastic-co', - }, - }, - { - match_phrase: { - 'service.name': 'kibana-front', - }, - }, - ], - }, - }, - }, - { - meta: { - index: 'apm-*', - }, - query: { - match_phrase: { - 'user_agent.name': 'Firefox', - }, - }, - }, - { - meta: { - index: 'apm-*', - negate: true, - }, - query: { - match_phrase: { - 'user_agent.name': 'Chrome', - }, - }, - }, - ]); - }); - }); + lnsAttr = new LensAttributes([layerConfig1]); - describe('Layer breakdowns', function () { - it('should add breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); + lnsAttr.getBreakdownColumn({ + sourceField: USER_AGENT_NAME, + layerId: 'layer0', + indexPattern: mockIndexPattern, + }); expect(lnsAttr.visualization.layers).toEqual([ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', palette: undefined, seriesType: 'line', - splitAccessor: 'break-down-column', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], + splitAccessor: 'breakdown-column-layer0', + xAccessor: 'x-axis-column-layer0', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], }, ]); - expect(lnsAttr.layers.layer1).toEqual({ - columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'], + expect(lnsAttr.layers.layer0).toEqual({ + columnOrder: ['x-axis-column-layer0', 'breakdown-column-layer0', 'y-axis-column-layer0'], columns: { - 'break-down-column': { + 'breakdown-column-layer0': { dataType: 'string', isBucketed: true, label: 'Top values of Browser family', operationType: 'terms', params: { missingBucket: false, - orderBy: { columnId: 'y-axis-column', type: 'column' }, + orderBy: { + columnId: 'y-axis-column-layer0', + type: 'column', + }, orderDirection: 'desc', otherBucket: true, size: 10, @@ -349,10 +319,10 @@ describe('Lens Attribute', () => { scale: 'ordinal', sourceField: 'user_agent.name', }, - 'x-axis-column': { + 'x-axis-column-layer0': { dataType: 'number', isBucketed: true, - label: 'Page load time', + label: 'Largest contentful paint', operationType: 'range', params: { maxBars: 'auto', @@ -360,62 +330,47 @@ describe('Lens Attribute', () => { type: 'histogram', }, scale: 'interval', - sourceField: 'transaction.duration.us', + sourceField: 'transaction.marks.agent.largestContentfulPaint', }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, }); }); + }); - it('should remove breakdown column', function () { - lnsAttr.addBreakdown(USER_AGENT_NAME); - - lnsAttr.removeBreakdown(); + describe('Layer Filters', function () { + it('should return expected filters', function () { + reportViewConfig.filters?.push( + ...buildPhrasesFilter('service.name', ['elastic', 'kibana'], mockIndexPattern) + ); - expect(lnsAttr.visualization.layers).toEqual([ - { - accessors: ['y-axis-column'], - layerId: 'layer1', - palette: undefined, - seriesType: 'line', - xAccessor: 'x-axis-column', - yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }], - }, - ]); + const layerConfig1: LayerConfig = { + reportConfig: reportViewConfig, + seriesType: 'line', + operationType: 'count', + indexPattern: mockIndexPattern, + reportDefinitions: { 'performance.metric': [LCP_FIELD] }, + time: { from: 'now-15m', to: 'now' }, + }; - expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']); + const filters = lnsAttr.getLayerFilters(layerConfig1, 2); - expect(lnsAttr.layers.layer1.columns).toEqual({ - 'x-axis-column': { - dataType: 'number', - isBucketed: true, - label: 'Page load time', - operationType: 'range', - params: { - maxBars: 'auto', - ranges: [{ from: 0, label: '', to: 1000 }], - type: 'histogram', - }, - scale: 'interval', - sourceField: 'transaction.duration.us', - }, - 'y-axis-column': { - dataType: 'number', - isBucketed: false, - label: 'Pages loaded', - operationType: 'count', - scale: 'ratio', - sourceField: 'Records', - }, - }); + expect(filters).toEqual( + '@timestamp >= now-15m and @timestamp <= now and transaction.type: page-load and processor.event: transaction and transaction.type : * and service.name: (elastic or kibana)' + ); }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts index 22ad18c663b32..208e8d8ba43c2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/lens_attributes.ts @@ -27,13 +27,12 @@ import { TermsIndexPatternColumn, CardinalityIndexPatternColumn, } from '../../../../../../lens/public'; -import { - buildPhraseFilter, - buildPhrasesFilter, - IndexPattern, -} from '../../../../../../../../src/plugins/data/common'; +import { urlFiltersToKueryString } from '../utils/stringify_kueries'; +import { ExistsFilter, IndexPattern } from '../../../../../../../../src/plugins/data/common'; import { FieldLabels, FILTER_RECORDS, USE_BREAK_DOWN_COLUMN, TERMS_COLUMN } from './constants'; import { ColumnFilter, DataSeries, UrlFilter, URLReportDefinition } from '../types'; +import { PersistableFilter } from '../../../../../../lens/common'; +import { parseAbsoluteDate } from '../series_date_picker/date_range_picker'; function getLayerReferenceName(layerId: string) { return `indexpattern-datasource-layer-${layerId}`; @@ -87,46 +86,50 @@ export const parseCustomFieldName = ( return { fieldName, columnType, columnFilters, timeScale, columnLabel }; }; -export class LensAttributes { +export interface LayerConfig { + filters?: UrlFilter[]; + reportConfig: DataSeries; + breakdown?: string; + seriesType?: SeriesType; + operationType?: OperationType; + reportDefinitions: URLReportDefinition; + time: { to: string; from: string }; indexPattern: IndexPattern; +} + +export class LensAttributes { layers: Record; visualization: XYState; - filters: UrlFilter[]; - seriesType: SeriesType; - reportViewConfig: DataSeries; - reportDefinitions: URLReportDefinition; - breakdownSource?: string; + layerConfigs: LayerConfig[]; - constructor( - indexPattern: IndexPattern, - reportViewConfig: DataSeries, - seriesType?: SeriesType, - filters?: UrlFilter[], - operationType?: OperationType, - reportDefinitions?: URLReportDefinition, - breakdownSource?: string - ) { - this.indexPattern = indexPattern; + constructor(layerConfigs: LayerConfig[]) { this.layers = {}; - this.filters = filters ?? []; - this.reportDefinitions = reportDefinitions ?? {}; - this.breakdownSource = breakdownSource; - - if (operationType) { - reportViewConfig.yAxisColumns.forEach((yAxisColumn) => { - if (typeof yAxisColumn.operationType !== undefined) { - yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; - } - }); - } - this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType; - this.reportViewConfig = reportViewConfig; - this.layers.layer1 = this.getLayer(); + + layerConfigs.forEach(({ reportConfig, operationType }) => { + if (operationType) { + reportConfig.yAxisColumns.forEach((yAxisColumn) => { + if (typeof yAxisColumn.operationType !== undefined) { + yAxisColumn.operationType = operationType as FieldBasedIndexPatternColumn['operationType']; + } + }); + } + }); + + this.layerConfigs = layerConfigs; + this.layers = this.getLayers(); this.visualization = this.getXyState(); } - getBreakdownColumn(sourceField: string): TermsIndexPatternColumn { - const fieldMeta = this.indexPattern.getFieldByName(sourceField); + getBreakdownColumn({ + sourceField, + layerId, + indexPattern, + }: { + sourceField: string; + layerId: string; + indexPattern: IndexPattern; + }): TermsIndexPatternColumn { + const fieldMeta = indexPattern.getFieldByName(sourceField); return { sourceField, @@ -136,8 +139,8 @@ export class LensAttributes { scale: 'ordinal', isBucketed: true, params: { + orderBy: { type: 'column', columnId: `y-axis-column-${layerId}` }, size: 10, - orderBy: { type: 'column', columnId: 'y-axis-column' }, orderDirection: 'desc', otherBucket: true, missingBucket: false, @@ -145,36 +148,14 @@ export class LensAttributes { }; } - addBreakdown(sourceField: string) { - const { xAxisColumn } = this.reportViewConfig; - if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - // do nothing since this will be used a x axis source - return; - } - this.layers.layer1.columns['break-down-column'] = this.getBreakdownColumn(sourceField); - - this.layers.layer1.columnOrder = [ - 'x-axis-column', - 'break-down-column', - 'y-axis-column', - ...Object.keys(this.getChildYAxises()), - ]; - - this.visualization.layers[0].splitAccessor = 'break-down-column'; - } - - removeBreakdown() { - delete this.layers.layer1.columns['break-down-column']; - - this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column']; - - this.visualization.layers[0].splitAccessor = undefined; - } - - getNumberRangeColumn(sourceField: string, label?: string): RangeIndexPatternColumn { + getNumberRangeColumn( + sourceField: string, + reportViewConfig: DataSeries, + label?: string + ): RangeIndexPatternColumn { return { sourceField, - label: this.reportViewConfig.labels[sourceField] ?? label, + label: reportViewConfig.labels[sourceField] ?? label, dataType: 'number', operationType: 'range', isBucketed: true, @@ -187,16 +168,36 @@ export class LensAttributes { }; } - getCardinalityColumn(sourceField: string, label?: string) { - return this.getNumberOperationColumn(sourceField, 'unique_count', label); + getCardinalityColumn({ + sourceField, + label, + reportViewConfig, + }: { + sourceField: string; + label?: string; + reportViewConfig: DataSeries; + }) { + return this.getNumberOperationColumn({ + sourceField, + operationType: 'unique_count', + label, + reportViewConfig, + }); } - getNumberColumn( - sourceField: string, - columnType?: string, - operationType?: string, - label?: string - ) { + getNumberColumn({ + reportViewConfig, + label, + sourceField, + columnType, + operationType, + }: { + sourceField: string; + columnType?: string; + operationType?: string; + label?: string; + reportViewConfig: DataSeries; + }) { if (columnType === 'operation' || operationType) { if ( operationType === 'median' || @@ -204,48 +205,58 @@ export class LensAttributes { operationType === 'sum' || operationType === 'unique_count' ) { - return this.getNumberOperationColumn(sourceField, operationType, label); + return this.getNumberOperationColumn({ + sourceField, + operationType, + label, + reportViewConfig, + }); } if (operationType?.includes('th')) { - return this.getPercentileNumberColumn(sourceField, operationType); + return this.getPercentileNumberColumn(sourceField, operationType, reportViewConfig!); } } - return this.getNumberRangeColumn(sourceField, label); + return this.getNumberRangeColumn(sourceField, reportViewConfig!, label); } - getNumberOperationColumn( - sourceField: string, - operationType: 'average' | 'median' | 'sum' | 'unique_count', - label?: string - ): + getNumberOperationColumn({ + sourceField, + label, + reportViewConfig, + operationType, + }: { + sourceField: string; + operationType: 'average' | 'median' | 'sum' | 'unique_count'; + label?: string; + reportViewConfig: DataSeries; + }): | AvgIndexPatternColumn | MedianIndexPatternColumn | SumIndexPatternColumn | CardinalityIndexPatternColumn { return { ...buildNumberColumn(sourceField), - label: - label || - i18n.translate('xpack.observability.expView.columns.operation.label', { - defaultMessage: '{operationType} of {sourceField}', - values: { - sourceField: this.reportViewConfig.labels[sourceField], - operationType: capitalize(operationType), - }, - }), + label: i18n.translate('xpack.observability.expView.columns.operation.label', { + defaultMessage: '{operationType} of {sourceField}', + values: { + sourceField: label || reportViewConfig.labels[sourceField], + operationType: capitalize(operationType), + }, + }), operationType, }; } getPercentileNumberColumn( sourceField: string, - percentileValue: string + percentileValue: string, + reportViewConfig: DataSeries ): PercentileIndexPatternColumn { return { ...buildNumberColumn(sourceField), label: i18n.translate('xpack.observability.expView.columns.label', { defaultMessage: '{percentileValue} percentile of {sourceField}', - values: { sourceField: this.reportViewConfig.labels[sourceField], percentileValue }, + values: { sourceField: reportViewConfig.labels[sourceField], percentileValue }, }), operationType: 'percentile', params: { percentile: Number(percentileValue.split('th')[0]) }, @@ -268,7 +279,7 @@ export class LensAttributes { return { operationType: 'terms', sourceField, - label: label || 'Top values of ' + sourceField, + label: 'Top values of ' + label || sourceField, dataType: 'string', isBucketed: true, scale: 'ordinal', @@ -283,30 +294,45 @@ export class LensAttributes { }; } - getXAxis() { - const { xAxisColumn } = this.reportViewConfig; + getXAxis(layerConfig: LayerConfig, layerId: string) { + const { xAxisColumn } = layerConfig.reportConfig; if (xAxisColumn?.sourceField === USE_BREAK_DOWN_COLUMN) { - return this.getBreakdownColumn(this.breakdownSource || this.reportViewConfig.breakdowns[0]); + return this.getBreakdownColumn({ + layerId, + indexPattern: layerConfig.indexPattern, + sourceField: layerConfig.breakdown || layerConfig.reportConfig.breakdowns[0], + }); } - return this.getColumnBasedOnType(xAxisColumn.sourceField!, undefined, xAxisColumn.label); + return this.getColumnBasedOnType({ + layerConfig, + label: xAxisColumn.label, + sourceField: xAxisColumn.sourceField!, + }); } - getColumnBasedOnType( - sourceField: string, - operationType?: OperationType, - label?: string, - colIndex?: number - ) { + getColumnBasedOnType({ + sourceField, + label, + layerConfig, + operationType, + colIndex, + }: { + sourceField: string; + operationType?: OperationType; + label?: string; + layerConfig: LayerConfig; + colIndex?: number; + }) { const { fieldMeta, columnType, fieldName, - columnFilters, - timeScale, columnLabel, - } = this.getFieldMeta(sourceField); + timeScale, + columnFilters, + } = this.getFieldMeta(sourceField, layerConfig); const { type: fieldType } = fieldMeta ?? {}; if (columnType === TERMS_COLUMN) { @@ -325,47 +351,76 @@ export class LensAttributes { return this.getDateHistogramColumn(fieldName); } if (fieldType === 'number') { - return this.getNumberColumn(fieldName, columnType, operationType, columnLabel || label); + return this.getNumberColumn({ + sourceField: fieldName, + columnType, + operationType, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } if (operationType === 'unique_count') { - return this.getCardinalityColumn(fieldName, columnLabel || label); + return this.getCardinalityColumn({ + sourceField: fieldName, + label: columnLabel || label, + reportViewConfig: layerConfig.reportConfig, + }); } // FIXME review my approach again return this.getDateHistogramColumn(fieldName); } - getCustomFieldName(sourceField: string) { - return parseCustomFieldName(sourceField, this.reportViewConfig, this.reportDefinitions); + getCustomFieldName({ + sourceField, + layerConfig, + }: { + sourceField: string; + layerConfig: LayerConfig; + }) { + return parseCustomFieldName( + sourceField, + layerConfig.reportConfig, + layerConfig.reportDefinitions + ); } - getFieldMeta(sourceField: string) { + getFieldMeta(sourceField: string, layerConfig: LayerConfig) { const { fieldName, columnType, + columnLabel, columnFilters, timeScale, - columnLabel, - } = this.getCustomFieldName(sourceField); + } = this.getCustomFieldName({ + sourceField, + layerConfig, + }); - const fieldMeta = this.indexPattern.getFieldByName(fieldName); + const fieldMeta = layerConfig.indexPattern.getFieldByName(fieldName); - return { fieldMeta, fieldName, columnType, columnFilters, timeScale, columnLabel }; + return { fieldMeta, fieldName, columnType, columnLabel, columnFilters, timeScale }; } - getMainYAxis() { - const { sourceField, operationType, label } = this.reportViewConfig.yAxisColumns[0]; + getMainYAxis(layerConfig: LayerConfig) { + const { sourceField, operationType, label } = layerConfig.reportConfig.yAxisColumns[0]; if (sourceField === 'Records' || !sourceField) { return this.getRecordsColumn(label); } - return this.getColumnBasedOnType(sourceField!, operationType, label, 0); + return this.getColumnBasedOnType({ + sourceField, + operationType, + label, + layerConfig, + colIndex: 0, + }); } - getChildYAxises() { + getChildYAxises(layerConfig: LayerConfig) { const lensColumns: Record = {}; - const yAxisColumns = this.reportViewConfig.yAxisColumns; + const yAxisColumns = layerConfig.reportConfig.yAxisColumns; // 1 means there is only main y axis if (yAxisColumns.length === 1) { return lensColumns; @@ -373,12 +428,13 @@ export class LensAttributes { for (let i = 1; i < yAxisColumns.length; i++) { const { sourceField, operationType, label } = yAxisColumns[i]; - lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType( - sourceField!, + lensColumns[`y-axis-column-${i}`] = this.getColumnBasedOnType({ + sourceField: sourceField!, operationType, label, - i - ); + layerConfig, + colIndex: i, + }); } return lensColumns; } @@ -396,20 +452,139 @@ export class LensAttributes { scale: 'ratio', sourceField: 'Records', filter: columnFilter, - timeScale, + ...(timeScale ? { timeScale } : {}), } as CountIndexPatternColumn; } - getLayer() { - return { - columnOrder: ['x-axis-column', 'y-axis-column', ...Object.keys(this.getChildYAxises())], - columns: { - 'x-axis-column': this.getXAxis(), - 'y-axis-column': this.getMainYAxis(), - ...this.getChildYAxises(), - }, - incompleteColumns: {}, - }; + getLayerFilters(layerConfig: LayerConfig, totalLayers: number) { + const { + filters, + time: { from, to }, + reportConfig: { filters: layerFilters, reportType }, + } = layerConfig; + let baseFilters = ''; + if (reportType !== 'kpi-over-time' && totalLayers > 1) { + // for kpi over time, we don't need to add time range filters + // since those are essentially plotted along the x-axis + baseFilters += `@timestamp >= ${from} and @timestamp <= ${to}`; + } + + layerFilters?.forEach((filter: PersistableFilter | ExistsFilter) => { + const qFilter = filter as PersistableFilter; + if (qFilter.query?.match_phrase) { + const fieldName = Object.keys(qFilter.query.match_phrase)[0]; + const kql = `${fieldName}: ${qFilter.query.match_phrase[fieldName]}`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + if (qFilter.query?.bool?.should) { + const values: string[] = []; + let fieldName = ''; + qFilter.query?.bool.should.forEach((ft: PersistableFilter['query']['match_phrase']) => { + if (ft.match_phrase) { + fieldName = Object.keys(ft.match_phrase)[0]; + values.push(ft.match_phrase[fieldName]); + } + }); + + const kueryString = `${fieldName}: (${values.join(' or ')})`; + + if (baseFilters.length > 0) { + baseFilters += ` and ${kueryString}`; + } else { + baseFilters += kueryString; + } + } + const existFilter = filter as ExistsFilter; + + if (existFilter.exists) { + const fieldName = existFilter.exists.field; + const kql = `${fieldName} : *`; + if (baseFilters.length > 0) { + baseFilters += ` and ${kql}`; + } else { + baseFilters += kql; + } + } + }); + + const rFilters = urlFiltersToKueryString(filters ?? []); + if (!baseFilters) { + return rFilters; + } + if (!rFilters) { + return baseFilters; + } + return `${rFilters} and ${baseFilters}`; + } + + getTimeShift(mainLayerConfig: LayerConfig, layerConfig: LayerConfig, index: number) { + if (index === 0 || mainLayerConfig.reportConfig.reportType !== 'kpi-over-time') { + return null; + } + + const { + time: { from: mainFrom }, + } = mainLayerConfig; + + const { + time: { from }, + } = layerConfig; + + const inDays = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'days'); + if (inDays > 1) { + return inDays + 'd'; + } + const inHours = parseAbsoluteDate(mainFrom).diff(parseAbsoluteDate(from), 'hours'); + return inHours + 'h'; + } + + getLayers() { + const layers: Record = {}; + const layerConfigs = this.layerConfigs; + + layerConfigs.forEach((layerConfig, index) => { + const { breakdown } = layerConfig; + + const layerId = `layer${index}`; + const columnFilter = this.getLayerFilters(layerConfig, layerConfigs.length); + const timeShift = this.getTimeShift(this.layerConfigs[0], layerConfig, index); + const mainYAxis = this.getMainYAxis(layerConfig); + layers[layerId] = { + columnOrder: [ + `x-axis-column-${layerId}`, + ...(breakdown ? [`breakdown-column-${layerId}`] : []), + `y-axis-column-${layerId}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + columns: { + [`x-axis-column-${layerId}`]: this.getXAxis(layerConfig, layerId), + [`y-axis-column-${layerId}`]: { + ...mainYAxis, + label: timeShift ? `${mainYAxis.label}(${timeShift})` : mainYAxis.label, + filter: { query: columnFilter, language: 'kuery' }, + ...(timeShift ? { timeShift } : {}), + }, + ...(breakdown && breakdown !== USE_BREAK_DOWN_COLUMN + ? // do nothing since this will be used a x axis source + { + [`breakdown-column-${layerId}`]: this.getBreakdownColumn({ + layerId, + sourceField: breakdown, + indexPattern: layerConfig.indexPattern, + }), + } + : {}), + ...this.getChildYAxises(layerConfig), + }, + incompleteColumns: {}, + }; + }); + + return layers; } getXyState(): XYState { @@ -422,71 +597,48 @@ export class LensAttributes { tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true }, gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true }, preferredSeriesType: 'line', - layers: [ - { - accessors: ['y-axis-column', ...Object.keys(this.getChildYAxises())], - layerId: 'layer1', - seriesType: this.seriesType ?? 'line', - palette: this.reportViewConfig.palette, - yConfig: this.reportViewConfig.yConfig || [ - { forAccessor: 'y-axis-column', color: 'green' }, - ], - xAccessor: 'x-axis-column', - }, - ], - ...(this.reportViewConfig.yTitle ? { yTitle: this.reportViewConfig.yTitle } : {}), + layers: this.layerConfigs.map((layerConfig, index) => ({ + accessors: [ + `y-axis-column-layer${index}`, + ...Object.keys(this.getChildYAxises(layerConfig)), + ], + layerId: `layer${index}`, + seriesType: layerConfig.seriesType || layerConfig.reportConfig.defaultSeriesType, + palette: layerConfig.reportConfig.palette, + yConfig: layerConfig.reportConfig.yConfig || [ + { forAccessor: `y-axis-column-layer${index}` }, + ], + xAccessor: `x-axis-column-layer${index}`, + ...(layerConfig.breakdown ? { splitAccessor: `breakdown-column-layer${index}` } : {}), + })), + ...(this.layerConfigs[0].reportConfig.yTitle + ? { yTitle: this.layerConfigs[0].reportConfig.yTitle } + : {}), }; } - parseFilters() { - const defaultFilters = this.reportViewConfig.filters ?? []; - const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : []; - - this.filters.forEach(({ field, values = [], notValues = [] }) => { - const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!; - - if (values?.length > 0) { - if (values?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern); - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern); - parsedFilters.push(filter); - } - } - - if (notValues?.length > 0) { - if (notValues?.length > 1) { - const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern); - multiFilter.meta.negate = true; - parsedFilters.push(multiFilter); - } else { - const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern); - filter.meta.negate = true; - parsedFilters.push(filter); - } - } - }); - - return parsedFilters; - } + parseFilters() {} getJSON(): TypedLensByValueInput['attributes'] { + const uniqueIndexPatternsIds = Array.from( + new Set([...this.layerConfigs.map(({ indexPattern }) => indexPattern.id)]) + ); + return { title: 'Prefilled from exploratory view app', description: '', visualizationType: 'lnsXY', references: [ - { - id: this.indexPattern.id!, + ...uniqueIndexPatternsIds.map((patternId) => ({ + id: patternId!, name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern', - }, - { - id: this.indexPattern.id!, - name: getLayerReferenceName('layer1'), + })), + ...this.layerConfigs.map(({ indexPattern }, index) => ({ + id: indexPattern.id!, + name: getLayerReferenceName(`layer${index}`), type: 'index-pattern', - }, + })), ], state: { datasourceStates: { @@ -496,7 +648,7 @@ export class LensAttributes { }, visualization: this.visualization, query: { query: '', language: 'kuery' }, - filters: this.parseFilters(), + filters: [], }, }; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts index 6f9806660e489..e1cb5a0370fb2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/mobile/device_distribution_config.ts @@ -14,7 +14,7 @@ import { MobileFields } from './mobile_fields'; export function getMobileDeviceDistributionConfig({ indexPattern }: ConfigProps): DataSeries { return { - reportType: 'mobile-device-distribution', + reportType: 'device-data-distribution', defaultSeriesType: 'bar', seriesTypes: ['bar', 'bar_horizontal'], xAxisColumn: { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts index 854f844db047d..b958c0dd71528 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/synthetics/data_distribution_config.ts @@ -10,10 +10,10 @@ import { FieldLabels, RECORDS_FIELD } from '../constants'; import { buildExistsFilter } from '../utils'; import { MONITORS_DURATION_LABEL, PINGS_LABEL } from '../constants/labels'; -export function getSyntheticsDistributionConfig({ indexPattern }: ConfigProps): DataSeries { +export function getSyntheticsDistributionConfig({ series, indexPattern }: ConfigProps): DataSeries { return { reportType: 'data-distribution', - defaultSeriesType: 'line', + defaultSeriesType: series?.seriesType || 'line', seriesTypes: [], xAxisColumn: { sourceField: 'performance.metric', diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts index 9b299e7d70bcc..edf2a42415820 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/test_data/sample_attribute.ts @@ -10,16 +10,16 @@ export const sampleAttribute = { visualizationType: 'lnsXY', references: [ { id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' }, - { id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' }, + { id: 'apm-*', name: 'indexpattern-datasource-layer-layer0', type: 'index-pattern' }, ], state: { datasourceStates: { indexpattern: { layers: { - layer1: { - columnOrder: ['x-axis-column', 'y-axis-column'], + layer0: { + columnOrder: ['x-axis-column-layer0', 'y-axis-column-layer0'], columns: { - 'x-axis-column': { + 'x-axis-column-layer0': { sourceField: 'transaction.duration.us', label: 'Page load time', dataType: 'number', @@ -32,13 +32,18 @@ export const sampleAttribute = { maxBars: 'auto', }, }, - 'y-axis-column': { + 'y-axis-column-layer0': { dataType: 'number', isBucketed: false, label: 'Pages loaded', operationType: 'count', scale: 'ratio', sourceField: 'Records', + filter: { + language: 'kuery', + query: + 'transaction.type: page-load and processor.event: transaction and transaction.type : *', + }, }, }, incompleteColumns: {}, @@ -57,18 +62,15 @@ export const sampleAttribute = { preferredSeriesType: 'line', layers: [ { - accessors: ['y-axis-column'], - layerId: 'layer1', + accessors: ['y-axis-column-layer0'], + layerId: 'layer0', seriesType: 'line', - yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }], - xAccessor: 'x-axis-column', + yConfig: [{ forAccessor: 'y-axis-column-layer0' }], + xAccessor: 'x-axis-column-layer0', }, ], }, query: { query: '', language: 'kuery' }, - filters: [ - { meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } }, - { meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } }, - ], + filters: [], }, }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts index fc60800bc4403..9b1e7ec141ca2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/configurations/utils.ts @@ -5,11 +5,12 @@ * 2.0. */ import rison, { RisonValue } from 'rison-node'; +import type { SeriesUrl, UrlFilter } from '../types'; import type { AllSeries, AllShortSeries } from '../hooks/use_series_storage'; -import type { SeriesUrl } from '../types'; import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns'; -import { esFilters } from '../../../../../../../../src/plugins/data/public'; +import { esFilters, ExistsFilter } from '../../../../../../../../src/plugins/data/public'; import { URL_KEYS } from './constants/url_constants'; +import { PersistableFilter } from '../../../../../../lens/common'; export function convertToShortUrl(series: SeriesUrl) { const { @@ -51,7 +52,7 @@ export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') { } export function buildPhraseFilter(field: string, value: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhraseFilter(fieldMeta, value, indexPattern)]; } @@ -59,7 +60,7 @@ export function buildPhraseFilter(field: string, value: string, indexPattern: II } export function buildPhrasesFilter(field: string, value: string[], indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildPhrasesFilter(fieldMeta, value, indexPattern)]; } @@ -67,9 +68,38 @@ export function buildPhrasesFilter(field: string, value: string[], indexPattern: } export function buildExistsFilter(field: string, indexPattern: IIndexPattern) { - const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field); + const fieldMeta = indexPattern?.fields.find((fieldT) => fieldT.name === field); if (fieldMeta) { return [esFilters.buildExistsFilter(fieldMeta, indexPattern)]; } return []; } + +type FiltersType = PersistableFilter[] | ExistsFilter[]; + +export function urlFilterToPersistedFilter({ + urlFilters, + initFilters, + indexPattern, +}: { + urlFilters: UrlFilter[]; + initFilters: FiltersType; + indexPattern: IIndexPattern; +}) { + const parsedFilters: FiltersType = initFilters ? [...initFilters] : []; + + urlFilters.forEach(({ field, values = [], notValues = [] }) => { + if (values?.length > 0) { + const filter = buildPhrasesFilter(field, values, indexPattern); + parsedFilters.push(...filter); + } + + if (notValues?.length > 0) { + const filter = buildPhrasesFilter(field, notValues, indexPattern)[0]; + filter.meta.negate = true; + parsedFilters.push(filter); + } + }); + + return parsedFilters; +} diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx index 779049601bd6d..989ebf17c2062 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.test.tsx @@ -51,8 +51,9 @@ describe('ExploratoryView', () => { const initSeries = { data: { 'ux-series': { + isNew: true, dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, breakdown: 'user_agent .name', reportDefinitions: { 'service.name': ['elastic-co'] }, time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx index 329ed20ffed3d..ad85ecab968b2 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/exploratory_view.tsx @@ -5,9 +5,10 @@ * 2.0. */ import { i18n } from '@kbn/i18n'; -import React, { useEffect, useRef, useState } from 'react'; +import React, { useEffect, useRef, useState, useCallback } from 'react'; import { EuiPanel, EuiTitle } from '@elastic/eui'; import styled from 'styled-components'; +import { isEmpty } from 'lodash'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ObservabilityPublicPluginsStart } from '../../../plugin'; import { ExploratoryViewHeader } from './header/header'; @@ -17,10 +18,37 @@ import { EmptyView } from './components/empty_view'; import { TypedLensByValueInput } from '../../../../../lens/public'; import { useAppIndexPatternContext } from './hooks/use_app_index_pattern'; import { SeriesBuilder } from './series_builder/series_builder'; +import { SeriesUrl } from './types'; + +export const combineTimeRanges = ( + allSeries: Record, + firstSeries?: SeriesUrl +) => { + let to: string = ''; + let from: string = ''; + if (firstSeries?.reportType === 'kpi-over-time') { + return firstSeries.time; + } + Object.values(allSeries ?? {}).forEach((series) => { + if (series.dataType && series.reportType && !isEmpty(series.reportDefinitions)) { + const seriesTo = new Date(series.time.to); + const seriesFrom = new Date(series.time.from); + if (!to || seriesTo > new Date(to)) { + to = series.time.to; + } + if (!from || seriesFrom < new Date(from)) { + from = series.time.from; + } + } + }); + return { to, from }; +}; export function ExploratoryView({ saveAttributes, + multiSeries, }: { + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { const { @@ -33,6 +61,8 @@ export function ExploratoryView({ const [height, setHeight] = useState('100vh'); const [seriesId, setSeriesId] = useState(''); + const [lastUpdated, setLastUpdated] = useState(); + const [lensAttributes, setLensAttributes] = useState( null ); @@ -47,9 +77,7 @@ export function ExploratoryView({ setSeriesId(firstSeriesId); }, [allSeries, firstSeriesId]); - const lensAttributesT = useLensAttributes({ - seriesId, - }); + const lensAttributesT = useLensAttributes(); const setHeightOffset = () => { if (seriesBuilderRef?.current && wrapperRef.current) { @@ -60,10 +88,12 @@ export function ExploratoryView({ }; useEffect(() => { - if (series?.dataType) { - loadIndexPattern({ dataType: series?.dataType }); - } - }, [series?.dataType, loadIndexPattern]); + Object.values(allSeries).forEach((seriesT) => { + loadIndexPattern({ + dataType: seriesT.dataType, + }); + }); + }, [allSeries, loadIndexPattern]); useEffect(() => { setLensAttributes(lensAttributesT); @@ -72,47 +102,62 @@ export function ExploratoryView({ } // eslint-disable-next-line react-hooks/exhaustive-deps - }, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]); + }, [JSON.stringify(lensAttributesT ?? {})]); useEffect(() => { setHeightOffset(); }); + const timeRange = combineTimeRanges(allSeries, series); + + const onLensLoad = useCallback(() => { + setLastUpdated(Date.now()); + }, []); + + const onBrushEnd = useCallback( + ({ range }: { range: number[] }) => { + if (series?.reportType !== 'data-distribution') { + setSeries(seriesId, { + ...series, + time: { + from: new Date(range[0]).toISOString(), + to: new Date(range[1]).toISOString(), + }, + }); + } else { + notifications?.toasts.add( + i18n.translate('xpack.observability.exploratoryView.noBrusing', { + defaultMessage: 'Zoom by brush selection is only available on time series charts.', + }) + ); + } + }, + [notifications?.toasts, series, seriesId, setSeries] + ); + return ( {lens ? ( <> - {lensAttributes && seriesId && series?.reportType && series?.time ? ( + {lensAttributes && timeRange.to && timeRange.from ? ( { - if (series?.reportType !== 'dist') { - setSeries(seriesId, { - ...series, - time: { - from: new Date(range[0]).toISOString(), - to: new Date(range[1]).toISOString(), - }, - }); - } else { - notifications?.toasts.add( - i18n.translate('xpack.observability.exploratoryView.noBrusing', { - defaultMessage: - 'Zoom by brush selection is only available on time series charts.', - }) - ); - } - }} + onLoad={onLensLoad} + onBrushEnd={onBrushEnd} /> ) : ( )} - + ) : ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx index 1dedc4142f174..8cd8977fcf741 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.test.tsx @@ -26,7 +26,7 @@ describe('ExploratoryViewHeader', function () { data: { 'uptime-pings-histogram': { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx index 3e02207e26272..dbe9cd163451d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/header/header.tsx @@ -13,6 +13,7 @@ import { useKibana } from '../../../../../../../../src/plugins/kibana_react/publ import { DataViewLabels } from '../configurations/constants'; import { ObservabilityAppServices } from '../../../../application/types'; import { useSeriesStorage } from '../hooks/use_series_storage'; +import { combineTimeRanges } from '../exploratory_view'; interface Props { seriesId: string; @@ -24,7 +25,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const { lens } = kServices; - const { getSeries } = useSeriesStorage(); + const { getSeries, allSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -32,6 +33,8 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { const LensSaveModalComponent = lens.SaveModalComponent; + const timeRange = combineTimeRanges(allSeries, series); + return ( <> @@ -63,7 +66,7 @@ export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) { lens.navigateToPrefilledEditor( { id: '', - timeRange: series.time, + timeRange, attributes: lensAttributes, }, true diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx index 4259bb778e511..7a5f12a72b1f0 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_app_index_pattern.tsx @@ -15,7 +15,6 @@ import { getDataHandler } from '../../../../data_handler'; export interface IIndexPatternContext { loading: boolean; - selectedApp: AppDataType; indexPatterns: IndexPatternState; hasAppData: HasAppDataState; loadIndexPattern: (params: { dataType: AppDataType }) => void; @@ -29,10 +28,10 @@ interface ProviderProps { type HasAppDataState = Record; type IndexPatternState = Record; +type LoadingState = Record; export function IndexPatternContextProvider({ children }: ProviderProps) { - const [loading, setLoading] = useState(false); - const [selectedApp, setSelectedApp] = useState(); + const [loading, setLoading] = useState({} as LoadingState); const [indexPatterns, setIndexPatterns] = useState({} as IndexPatternState); const [hasAppData, setHasAppData] = useState({ infra_metrics: null, @@ -49,10 +48,9 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { const loadIndexPattern: IIndexPatternContext['loadIndexPattern'] = useCallback( async ({ dataType }) => { - setSelectedApp(dataType); + if (hasAppData[dataType] === null && !loading[dataType]) { + setLoading((prevState) => ({ ...prevState, [dataType]: true })); - if (hasAppData[dataType] === null) { - setLoading(true); try { let hasDataT = false; let indices: string | undefined = ''; @@ -78,23 +76,22 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { setIndexPatterns((prevState) => ({ ...prevState, [dataType]: indPattern })); } - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } catch (e) { - setLoading(false); + setLoading((prevState) => ({ ...prevState, [dataType]: false })); } } }, - [data, hasAppData] + [data, hasAppData, loading] ); return ( loadingT), }} > {children} @@ -102,19 +99,23 @@ export function IndexPatternContextProvider({ children }: ProviderProps) { ); } -export const useAppIndexPatternContext = () => { - const { selectedApp, loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( +export const useAppIndexPatternContext = (dataType?: AppDataType) => { + const { loading, hasAppData, loadIndexPattern, indexPatterns } = useContext( (IndexPatternContext as unknown) as Context ); + if (dataType && !indexPatterns?.[dataType] && !loading) { + loadIndexPattern({ dataType }); + } + return useMemo(() => { return { hasAppData, - selectedApp, loading, - indexPattern: indexPatterns?.[selectedApp], - hasData: hasAppData?.[selectedApp], + indexPatterns, + indexPattern: dataType ? indexPatterns?.[dataType] : undefined, + hasData: dataType ? hasAppData?.[dataType] : undefined, loadIndexPattern, }; - }, [hasAppData, indexPatterns, loadIndexPattern, loading, selectedApp]); + }, [dataType, hasAppData, indexPatterns, loadIndexPattern, loading]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts index 1c85bc5089b2a..11487afe28e96 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_lens_attributes.ts @@ -8,17 +8,13 @@ import { useMemo } from 'react'; import { isEmpty } from 'lodash'; import { TypedLensByValueInput } from '../../../../../../lens/public'; -import { LensAttributes } from '../configurations/lens_attributes'; +import { LayerConfig, LensAttributes } from '../configurations/lens_attributes'; import { useSeriesStorage } from './use_series_storage'; import { getDefaultConfigs } from '../configurations/default_configs'; import { DataSeries, SeriesUrl, UrlFilter } from '../types'; import { useAppIndexPatternContext } from './use_app_index_pattern'; -interface Props { - seriesId: string; -} - export const getFiltersFromDefs = ( reportDefinitions: SeriesUrl['reportDefinitions'], dataViewConfig: DataSeries @@ -37,54 +33,51 @@ export const getFiltersFromDefs = ( }); }; -export const useLensAttributes = ({ - seriesId, -}: Props): TypedLensByValueInput['attributes'] | null => { - const { getSeries } = useSeriesStorage(); - const series = getSeries(seriesId); - const { breakdown, seriesType, operationType, reportType, dataType, reportDefinitions = {} } = - series ?? {}; +export const useLensAttributes = (): TypedLensByValueInput['attributes'] | null => { + const { allSeriesIds, allSeries } = useSeriesStorage(); - const { indexPattern } = useAppIndexPatternContext(); + const { indexPatterns } = useAppIndexPatternContext(); return useMemo(() => { - if (!indexPattern || !reportType || isEmpty(reportDefinitions)) { + if (isEmpty(indexPatterns) || isEmpty(allSeriesIds)) { return null; } - const dataViewConfig = getDefaultConfigs({ - reportType, - dataType, - indexPattern, - }); + const layerConfigs: LayerConfig[] = []; + + allSeriesIds.forEach((seriesIdT) => { + const seriesT = allSeries[seriesIdT]; + const indexPattern = indexPatterns?.[seriesT?.dataType]; + if (indexPattern && seriesT.reportType && !isEmpty(seriesT.reportDefinitions)) { + const reportViewConfig = getDefaultConfigs({ + reportType: seriesT.reportType, + dataType: seriesT.dataType, + indexPattern, + }); - const filters: UrlFilter[] = (series.filters ?? []).concat( - getFiltersFromDefs(reportDefinitions, dataViewConfig) - ); + const filters: UrlFilter[] = (seriesT.filters ?? []).concat( + getFiltersFromDefs(seriesT.reportDefinitions, reportViewConfig) + ); - const lensAttributes = new LensAttributes( - indexPattern, - dataViewConfig, - seriesType, - filters, - operationType, - reportDefinitions, - breakdown - ); + layerConfigs.push({ + filters, + indexPattern, + reportConfig: reportViewConfig, + breakdown: seriesT.breakdown, + operationType: seriesT.operationType, + seriesType: seriesT.seriesType, + reportDefinitions: seriesT.reportDefinitions ?? {}, + time: seriesT.time, + }); + } + }); - if (breakdown) { - lensAttributes.addBreakdown(breakdown); + if (layerConfigs.length < 1) { + return null; } + const lensAttributes = new LensAttributes(layerConfigs); + return lensAttributes.getJSON(); - }, [ - indexPattern, - reportType, - reportDefinitions, - dataType, - series.filters, - seriesType, - operationType, - breakdown, - ]); + }, [indexPatterns, allSeriesIds, allSeries]); }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx index fac75f910a93f..e9ae43950d47d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/hooks/use_series_storage.tsx @@ -12,7 +12,7 @@ import { } from '../../../../../../../../src/plugins/kibana_utils/public'; import type { AppDataType, - ReportViewTypeId, + ReportViewType, SeriesUrl, UrlFilter, URLReportDefinition, @@ -36,6 +36,16 @@ interface ProviderProps { storage: IKbnUrlStateStorage | ISessionStorageStateStorage; } +function convertAllShortSeries(allShortSeries: AllShortSeries) { + const allSeriesIds = Object.keys(allShortSeries); + const allSeriesN: AllSeries = {}; + allSeriesIds.forEach((seriesKey) => { + allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); + }); + + return allSeriesN; +} + export function UrlStorageContextProvider({ children, storage, @@ -45,15 +55,14 @@ export function UrlStorageContextProvider({ const [allShortSeries, setAllShortSeries] = useState( () => storage.get(allSeriesKey) ?? {} ); - const [allSeries, setAllSeries] = useState({}); + const [allSeries, setAllSeries] = useState(() => + convertAllShortSeries(storage.get(allSeriesKey) ?? {}) + ); const [firstSeriesId, setFirstSeriesId] = useState(''); useEffect(() => { const allSeriesIds = Object.keys(allShortSeries); - const allSeriesN: AllSeries = {}; - allSeriesIds.forEach((seriesKey) => { - allSeriesN[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]); - }); + const allSeriesN: AllSeries = convertAllShortSeries(allShortSeries ?? {}); setAllSeries(allSeriesN); setFirstSeriesId(allSeriesIds?.[0]); @@ -68,8 +77,10 @@ export function UrlStorageContextProvider({ }; const removeSeries = (seriesIdN: string) => { - delete allShortSeries[seriesIdN]; - delete allSeries[seriesIdN]; + setAllShortSeries((prevState) => { + delete prevState[seriesIdN]; + return { ...prevState }; + }); }; const allSeriesIds = Object.keys(allShortSeries); @@ -115,7 +126,7 @@ function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl { interface ShortUrlSeries { [URL_KEYS.OPERATION_TYPE]?: OperationType; - [URL_KEYS.REPORT_TYPE]?: ReportViewTypeId; + [URL_KEYS.REPORT_TYPE]?: ReportViewType; [URL_KEYS.DATA_TYPE]?: AppDataType; [URL_KEYS.SERIES_TYPE]?: SeriesType; [URL_KEYS.BREAK_DOWN]?: string; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx index 3de29b02853e8..e55752ceb62ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/index.tsx @@ -25,9 +25,11 @@ import { TypedLensByValueInput } from '../../../../../lens/public'; export function ExploratoryViewPage({ saveAttributes, + multiSeries = false, useSessionStorage = false, }: { useSessionStorage?: boolean; + multiSeries?: boolean; saveAttributes?: (attr: TypedLensByValueInput['attributes'] | null) => void; }) { useTrackPageview({ app: 'observability-overview', path: 'exploratory-view' }); @@ -59,7 +61,7 @@ export function ExploratoryViewPage({ - + diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx index 8e54ab7629d26..972e3beb4b722 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx @@ -35,8 +35,11 @@ import { getStubIndexPattern } from '../../../../../../../src/plugins/data/publi import indexPatternData from './configurations/test_data/test_index_pattern.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services'; -import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { UrlFilter } from './types'; +import { + IndexPattern, + IndexPatternsContract, +} from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; +import { AppDataType, UrlFilter } from './types'; import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks'; import { ListItem } from '../../../hooks/use_values_list'; @@ -232,11 +235,11 @@ export const mockAppIndexPattern = () => { const loadIndexPattern = jest.fn(); const spy = jest.spyOn(useAppIndexPatternHook, 'useAppIndexPatternContext').mockReturnValue({ indexPattern: mockIndexPattern, - selectedApp: 'ux', hasData: true, loading: false, hasAppData: { ux: true } as any, loadIndexPattern, + indexPatterns: ({ ux: mockIndexPattern } as unknown) as Record, }); return { spy, loadIndexPattern }; }; @@ -260,7 +263,7 @@ function mockSeriesStorageContext({ }) { const mockDataSeries = data || { 'performance-distribution': { - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', breakdown: breakdown || 'user_agent.name', time: { from: 'now-15m', to: 'now' }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx index 9ae8b68bf3e8c..50c2f91e6067d 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/chart_types.tsx @@ -27,18 +27,14 @@ export function SeriesChartTypesSelect({ seriesTypes?: SeriesType[]; defaultChartType: SeriesType; }) { - const { getSeries, setSeries, allSeries } = useSeriesStorage(); + const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); const seriesType = series?.seriesType ?? defaultChartType; const onChange = (value: SeriesType) => { - Object.keys(allSeries).forEach((seriesKey) => { - const seriesN = allSeries[seriesKey]; - - setSeries(seriesKey, { ...seriesN, seriesType: value }); - }); + setSeries(seriesId, { ...series, seriesType: value }); }; return ( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx index e3c1666c533ef..b10702ebded57 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.test.tsx @@ -29,7 +29,14 @@ describe('DataTypesCol', function () { fireEvent.click(screen.getByText(/user experience \(rum\)/i)); expect(setSeries).toHaveBeenCalledTimes(1); - expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux' }); + expect(setSeries).toHaveBeenCalledWith(seriesId, { + dataType: 'ux', + isNew: true, + time: { + from: 'now-15m', + to: 'now', + }, + }); }); it('should set series on change on already selected', function () { @@ -37,7 +44,7 @@ describe('DataTypesCol', function () { data: { [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, }, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx index 985afdf888868..f386f62d9ed73 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/data_types_col.tsx @@ -31,7 +31,11 @@ export function DataTypesCol({ seriesId }: { seriesId: string }) { if (!dataType) { removeSeries(seriesId); } else { - setSeries(seriesId || `${dataType}-series`, { dataType } as any); + setSeries(seriesId || `${dataType}-series`, { + dataType, + isNew: true, + time: series.time, + } as any); } }; diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx index 175fbea9445c1..6be78084ae195 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/date_picker_col.tsx @@ -8,14 +8,23 @@ import React from 'react'; import styled from 'styled-components'; import { SeriesDatePicker } from '../../series_date_picker'; +import { DateRangePicker } from '../../series_date_picker/date_range_picker'; +import { useSeriesStorage } from '../../hooks/use_series_storage'; interface Props { seriesId: string; } export function DatePickerCol({ seriesId }: Props) { + const { firstSeriesId, getSeries } = useSeriesStorage(); + const { reportType } = getSeries(firstSeriesId); + return ( - + {firstSeriesId === seriesId || reportType !== 'kpi-over-time' ? ( + + ) : ( + + )} ); } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx index c262a94f968be..516f04e3812ba 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/operation_type_select.test.tsx @@ -22,7 +22,7 @@ describe('OperationTypeSelect', function () { data: { 'performance-distribution': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -39,7 +39,7 @@ describe('OperationTypeSelect', function () { data: { 'series-id': { dataType: 'ux' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, operationType: 'median' as const, time: { from: 'now-15m', to: 'now' }, }, @@ -53,7 +53,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: 'median', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); @@ -61,7 +61,7 @@ describe('OperationTypeSelect', function () { expect(setSeries).toHaveBeenCalledWith('series-id', { operationType: '95th', dataType: 'ux', - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx index 805186e877d57..203382afc1624 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_breakdowns.test.tsx @@ -15,7 +15,7 @@ import { USER_AGENT_OS } from '../../configurations/constants/elasticsearch_fiel describe('Series Builder ReportBreakdowns', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', dataType: 'ux', indexPattern: mockIndexPattern, }); @@ -45,7 +45,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: USER_AGENT_OS, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); @@ -67,7 +67,7 @@ describe('Series Builder ReportBreakdowns', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { breakdown: undefined, dataType: 'ux', - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-15m', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx index e947961fb4300..2e5c674b9fad8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.test.tsx @@ -22,7 +22,7 @@ describe('Series Builder ReportDefinitionCol', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); @@ -31,7 +31,7 @@ describe('Series Builder ReportDefinitionCol', function () { data: { [seriesId]: { dataType: 'ux' as const, - reportType: 'dist' as const, + reportType: 'data-distribution' as const, time: { from: 'now-30d', to: 'now' }, reportDefinitions: { [SERVICE_NAME]: ['elastic-co'] }, }, @@ -81,7 +81,7 @@ describe('Series Builder ReportDefinitionCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'ux', reportDefinitions: {}, - reportType: 'dist', + reportType: 'data-distribution', time: { from: 'now-30d', to: 'now' }, }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx index 338f5d52c26fa..47962af0d4bc4 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_definition_col.tsx @@ -8,7 +8,6 @@ import React from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule } from '@elastic/eui'; import styled from 'styled-components'; -import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { CustomReportField } from '../custom_report_field'; import { DataSeries, URLReportDefinition } from '../../types'; @@ -36,8 +35,6 @@ export function ReportDefinitionCol({ dataViewSeries: DataSeries; seriesId: string; }) { - const { indexPattern } = useAppIndexPatternContext(); - const { getSeries, setSeries } = useSeriesStorage(); const series = getSeries(seriesId); @@ -69,21 +66,20 @@ export function ReportDefinitionCol({ - {indexPattern && - reportDefinitions.map(({ field, custom, options }) => ( - - {!custom ? ( - - ) : ( - - )} - - ))} + {reportDefinitions.map(({ field, custom, options }) => ( + + {!custom ? ( + + ) : ( + + )} + + ))} {(hasOperationType || columnType === 'operation') && ( { - if (!custom && selectedReportDefinitions?.[fieldT] && fieldT !== field) { + if (!custom && indexPattern && selectedReportDefinitions?.[fieldT] && fieldT !== field) { const values = selectedReportDefinitions?.[fieldT]; const valueFilter = buildPhrasesFilter(fieldT, values, indexPattern)[0]; filtersN.push(valueFilter.query); @@ -64,16 +64,18 @@ export function ReportDefinitionField({ seriesId, field, dataSeries, onChange }: return ( - onChange(field, val)} - filters={queryFilters} - time={series.time} - fullWidth={true} - /> + {indexPattern && ( + onChange(field, val)} + filters={queryFilters} + time={series.time} + fullWidth={true} + /> + )} ); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx index 7ca947fed0bc9..f35639388aac5 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_filters.test.tsx @@ -15,7 +15,7 @@ describe('Series Builder ReportFilters', function () { const seriesId = 'test-series-id'; const dataViewSeries = getDefaultConfigs({ - reportType: 'dist', + reportType: 'data-distribution', indexPattern: mockIndexPattern, dataType: 'ux', }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx index f36d64ca5bbbd..f7cfe06c0d928 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.test.tsx @@ -11,10 +11,9 @@ import { mockAppIndexPattern, render } from '../../rtl_helpers'; import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col'; import { ReportTypes } from '../series_builder'; import { DEFAULT_TIME } from '../../configurations/constants'; -import { NEW_SERIES_KEY } from '../../hooks/use_series_storage'; describe('ReportTypesCol', function () { - const seriesId = 'test-series-id'; + const seriesId = 'performance-distribution'; mockAppIndexPattern(); @@ -40,7 +39,7 @@ describe('ReportTypesCol', function () { breakdown: 'user_agent.name', dataType: 'ux', reportDefinitions: {}, - reportType: 'kpi', + reportType: 'kpi-over-time', time: { from: 'now-15m', to: 'now' }, }); expect(setSeries).toHaveBeenCalledTimes(1); @@ -49,11 +48,12 @@ describe('ReportTypesCol', function () { it('should set selected as filled', function () { const initSeries = { data: { - [NEW_SERIES_KEY]: { + [seriesId]: { dataType: 'synthetics' as const, - reportType: 'kpi' as const, + reportType: 'kpi-over-time' as const, breakdown: 'monitor.status', time: { from: 'now-15m', to: 'now' }, + isNew: true, }, }, }; @@ -74,6 +74,7 @@ describe('ReportTypesCol', function () { expect(setSeries).toHaveBeenCalledWith(seriesId, { dataType: 'synthetics', time: DEFAULT_TIME, + isNew: true, }); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx index 9fff8dae14a47..64c7b48c668b8 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_builder/columns/report_types_col.tsx @@ -7,27 +7,33 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; +import { map } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import styled from 'styled-components'; -import { ReportViewTypeId, SeriesUrl } from '../../types'; +import { ReportViewType, SeriesUrl } from '../../types'; import { useSeriesStorage } from '../../hooks/use_series_storage'; import { DEFAULT_TIME } from '../../configurations/constants'; import { useAppIndexPatternContext } from '../../hooks/use_app_index_pattern'; +import { ReportTypeItem, SELECT_DATA_TYPE } from '../series_builder'; interface Props { seriesId: string; - reportTypes: Array<{ id: ReportViewTypeId; label: string }>; + reportTypes: ReportTypeItem[]; } export function ReportTypesCol({ seriesId, reportTypes }: Props) { - const { setSeries, getSeries } = useSeriesStorage(); + const { setSeries, getSeries, firstSeries, firstSeriesId } = useSeriesStorage(); const { reportType: selectedReportType, ...restSeries } = getSeries(seriesId); - const { loading, hasData, selectedApp } = useAppIndexPatternContext(); + const { loading, hasData } = useAppIndexPatternContext(restSeries.dataType); - if (!loading && !hasData && selectedApp) { + if (!restSeries.dataType) { + return {SELECT_DATA_TYPE}; + } + + if (!loading && !hasData) { return ( firstSeriesId !== seriesId && reportType !== firstSeries.reportType + ), + 'reportType' + ); + return reportTypes?.length > 0 ? ( - {reportTypes.map(({ id: reportType, label }) => ( + {reportTypes.map(({ reportType, label }) => ( - -
+ +
+
+ +
+ + + +
+ - -
+
+
-
- - - -
+ recent 2 + + + + + +
- -
+
+
- - - -
-
- + + + +
+
+ +
-
- + + + + + +

+ Analytics +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-kibana" - iconType="logoKibana" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="kibana" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Analytics" + paddingSize="none" > - - - - - - -

- Analytics -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ dashboard + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Observability +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-observability" - iconType="logoObservability" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="observability" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Observability" + paddingSize="none" > - - - - - - -

- Observability -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ logs + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Security +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-securitySolution" - iconType="logoSecurity" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="securitySolution" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Security" + paddingSize="none" > - - - - - - -

- Security -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ siem + + + + + +
- -
+
+
-
-
- + + + + + + + + + +

+ Management +

+
+
+ + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-management" - iconType="managementApp" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="management" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Management" + paddingSize="none" > - - - - - - -

- Management -

-
-
- - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--withHeading" +
-
- -
-
+ +
+ +
+ + + +
+
+ - -
+
+
-
- - - -
+ monitoring + + + + + +
- -
+
+
-
-
- + + + +
-
- - - -
+ canvas + + + + + +
- - - +
+
+ + +
-
- -
    - - - - Dock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lockOpen" - label="Dock navigation" - onClick={[Function]} - size="xs" - > -
  • - -
  • -
    -
-
-
+ , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lockOpen" + label="Dock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + + +
    + + `; @@ -2770,42 +2706,57 @@ exports[`CollapsibleNav renders the default nav 3`] = ` isOpen={false} onClose={[Function]} > - - -
    -
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="generated-id" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - -
    - -
    -

    - No recently viewed items -

    -
    -
    -
    -
    -
    +

    + No recently viewed items +

    +
    + +
    +
    -
    -
    + + -
    -
    - -
    -
    - + + + +
    +
    + +
    -
    - - + +
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" + onClick={[Function]} + size="xs" + > +
  • + +
  • + + +
    - - -
    - - - - - - - -
    - +
    + + + +
    + + `; diff --git a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap index 6ad1e2d3a1cc6..5aee9ca1b7c08 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/header.test.tsx.snap @@ -4947,42 +4947,57 @@ exports[`Header renders 1`] = ` isOpen={false} onClose={[Function]} > - - -
    -
    +
    + +
    +
    + +
    -
    -
    -
    - - - -
    + data-euiicon-type="home" + /> + + + Home + + + + + +
    -
    -
    - - + +
    +
    + + + + +

    + Recently viewed +

    +
    +
    + + } + className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" data-test-subj="collapsibleNavGroup-recentlyViewed" + id="mockId" initialIsOpen={true} - isCollapsible={true} - key="recentlyViewed" + isLoading={false} + isLoadingMessage={false} onToggle={[Function]} - title="Recently viewed" + paddingSize="none" > - - - -

    - Recently viewed -

    -
    -
    - - } - className="euiCollapsibleNavGroup euiCollapsibleNavGroup--light euiCollapsibleNavGroup--withHeading" +
    -
    - -
    -
    + +
    + +
    + + + +
    +
    + - -
    +
    +
    -
    - - - -
    + dashboard + + + + + +
    - -
    +
    +
    -
    -
    - -
    -
    - + + + +
    +
    + +
    -
    - +
    + +
      + +
    • + +
    • +
      +
    +
    +
    +
    + + +
    + + + Undock navigation + + , + } + } + color="subdued" + data-test-subj="collapsible-nav-lock" + iconType="lock" + label="Undock navigation" onClick={[Function]} - size="s" + size="xs" >
  • @@ -5445,163 +5540,11 @@ exports[`Header renders 1`] = `
    - - -
    -
    - -
      - - - - Undock navigation - - , - } - } - color="subdued" - data-test-subj="collapsible-nav-lock" - iconType="lock" - label="Undock navigation" - onClick={[Function]} - size="xs" - > -
    • - -
    • -
      -
    -
    -
    -
    -
    -
    -
    -
    - - - - - - - - + + +
    + + diff --git a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx index 7f338a859e7b4..460770744d53a 100644 --- a/src/core/public/chrome/ui/header/collapsible_nav.test.tsx +++ b/src/core/public/chrome/ui/header/collapsible_nav.test.tsx @@ -16,10 +16,6 @@ import { httpServiceMock } from '../../../http/http_service.mock'; import { ChromeRecentlyAccessedHistoryItem } from '../../recently_accessed'; import { CollapsibleNav } from './collapsible_nav'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => 'mockId', -})); - const { kibana, observability, security, management } = DEFAULT_APP_CATEGORIES; function mockLink({ title = 'discover', category }: Partial) { diff --git a/src/core/public/chrome/ui/header/header.test.tsx b/src/core/public/chrome/ui/header/header.test.tsx index fdbdde8556eeb..a3a0197b4017e 100644 --- a/src/core/public/chrome/ui/header/header.test.tsx +++ b/src/core/public/chrome/ui/header/header.test.tsx @@ -99,7 +99,7 @@ describe('Header', () => { act(() => isLocked$.next(true)); component.update(); - expect(component.find('nav[aria-label="Primary"]').exists()).toBeTruthy(); + expect(component.find('[data-test-subj="collapsibleNav"]').exists()).toBeTruthy(); expect(component).toMatchSnapshot(); act(() => diff --git a/src/core/public/chrome/ui/header/header.tsx b/src/core/public/chrome/ui/header/header.tsx index 67cdd24aae848..246ca83ef5ade 100644 --- a/src/core/public/chrome/ui/header/header.tsx +++ b/src/core/public/chrome/ui/header/header.tsx @@ -87,6 +87,7 @@ export function Header({ const isVisible = useObservable(observables.isVisible$, false); const isLocked = useObservable(observables.isLocked$, false); const [isNavOpen, setIsNavOpen] = useState(false); + const [navId] = useState(htmlIdGenerator()()); const breadcrumbsAppendExtension = useObservable(breadcrumbsAppendExtension$); if (!isVisible) { @@ -99,7 +100,6 @@ export function Header({ } const toggleCollapsibleNavRef = createRef void }>(); - const navId = htmlIdGenerator()(); const className = classnames('hide-for-sharing', 'headerGlobalNav'); const Breadcrumbs = ( diff --git a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap index f5a1c51ccbe15..fbd09f3096854 100644 --- a/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap +++ b/src/core/public/overlays/flyout/__snapshots__/flyout_service.test.tsx.snap @@ -26,7 +26,7 @@ Array [ ] `; -exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
    Flyout content
    "`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
    Flyout content 2
    "`; diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 3386fa73f328a..de138cdf402e6 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -26,7 +26,7 @@ } .euiBody--collapsibleNavIsDocked .euiBottomBar { - margin-left: $euiCollapsibleNavWidth; + margin-left: 320px; // Hard-coded for now -- @cchaos } // Temporary fix for EuiPageHeader with a bottom border but no tabs or padding diff --git a/src/plugins/console/public/application/components/welcome_panel.tsx b/src/plugins/console/public/application/components/welcome_panel.tsx index eb746e313d228..8514d41c04a51 100644 --- a/src/plugins/console/public/application/components/welcome_panel.tsx +++ b/src/plugins/console/public/application/components/welcome_panel.tsx @@ -27,7 +27,7 @@ interface Props { export function WelcomePanel(props: Props) { return ( - +

    diff --git a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap index 9f56740fdac22..afe339f3f43a2 100644 --- a/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap +++ b/src/plugins/dashboard/public/application/embeddable/empty_screen/__snapshots__/dashboard_empty_screen.test.tsx.snap @@ -603,7 +603,7 @@ exports[`DashboardEmptyScreen renders correctly with readonly mode 1`] = ` } > -
    -
    +
    @@ -950,7 +950,7 @@ exports[`DashboardEmptyScreen renders correctly with view mode 1`] = ` } > -
    -
    +
    diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index a0a7e54d27532..0ab3f8a4e3466 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -176,27 +176,27 @@ exports[`Inspector Data View component should render empty state 1`] = `
    + +

    + + No data available + +

    +
    - -

    - - No data available - -

    -
    diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx index 60841799b1398..50be2473a441e 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_flyout.test.tsx @@ -144,7 +144,9 @@ describe('Discover flyout', function () { expect(props.setExpandedDoc.mock.calls[0][0]._id).toBe('4'); }); - it('allows navigating with arrow keys through documents', () => { + // EuiFlyout is mocked in Jest environments. + // EUI team to reinstate `onKeyDown`: https://github.com/elastic/eui/issues/4883 + it.skip('allows navigating with arrow keys through documents', () => { const props = getProps(); const component = mountWithIntl(); findTestSubject(component, 'docTableDetailsFlyout').simulate('keydown', { key: 'ArrowRight' }); diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index f40dbbbae1f87..68786871825ac 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -147,27 +147,27 @@ exports[`Source Viewer component renders error state 1`] = ` />
    + +

    + An Error Occurred +

    +
    - -

    - An Error Occurred -

    -
    diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap index 40170c39942e5..79c1a11cfef84 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/url/__snapshots__/url.test.tsx.snap @@ -153,7 +153,7 @@ exports[`UrlFormatEditor should render normally 1`] = ` class="euiFormControlLayout__childrenWrapper" > diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index 5ad8205365146..67d2cf72c5375 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -329,6 +329,7 @@ exports[`InspectorPanel should render as expected 1`] = ` >
    & { +export type KibanaPageTemplateSolutionNavProps = Partial> & { /** * Name of the solution, i.e. "Observability" */ diff --git a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx index 5b424c7e95f18..1af85da983085 100644 --- a/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx +++ b/src/plugins/presentation_util/public/components/labs/labs_flyout.tsx @@ -20,7 +20,6 @@ import { EuiFlexItem, EuiFlexGroup, EuiIcon, - EuiOverlayMask, } from '@elastic/eui'; import { SolutionName, ProjectStatus, ProjectID, Project, EnvironmentName } from '../../../common'; @@ -124,30 +123,32 @@ export const LabsFlyout = (props: Props) => { ); return ( - onClose()} headerZindexLocation="below"> - - - -

    - - - - - {strings.getTitleLabel()} - -

    -
    - - -

    {strings.getDescriptionMessage()}

    -
    -
    - - - - {footer} -
    -
    + + + +

    + + + + + {strings.getTitleLabel()} + +

    +
    + + +

    {strings.getDescriptionMessage()}

    +
    +
    + + + + {footer} +
    ); }; diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap index 5239a92543539..5a8cd06b8ecc0 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/intro.test.tsx.snap @@ -47,20 +47,30 @@ exports[`Intro component renders correctly 1`] = `
    -
    - +
    - Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. - -
    +
    + + Modifying objects is for advanced users only. Object properties are not validated and invalid objects could cause errors, data loss, or worse. Unless someone with intimate knowledge of the code told you to be in here, you probably shouldn’t be. + +
    +
    +
    diff --git a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap index bddfe000008d4..f977c17df41d3 100644 --- a/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/object_view/components/__snapshots__/not_found_errors.test.tsx.snap @@ -49,29 +49,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern type 1`] =
    -
    - - The index pattern associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The index pattern associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -128,29 +138,39 @@ exports[`NotFoundErrors component renders correctly for index-pattern-field type
    -
    - - A field associated with this object no longer exists in the index pattern. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + A field associated with this object no longer exists in the index pattern. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -207,29 +227,39 @@ exports[`NotFoundErrors component renders correctly for search type 1`] = `
    -
    - - The saved search associated with this object no longer exists. - -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    + + The saved search associated with this object no longer exists. + +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    @@ -286,21 +316,31 @@ exports[`NotFoundErrors component renders correctly for unknown type 1`] = `
    -
    -
    - +
    - If you know what this error means, go ahead and fix it — otherwise click the delete button above. - -
    +
    +
    + + If you know what this error means, go ahead and fix it — otherwise click the delete button above. + +
    +
    +
    diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index a68e8891b5ad1..bd97f2e6bffb1 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -2,6 +2,7 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` @@ -277,6 +278,7 @@ exports[`Flyout conflicts should allow conflict resolution 2`] = ` exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` @@ -548,6 +550,7 @@ Array [ exports[`Flyout should render import step 1`] = ` diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index 62e0cd0504e8e..f6c8d5fb69408 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -960,7 +960,7 @@ export class Flyout extends Component { } return ( - +

    diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx index 8e975f9904256..50d3e8c38e389 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_picker.test.tsx @@ -36,7 +36,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: '#68BC00' }; component = mount(); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('#68BC00'); }); @@ -44,7 +44,7 @@ describe('ColorPicker', () => { const props = { ...defaultProps, value: 'rgba(85,66,177,1)' }; component = mount(); component.find('.tvbColorPicker button').simulate('click'); - const input = findTestSubject(component, 'topColorPickerInput'); + const input = findTestSubject(component, 'euiColorPickerInput_top'); expect(input.props().value).toBe('85,66,177,1'); }); diff --git a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap index 25ec05c83a8c6..56e2cb1b60f3c 100644 --- a/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap +++ b/src/plugins/visualizations/public/components/__snapshots__/visualization_noresults.test.js.snap @@ -14,7 +14,7 @@ exports[`VisualizationNoResults should render according to snapshot 1`] = ` data-euiicon-type="visualizeApp" />
    { await PageObjects.settings.clickEditFieldFormat(); await a11y.testAppSnapshot(); + await PageObjects.settings.clickCloseEditFieldFormatFlyout(); }); it('Advanced settings', async () => { diff --git a/test/functional/apps/management/_import_objects.ts b/test/functional/apps/management/_import_objects.ts index 0278955c577a1..6ef0bfd5a09e8 100644 --- a/test/functional/apps/management/_import_objects.ts +++ b/test/functional/apps/management/_import_objects.ts @@ -419,14 +419,16 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'index-pattern-test-1' ); - await testSubjects.click('pagination-button-next'); + const flyout = await testSubjects.find('importSavedObjectsFlyout'); + + await (await flyout.findByTestSubject('pagination-button-next')).click(); await PageObjects.savedObjects.setOverriddenIndexPatternValue( 'missing-index-pattern-7', 'index-pattern-test-2' ); - await testSubjects.click('pagination-button-previous'); + await (await flyout.findByTestSubject('pagination-button-previous')).click(); const selectedIdForMissingIndexPattern1 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-1', @@ -435,7 +437,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(selectedIdForMissingIndexPattern1).to.eql('f1e4c910-a2e6-11e7-bb30-233be9be6a20'); - await testSubjects.click('pagination-button-next'); + await (await flyout.findByTestSubject('pagination-button-next')).click(); const selectedIdForMissingIndexPattern7 = await testSubjects.getAttribute( 'managementChangeIndexSelection-missing-index-pattern-7', diff --git a/test/functional/page_objects/settings_page.ts b/test/functional/page_objects/settings_page.ts index 88951bb04c956..cb8f198177017 100644 --- a/test/functional/page_objects/settings_page.ts +++ b/test/functional/page_objects/settings_page.ts @@ -739,6 +739,10 @@ export class SettingsPageObject extends FtrService { await this.testSubjects.click('editFieldFormat'); } + async clickCloseEditFieldFormatFlyout() { + await this.testSubjects.click('euiFlyoutCloseButton'); + } + async associateIndexPattern(oldIndexPatternId: string, newIndexPatternTitle: string) { await this.find.clickByCssSelector( `select[data-test-subj="managementChangeIndexSelection-${oldIndexPatternId}"] > diff --git a/test/functional/page_objects/visual_builder_page.ts b/test/functional/page_objects/visual_builder_page.ts index 6e263dd1cdbbf..7f1ea64bcd979 100644 --- a/test/functional/page_objects/visual_builder_page.ts +++ b/test/functional/page_objects/visual_builder_page.ts @@ -563,7 +563,7 @@ export class VisualBuilderPageObject extends FtrService { public async checkColorPickerPopUpIsPresent(): Promise { this.log.debug(`Check color picker popup is present`); - await this.testSubjects.existOrFail('colorPickerPopover', { timeout: 5000 }); + await this.testSubjects.existOrFail('euiColorPickerPopover', { timeout: 5000 }); } public async changePanelPreview(nth: number = 0): Promise { diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx index 8549f09bba248..09fbf07b8ecbd 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/WaterfallWithSummmary/WaterfallContainer/Waterfall/ResponsiveFlyout.tsx @@ -5,10 +5,21 @@ * 2.0. */ +import { ReactNode } from 'react'; +import { StyledComponent } from 'styled-components'; import { EuiFlyout } from '@elastic/eui'; -import { euiStyled } from '../../../../../../../../../../src/plugins/kibana_react/common'; +import { + euiStyled, + EuiTheme, +} from '../../../../../../../../../../src/plugins/kibana_react/common'; -export const ResponsiveFlyout = euiStyled(EuiFlyout)` +// TODO: EUI team follow up on complex types and styled-components `styled` +// https://github.com/elastic/eui/issues/4855 +export const ResponsiveFlyout: StyledComponent< + typeof EuiFlyout, + EuiTheme, + { children?: ReactNode } +> = euiStyled(EuiFlyout)` width: 100%; @media (min-width: 800px) { diff --git a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot index 34b6b333f3ef5..d567d3cf85f13 100644 --- a/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/asset_manager/__stories__/__snapshots__/asset_manager.stories.storyshot @@ -116,20 +116,13 @@ exports[`Storyshots components/Assets/AssetManager no assets 1`] = ` size="xxl" />
    - -

    - Import your assets to get started -

    -
    - + Import your assets to get started +

    diff --git a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot index 18f86aca24302..dc66eef809050 100644 --- a/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot +++ b/x-pack/plugins/canvas/public/components/custom_element_modal/__stories__/__snapshots__/custom_element_modal.stories.storyshot @@ -80,7 +80,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormControlLayout__childrenWrapper" >
    40 characters remaining
    @@ -119,7 +119,7 @@ exports[`Storyshots components/Elements/CustomElementModal with description 1`] className="euiFormRow__fieldWrapper" >