From a50a87955cb433961bd72d0e662e1e53f09b49c5 Mon Sep 17 00:00:00 2001 From: Scotty Bollinger Date: Thu, 26 Aug 2021 00:32:13 -0500 Subject: [PATCH 01/11] Fix flakey test (#110154) --- .../credential_item/credential_item.test.tsx | 17 ++--------------- 1 file changed, 2 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx index 26bbbc4bed9e8..101d1e0eb2239 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/credential_item/credential_item.test.tsx @@ -20,18 +20,6 @@ const value = 'foo'; const props = { label, testSubj, value }; describe('CredentialItem', () => { - const setState = jest.fn(); - const useStateMock: any = (initState: any) => [initState, setState]; - - beforeEach(() => { - jest.spyOn(React, 'useState').mockImplementation(useStateMock); - setState(false); - }); - - afterEach(() => { - jest.clearAllMocks(); - }); - it('renders', () => { const wrapper = shallow(); @@ -52,16 +40,15 @@ describe('CredentialItem', () => { expect(wrapper.find(EuiCopy)).toHaveLength(0); }); - it.skip('handles credential visible toggle click', () => { + it('handles credential visible toggle click', () => { const wrapper = shallow(); const button = wrapper.find(EuiButtonIcon).dive().find('button'); button.simulate('click'); - expect(setState).toHaveBeenCalled(); expect(wrapper.find(EuiFieldText)).toHaveLength(1); }); - it.skip('handles select all button click', () => { + it('handles select all button click', () => { const wrapper = shallow(); // Toggle isVisible before EuiFieldText is visible const button = wrapper.find(EuiButtonIcon).dive().find('button'); From de7ae4138d23d95662d7c286c73cff155d395f85 Mon Sep 17 00:00:00 2001 From: Gloria Hornero Date: Thu, 26 Aug 2021 08:43:46 +0200 Subject: [PATCH 02/11] fixes 'Marking alerts as acknowledged' test (#110032) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../integration/detection_alerts/acknowledged.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts index d81c444824a2a..5d72105178b69 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/acknowledged.spec.ts @@ -12,11 +12,9 @@ import { selectNumberOfAlerts, waitForAlertsPanelToBeLoaded, waitForAlerts, - waitForAlertsToBeLoaded, markAcknowledgedFirstAlert, goToAcknowledgedAlerts, waitForAlertsIndexToBeCreated, - goToOpenedAlerts, } from '../../tasks/alerts'; import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; import { cleanKibana } from '../../tasks/common'; @@ -26,7 +24,7 @@ import { refreshPage } from '../../tasks/security_header'; import { ALERTS_URL } from '../../urls/navigation'; -describe.skip('Marking alerts as acknowledged', () => { +describe('Marking alerts as acknowledged', () => { beforeEach(() => { cleanKibana(); loginAndWaitForPage(ALERTS_URL); @@ -50,10 +48,6 @@ describe.skip('Marking alerts as acknowledged', () => { cy.get(TAKE_ACTION_POPOVER_BTN).should('exist'); markAcknowledgedFirstAlert(); - refreshPage(); - waitForAlertsToBeLoaded(); - goToOpenedAlerts(); - waitForAlertsToBeLoaded(); const expectedNumberOfAlerts = +numberOfAlerts - numberOfAlertsToBeMarkedAcknowledged; cy.get(ALERTS_COUNT).should('have.text', `${expectedNumberOfAlerts} alerts`); From 9e04d2c5c7b2139b7e23743b133ea79bf93f5ba3 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Thu, 26 Aug 2021 09:53:28 +0200 Subject: [PATCH 03/11] [Reporting/Dashboard] Update integration to use v2 reports (#108553) * very wip, updating dashboard integration to use v2 reports. at the moment time filters are not working correctly * added missing dependency to hook * added tests and refined ForwadedAppState interface * remove unused import * updated test because generating a report from an unsaved report is possible * migrated locator to forward state on history only, reordered methods on react component * remove unused import * update locator test and use panel index number if panelIndex does not exist * ensure locator params are serializable * - moved getSerializableRecord to locator.ts to ensure that the values we get from it will never contain something that cannot be passed to history.push - updated types to remove some `& SerializableRecord` instances - fixed embeddable drilldown Jest tests given that we no longer expect state to be in the URL * update generated api docs * remove unused variable * - removed SerializedRecord extension from dashboard locator params interface - factored out state conversion logic from the locator getLocation * updated locator jest tests and SerializableRecord types * explicitly map values to dashboardlocatorparams and export serializable params type * use serializable params type in embeddable * factored out logic for converting panels to dashboard panels map * use "type =" instead of "interface" * big update to locator params: type fixes and added options key * added comment about why we are using "type" alias instead of "interface" declaration * simplify is v2 job param check Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../kibana-plugin-plugins-data-public.md | 2 +- ...gin-plugins-data-public.refreshinterval.md | 15 +- ...ugins-data-public.refreshinterval.pause.md | 11 - ...ugins-data-public.refreshinterval.value.md | 11 - src/plugins/dashboard/common/bwc/types.ts | 3 +- .../dashboard/common/embeddable/types.ts | 5 +- .../dashboard/common/migrate_to_730_panels.ts | 7 +- .../hooks/use_dashboard_app_state.ts | 9 + .../lib/convert_dashboard_state.ts | 14 +- .../lib/convert_saved_panels_to_panel_map.ts | 18 ++ .../dashboard/public/application/lib/index.ts | 1 + .../load_dashboard_history_location_state.ts | 29 +++ .../lib/load_dashboard_url_state.ts | 11 +- .../application/lib/migrate_app_state.ts | 3 +- .../application/top_nav/dashboard_top_nav.tsx | 4 +- .../application/top_nav/show_share_modal.tsx | 49 ++++- src/plugins/dashboard/public/locator.test.ts | 190 +++++++++++++++--- src/plugins/dashboard/public/locator.ts | 60 ++++-- src/plugins/dashboard/public/types.ts | 8 +- .../data/common/query/timefilter/types.ts | 9 +- src/plugins/data/public/public.api.md | 6 +- ...embeddable_to_dashboard_drilldown.test.tsx | 46 ++++- x-pack/plugins/reporting/common/job_utils.ts | 2 +- .../register_pdf_png_reporting.tsx | 5 +- .../reporting_panel_content.tsx | 8 +- .../apps/dashboard/reporting/screenshots.ts | 12 +- 26 files changed, 388 insertions(+), 150 deletions(-) delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md delete mode 100644 docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md create mode 100644 src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts create mode 100644 src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index 185dd771c4ace..7548aa62eb313 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -80,7 +80,6 @@ | [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* | | [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* | | [Reason](./kibana-plugin-plugins-data-public.reason.md) | | -| [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | | | [SavedQuery](./kibana-plugin-plugins-data-public.savedquery.md) | | | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchSessionInfoProvider](./kibana-plugin-plugins-data-public.searchsessioninfoprovider.md) | Provide info about current search session to be stored in the Search Session saved object | @@ -176,6 +175,7 @@ | [RangeFilter](./kibana-plugin-plugins-data-public.rangefilter.md) | | | [RangeFilterMeta](./kibana-plugin-plugins-data-public.rangefiltermeta.md) | | | [RangeFilterParams](./kibana-plugin-plugins-data-public.rangefilterparams.md) | | +| [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) | | | [SavedQueryTimeFilter](./kibana-plugin-plugins-data-public.savedquerytimefilter.md) | | | [SearchBarProps](./kibana-plugin-plugins-data-public.searchbarprops.md) | | | [StatefulSearchBarProps](./kibana-plugin-plugins-data-public.statefulsearchbarprops.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md index 6a6350d8ba4f6..b6067e081b943 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.md @@ -2,18 +2,13 @@ [Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) -## RefreshInterval interface +## RefreshInterval type Signature: ```typescript -export interface RefreshInterval +export declare type RefreshInterval = { + pause: boolean; + value: number; +}; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [pause](./kibana-plugin-plugins-data-public.refreshinterval.pause.md) | boolean | | -| [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md) | number | | - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md deleted file mode 100644 index fb854fcbbc277..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.pause.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) > [pause](./kibana-plugin-plugins-data-public.refreshinterval.pause.md) - -## RefreshInterval.pause property - -Signature: - -```typescript -pause: boolean; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md deleted file mode 100644 index 021a01391b71e..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.refreshinterval.value.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [RefreshInterval](./kibana-plugin-plugins-data-public.refreshinterval.md) > [value](./kibana-plugin-plugins-data-public.refreshinterval.value.md) - -## RefreshInterval.value property - -Signature: - -```typescript -value: number; -``` diff --git a/src/plugins/dashboard/common/bwc/types.ts b/src/plugins/dashboard/common/bwc/types.ts index f3c384a76c391..ba479210cb009 100644 --- a/src/plugins/dashboard/common/bwc/types.ts +++ b/src/plugins/dashboard/common/bwc/types.ts @@ -7,6 +7,7 @@ */ import { SavedObjectReference } from 'kibana/public'; +import type { Serializable } from '@kbn/utility-types'; import { GridData } from '../'; @@ -110,7 +111,7 @@ export type RawSavedDashboardPanel630 = RawSavedDashboardPanel620; // In 6.2 we added an inplace migration, moving uiState into each panel's new embeddableConfig property. // Source: https://github.com/elastic/kibana/pull/14949 export type RawSavedDashboardPanel620 = RawSavedDashboardPanel610 & { - embeddableConfig: { [key: string]: unknown }; + embeddableConfig: { [key: string]: Serializable }; version: string; }; diff --git a/src/plugins/dashboard/common/embeddable/types.ts b/src/plugins/dashboard/common/embeddable/types.ts index 83507d21665d9..d786078766f78 100644 --- a/src/plugins/dashboard/common/embeddable/types.ts +++ b/src/plugins/dashboard/common/embeddable/types.ts @@ -6,10 +6,11 @@ * Side Public License, v 1. */ -export interface GridData { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type GridData = { w: number; h: number; x: number; y: number; i: string; -} +}; diff --git a/src/plugins/dashboard/common/migrate_to_730_panels.ts b/src/plugins/dashboard/common/migrate_to_730_panels.ts index 48c3ddb463ed8..ad0fa7658b6fa 100644 --- a/src/plugins/dashboard/common/migrate_to_730_panels.ts +++ b/src/plugins/dashboard/common/migrate_to_730_panels.ts @@ -7,6 +7,7 @@ */ import { i18n } from '@kbn/i18n'; +import type { SerializableRecord } from '@kbn/utility-types'; import semverSatisfies from 'semver/functions/satisfies'; import uuid from 'uuid'; import { @@ -80,7 +81,7 @@ function migratePre61PanelToLatest( panel: RawSavedDashboardPanelTo60, version: string, useMargins: boolean, - uiState?: { [key: string]: { [key: string]: unknown } } + uiState?: { [key: string]: SerializableRecord } ): RawSavedDashboardPanel730ToLatest { if (panel.col === undefined || panel.row === undefined) { throw new Error( @@ -138,7 +139,7 @@ function migrate610PanelToLatest( panel: RawSavedDashboardPanel610, version: string, useMargins: boolean, - uiState?: { [key: string]: { [key: string]: unknown } } + uiState?: { [key: string]: SerializableRecord } ): RawSavedDashboardPanel730ToLatest { (['w', 'x', 'h', 'y'] as Array).forEach((key) => { if (panel.gridData[key] === undefined) { @@ -273,7 +274,7 @@ export function migratePanelsTo730( >, version: string, useMargins: boolean, - uiState?: { [key: string]: { [key: string]: unknown } } + uiState?: { [key: string]: SerializableRecord } ): RawSavedDashboardPanel730ToLatest[] { return panels.map((panel) => { if (isPre61Panel(panel)) { diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts index 9df486c677dda..c01cd43b1f1e3 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_app_state.ts @@ -25,7 +25,9 @@ import { DashboardRedirect, DashboardState, } from '../../types'; +import { DashboardAppLocatorParams } from '../../locator'; import { + loadDashboardHistoryLocationState, tryDestroyDashboardContainer, syncDashboardContainerInput, savedObjectToDashboardState, @@ -88,6 +90,7 @@ export const useDashboardAppState = ({ savedObjectsTagging, dashboardCapabilities, dashboardSessionStorage, + scopedHistory, } = services; const { docTitle } = chrome; const { notifications } = core; @@ -149,10 +152,15 @@ export const useDashboardAppState = ({ */ const dashboardSessionStorageState = dashboardSessionStorage.getState(savedDashboardId) || {}; const dashboardURLState = loadDashboardUrlState(dashboardBuildContext); + const forwardedAppState = loadDashboardHistoryLocationState( + scopedHistory()?.location?.state as undefined | DashboardAppLocatorParams + ); + const initialDashboardState = { ...savedDashboardState, ...dashboardSessionStorageState, ...dashboardURLState, + ...forwardedAppState, // if there is an incoming embeddable, dashboard always needs to be in edit mode to receive it. ...(incomingEmbeddable ? { viewMode: ViewMode.EDIT } : {}), @@ -312,6 +320,7 @@ export const useDashboardAppState = ({ getStateTransfer, savedDashboards, usageCollection, + scopedHistory, notifications, indexPatterns, kibanaVersion, diff --git a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts index d17f8405d734f..ad84b794a2379 100644 --- a/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts +++ b/src/plugins/dashboard/public/application/lib/convert_dashboard_state.ts @@ -11,19 +11,15 @@ import type { KibanaExecutionContext } from 'src/core/public'; import { DashboardSavedObject } from '../../saved_dashboards'; import { getTagsFromSavedDashboard, migrateAppState } from '.'; import { EmbeddablePackageState, ViewMode } from '../../services/embeddable'; -import { - convertPanelStateToSavedDashboardPanel, - convertSavedDashboardPanelToPanelState, -} from '../../../common/embeddable/embeddable_saved_object_converters'; +import { convertPanelStateToSavedDashboardPanel } from '../../../common/embeddable/embeddable_saved_object_converters'; import { DashboardState, RawDashboardState, - DashboardPanelMap, - SavedDashboardPanel, DashboardAppServices, DashboardContainerInput, DashboardBuildContext, } from '../../types'; +import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map'; interface SavedObjectToDashboardStateProps { version: string; @@ -77,11 +73,7 @@ export const savedObjectToDashboardState = ({ usageCollection ); - const panels: DashboardPanelMap = {}; - rawState.panels?.forEach((panel: SavedDashboardPanel) => { - panels[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); - }); - return { ...rawState, panels }; + return { ...rawState, panels: convertSavedPanelsToPanelMap(rawState.panels) }; }; /** diff --git a/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts b/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts new file mode 100644 index 0000000000000..b91940f45081b --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/convert_saved_panels_to_panel_map.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { convertSavedDashboardPanelToPanelState } from '../../../common/embeddable/embeddable_saved_object_converters'; +import type { SavedDashboardPanel, DashboardPanelMap } from '../../types'; + +export const convertSavedPanelsToPanelMap = (panels?: SavedDashboardPanel[]): DashboardPanelMap => { + const panelsMap: DashboardPanelMap = {}; + panels?.forEach((panel, idx) => { + panelsMap![panel.panelIndex ?? String(idx)] = convertSavedDashboardPanelToPanelState(panel); + }); + return panelsMap; +}; diff --git a/src/plugins/dashboard/public/application/lib/index.ts b/src/plugins/dashboard/public/application/lib/index.ts index 937c1d2a77c06..9aba481c3fb86 100644 --- a/src/plugins/dashboard/public/application/lib/index.ts +++ b/src/plugins/dashboard/public/application/lib/index.ts @@ -20,6 +20,7 @@ export { syncDashboardFilterState } from './sync_dashboard_filter_state'; export { syncDashboardIndexPatterns } from './sync_dashboard_index_patterns'; export { syncDashboardContainerInput } from './sync_dashboard_container_input'; export { diffDashboardContainerInput, diffDashboardState } from './diff_dashboard_state'; +export { loadDashboardHistoryLocationState } from './load_dashboard_history_location_state'; export { buildDashboardContainer, tryDestroyDashboardContainer } from './build_dashboard_container'; export { stateToDashboardContainerInput, diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts new file mode 100644 index 0000000000000..d20e14cea74b5 --- /dev/null +++ b/src/plugins/dashboard/public/application/lib/load_dashboard_history_location_state.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { ForwardedDashboardState } from '../../locator'; +import { DashboardState } from '../../types'; +import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map'; + +export const loadDashboardHistoryLocationState = ( + state?: ForwardedDashboardState +): Partial => { + if (!state) { + return {}; + } + + const { panels, ...restOfState } = state; + if (!panels?.length) { + return restOfState; + } + + return { + ...restOfState, + ...{ panels: convertSavedPanelsToPanelMap(panels) }, + }; +}; diff --git a/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts b/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts index efff2ba6bc087..f76382c1fbbdd 100644 --- a/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts +++ b/src/plugins/dashboard/public/application/lib/load_dashboard_url_state.ts @@ -11,15 +11,14 @@ import _ from 'lodash'; import { migrateAppState } from '.'; import { replaceUrlHashQuery } from '../../../../kibana_utils/public'; import { DASHBOARD_STATE_STORAGE_KEY } from '../../dashboard_constants'; -import { convertSavedDashboardPanelToPanelState } from '../../../common/embeddable/embeddable_saved_object_converters'; -import { +import type { DashboardBuildContext, DashboardPanelMap, DashboardState, RawDashboardState, - SavedDashboardPanel, } from '../../types'; import { migrateLegacyQuery } from './migrate_legacy_query'; +import { convertSavedPanelsToPanelMap } from './convert_saved_panels_to_panel_map'; /** * Loads any dashboard state from the URL, and removes the state from the URL. @@ -32,12 +31,10 @@ export const loadDashboardUrlState = ({ const rawAppStateInUrl = kbnUrlStateStorage.get(DASHBOARD_STATE_STORAGE_KEY); if (!rawAppStateInUrl) return {}; - const panelsMap: DashboardPanelMap = {}; + let panelsMap: DashboardPanelMap = {}; if (rawAppStateInUrl.panels && rawAppStateInUrl.panels.length > 0) { const rawState = migrateAppState(rawAppStateInUrl, kibanaVersion, usageCollection); - rawState.panels?.forEach((panel: SavedDashboardPanel) => { - panelsMap[panel.panelIndex] = convertSavedDashboardPanelToPanelState(panel); - }); + panelsMap = convertSavedPanelsToPanelMap(rawState.panels); } const migratedQuery = rawAppStateInUrl.query diff --git a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts index fb8ef1b9ba2da..06290205d65df 100644 --- a/src/plugins/dashboard/public/application/lib/migrate_app_state.ts +++ b/src/plugins/dashboard/public/application/lib/migrate_app_state.ts @@ -7,6 +7,7 @@ */ import semverSatisfies from 'semver/functions/satisfies'; +import type { SerializableRecord } from '@kbn/utility-types'; import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; @@ -75,7 +76,7 @@ export function migrateAppState( >, kibanaVersion, appState.useMargins as boolean, - appState.uiState as Record> + appState.uiState as { [key: string]: SerializableRecord } ) as SavedDashboardPanel[]; delete appState.uiState; } diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 80368d52cb110..e6a2c41fd4ecb 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -404,6 +404,7 @@ export function DashboardTopNav({ (anchorElement: HTMLElement) => { if (!share) return; const currentState = dashboardAppState.getLatestDashboardState(); + const timeRange = timefilter.getTime(); ShowShareModal({ share, kibanaVersion, @@ -412,9 +413,10 @@ export function DashboardTopNav({ currentDashboardState: currentState, savedDashboard: dashboardAppState.savedDashboard, isDirty: Boolean(dashboardAppState.hasUnsavedChanges), + timeRange, }); }, - [dashboardAppState, dashboardCapabilities, share, kibanaVersion] + [dashboardAppState, dashboardCapabilities, share, kibanaVersion, timefilter] ); const dashboardTopNavActions = useMemo(() => { diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx index 239d2bf72b9c1..b9c77dec87b66 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.tsx @@ -6,16 +6,20 @@ * Side Public License, v 1. */ -import { Capabilities } from 'src/core/public'; import { EuiCheckboxGroup } from '@elastic/eui'; -import React from 'react'; -import { ReactElement, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import React, { ReactElement, useState } from 'react'; +import type { Capabilities } from 'src/core/public'; import { DashboardSavedObject } from '../..'; +import { shareModalStrings } from '../../dashboard_strings'; +import { DashboardAppLocatorParams, DASHBOARD_APP_LOCATOR } from '../../locator'; +import { TimeRange } from '../../services/data'; +import { ViewMode } from '../../services/embeddable'; import { setStateToKbnUrl, unhashUrl } from '../../services/kibana_utils'; import { SharePluginStart } from '../../services/share'; -import { dashboardUrlParams } from '../dashboard_router'; -import { shareModalStrings } from '../../dashboard_strings'; import { DashboardAppCapabilities, DashboardState } from '../../types'; +import { dashboardUrlParams } from '../dashboard_router'; import { stateToRawDashboardState } from '../lib/convert_dashboard_state'; const showFilterBarId = 'showFilterBar'; @@ -28,6 +32,7 @@ interface ShowShareModalProps { savedDashboard: DashboardSavedObject; currentDashboardState: DashboardState; dashboardCapabilities: DashboardAppCapabilities; + timeRange: TimeRange; } export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => { @@ -46,6 +51,7 @@ export function ShowShareModal({ savedDashboard, dashboardCapabilities, currentDashboardState, + timeRange, }: ShowShareModalProps) { const EmbedUrlParamExtension = ({ setParamValue, @@ -104,6 +110,25 @@ export function ShowShareModal({ ); }; + const rawDashboardState = stateToRawDashboardState({ + state: currentDashboardState, + version: kibanaVersion, + }); + + const locatorParams: DashboardAppLocatorParams = { + dashboardId: savedDashboard.id, + filters: rawDashboardState.filters, + preserveSavedFilters: true, + query: rawDashboardState.query, + savedQuery: rawDashboardState.savedQuery, + useHash: false, + panels: rawDashboardState.panels, + timeRange, + viewMode: ViewMode.VIEW, // For share locators we always load the dashboard in view mode + refreshInterval: undefined, // We don't share refresh interval externally + options: rawDashboardState.options, + }; + share.toggleShareContextMenu({ isDirty, anchorElement, @@ -111,14 +136,24 @@ export function ShowShareModal({ allowShortUrl: dashboardCapabilities.createShortUrl, shareableUrl: setStateToKbnUrl( '_a', - stateToRawDashboardState({ state: currentDashboardState, version: kibanaVersion }), + rawDashboardState, { useHash: false, storeInHashQuery: true }, unhashUrl(window.location.href) ), objectId: savedDashboard.id, objectType: 'dashboard', sharingData: { - title: savedDashboard.title, + title: + savedDashboard.title || + i18n.translate('dashboard.share.defaultDashboardTitle', { + defaultMessage: 'Dashboard [{date}]', + values: { date: moment().toISOString(true) }, + }), + locatorParams: { + id: DASHBOARD_APP_LOCATOR, + version: kibanaVersion, + params: locatorParams, + }, }, embedUrlParamExtensions: [ { diff --git a/src/plugins/dashboard/public/locator.test.ts b/src/plugins/dashboard/public/locator.test.ts index 0b647ac00ce31..f3f5aec9f478c 100644 --- a/src/plugins/dashboard/public/locator.test.ts +++ b/src/plugins/dashboard/public/locator.test.ts @@ -17,7 +17,7 @@ describe('dashboard locator', () => { hashedItemStore.storage = mockStorage; }); - test('creates a link to a saved dashboard', async () => { + test('creates a link to an unsaved dashboard', async () => { const definition = new DashboardAppLocatorDefinition({ useHashedUrl: false, getDashboardFilterFields: async (dashboardId: string) => [], @@ -26,7 +26,7 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: '#/create?_a=()&_g=()', + path: '#/create?_g=()', state: {}, }); }); @@ -42,8 +42,14 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: '#/create?_a=()&_g=(time:(from:now-15m,mode:relative,to:now))', - state: {}, + path: '#/create?_g=(time:(from:now-15m,mode:relative,to:now))', + state: { + timeRange: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }, }); }); @@ -82,8 +88,47 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: `#/view/123?_a=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),query:(language:kuery,query:bye))&_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`, - state: {}, + path: `#/view/123?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:hi))),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))`, + state: { + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'hi', + }, + }, + { + $state: { + store: 'globalState', + }, + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'hi', + }, + }, + ], + query: { + language: 'kuery', + query: 'bye', + }, + refreshInterval: { + pause: false, + value: 300, + }, + timeRange: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }, }); }); @@ -103,8 +148,23 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: `#/view/123?_a=(filters:!(),query:(language:kuery,query:bye))&_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`, - state: {}, + path: `#/view/123?_g=(filters:!(),refreshInterval:(pause:!f,value:300),time:(from:now-15m,mode:relative,to:now))&searchSessionId=__sessionSearchId__`, + state: { + filters: [], + query: { + language: 'kuery', + query: 'bye', + }, + refreshInterval: { + pause: false, + value: 300, + }, + timeRange: { + from: 'now-15m', + mode: 'relative', + to: 'now', + }, + }, }); }); @@ -119,10 +179,11 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: `#/create?_a=(savedQuery:__savedQueryId__)&_g=()`, - state: {}, + path: `#/create?_g=()`, + state: { + savedQuery: '__savedQueryId__', + }, }); - expect(location.path).toContain('__savedQueryId__'); }); test('panels', async () => { @@ -136,8 +197,10 @@ describe('dashboard locator', () => { expect(location).toMatchObject({ app: 'dashboards', - path: `#/create?_a=(panels:!((fakePanelContent:fakePanelContent)))&_g=()`, - state: {}, + path: `#/create?_g=()`, + state: { + panels: [{ fakePanelContent: 'fakePanelContent' }], + }, }); }); @@ -224,16 +287,62 @@ describe('dashboard locator', () => { filters: [appliedFilter], }); - expect(location1.path).toEqual(expect.stringContaining('query:savedfilter1')); - expect(location1.path).toEqual(expect.stringContaining('query:appliedfilter')); + expect(location1.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`); + expect(location1.state).toMatchObject({ + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'savedfilter1', + }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'appliedfilter', + }, + }, + ], + }); const location2 = await definition.getLocation({ dashboardId: 'dashboard2', filters: [appliedFilter], }); - expect(location2.path).toEqual(expect.stringContaining('query:savedfilter2')); - expect(location2.path).toEqual(expect.stringContaining('query:appliedfilter')); + expect(location2.path).toMatchInlineSnapshot(`"#/view/dashboard2?_g=(filters:!())"`); + expect(location2.state).toMatchObject({ + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'savedfilter2', + }, + }, + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'appliedfilter', + }, + }, + ], + }); }); test("doesn't fail if can't retrieve filters from destination dashboard", async () => { @@ -252,8 +361,21 @@ describe('dashboard locator', () => { filters: [appliedFilter], }); - expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(location.path).toEqual(expect.stringContaining('query:appliedfilter')); + expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`); + expect(location.state).toMatchObject({ + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'appliedfilter', + }, + }, + ], + }); }); test('can enforce empty filters', async () => { @@ -273,11 +395,10 @@ describe('dashboard locator', () => { preserveSavedFilters: false, }); - expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(location.path).not.toEqual(expect.stringContaining('query:appliedfilter')); - expect(location.path).toMatchInlineSnapshot( - `"#/view/dashboard1?_a=(filters:!())&_g=(filters:!())"` - ); + expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`); + expect(location.state).toMatchObject({ + filters: [], + }); }); test('no filters in result url if no filters applied', async () => { @@ -295,8 +416,8 @@ describe('dashboard locator', () => { dashboardId: 'dashboard1', }); - expect(location.path).not.toEqual(expect.stringContaining('filters')); - expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_a=()&_g=()"`); + expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=()"`); + expect(location.state).toMatchObject({}); }); test('can turn off preserving filters', async () => { @@ -316,8 +437,21 @@ describe('dashboard locator', () => { preserveSavedFilters: false, }); - expect(location.path).not.toEqual(expect.stringContaining('query:savedfilter1')); - expect(location.path).toEqual(expect.stringContaining('query:appliedfilter')); + expect(location.path).toMatchInlineSnapshot(`"#/view/dashboard1?_g=(filters:!())"`); + expect(location.state).toMatchObject({ + filters: [ + { + meta: { + alias: null, + disabled: false, + negate: false, + }, + query: { + query: 'appliedfilter', + }, + }, + ], + }); }); }); }); diff --git a/src/plugins/dashboard/public/locator.ts b/src/plugins/dashboard/public/locator.ts index ed4e7a5dd4d4c..a256a65a5d7f4 100644 --- a/src/plugins/dashboard/public/locator.ts +++ b/src/plugins/dashboard/public/locator.ts @@ -7,14 +7,21 @@ */ import type { SerializableRecord } from '@kbn/utility-types'; +import { flow } from 'lodash'; import type { TimeRange, Filter, Query, QueryState, RefreshInterval } from '../../data/public'; import type { LocatorDefinition, LocatorPublic } from '../../share/public'; import type { SavedDashboardPanel } from '../common/types'; +import type { RawDashboardState } from './types'; import { esFilters } from '../../data/public'; import { setStateToKbnUrl } from '../../kibana_utils/public'; import { ViewMode } from '../../embeddable/public'; import { DashboardConstants } from './dashboard_constants'; +/** + * Useful for ensuring that we don't pass any non-serializable values to history.push (for example, functions). + */ +const getSerializableRecord: (o: O) => O & SerializableRecord = flow(JSON.stringify, JSON.parse); + const cleanEmptyKeys = (stateObj: Record) => { Object.keys(stateObj).forEach((key) => { if (stateObj[key] === undefined) { @@ -26,7 +33,12 @@ const cleanEmptyKeys = (stateObj: Record) => { export const DASHBOARD_APP_LOCATOR = 'DASHBOARD_APP_LOCATOR'; -export interface DashboardAppLocatorParams extends SerializableRecord { +/** + * We use `type` instead of `interface` to avoid having to extend this type with + * `SerializableRecord`. See https://github.com/microsoft/TypeScript/issues/15300. + */ +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type DashboardAppLocatorParams = { /** * If given, the dashboard saved object with this id will be loaded. If not given, * a new, unsaved dashboard will be loaded up. @@ -40,7 +52,7 @@ export interface DashboardAppLocatorParams extends SerializableRecord { /** * Optionally set the refresh interval. */ - refreshInterval?: RefreshInterval & SerializableRecord; + refreshInterval?: RefreshInterval; /** * Optionally apply filers. NOTE: if given and used in conjunction with `dashboardId`, and the @@ -80,13 +92,15 @@ export interface DashboardAppLocatorParams extends SerializableRecord { /** * List of dashboard panels */ - panels?: SavedDashboardPanel[] & SerializableRecord; + panels?: SavedDashboardPanel[]; /** * Saved query ID */ savedQuery?: string; -} + + options?: RawDashboardState['options']; +}; export type DashboardAppLocator = LocatorPublic; @@ -95,17 +109,29 @@ export interface DashboardAppLocatorDependencies { getDashboardFilterFields: (dashboardId: string) => Promise; } +export type ForwardedDashboardState = Omit< + DashboardAppLocatorParams, + 'dashboardId' | 'preserveSavedFilters' | 'useHash' | 'searchSessionId' +>; + export class DashboardAppLocatorDefinition implements LocatorDefinition { public readonly id = DASHBOARD_APP_LOCATOR; constructor(protected readonly deps: DashboardAppLocatorDependencies) {} public readonly getLocation = async (params: DashboardAppLocatorParams) => { - const useHash = params.useHash ?? this.deps.useHashedUrl; - const hash = params.dashboardId ? `view/${params.dashboardId}` : `create`; + const { + filters, + useHash: paramsUseHash, + preserveSavedFilters, + dashboardId, + ...restParams + } = params; + const useHash = paramsUseHash ?? this.deps.useHashedUrl; + const hash = dashboardId ? `view/${dashboardId}` : `create`; const getSavedFiltersFromDestinationDashboardIfNeeded = async (): Promise => { - if (params.preserveSavedFilters === false) return []; + if (preserveSavedFilters === false) return []; if (!params.dashboardId) return []; try { return await this.deps.getDashboardFilterFields(params.dashboardId); @@ -116,26 +142,16 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition !esFilters.isFilterPinned(f)), - viewMode: params.viewMode, - panels: params.panels, - savedQuery: params.savedQuery, - }), - { useHash }, - `#/${hash}` - ); - + let path = `#/${hash}`; path = setStateToKbnUrl( '_g', cleanEmptyKeys({ @@ -154,7 +170,7 @@ export class DashboardAppLocatorDefinition implements LocatorDefinition void; export type RedirectToProps = diff --git a/src/plugins/data/common/query/timefilter/types.ts b/src/plugins/data/common/query/timefilter/types.ts index 0f468a8f6b586..51558183c3db3 100644 --- a/src/plugins/data/common/query/timefilter/types.ts +++ b/src/plugins/data/common/query/timefilter/types.ts @@ -6,14 +6,15 @@ * Side Public License, v 1. */ -import { Moment } from 'moment'; +import type { Moment } from 'moment'; -export interface RefreshInterval { +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions +export type RefreshInterval = { pause: boolean; value: number; -} +}; -// eslint-disable-next-line +// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type TimeRange = { from: string; to: string; diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index 05743f40a7b68..d5a39e3108325 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1955,12 +1955,10 @@ export interface Reason { // Warning: (ae-missing-release-tag) "RefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export interface RefreshInterval { - // (undocumented) +export type RefreshInterval = { pause: boolean; - // (undocumented) value: number; -} +}; // Warning: (ae-missing-release-tag) "SavedQuery" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // diff --git a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx index a817d9f65c916..3272a6e27de23 100644 --- a/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx +++ b/x-pack/plugins/dashboard_enhanced/public/services/drilldowns/embeddable_to_dashboard_drilldown/embeddable_to_dashboard_drilldown.test.tsx @@ -88,6 +88,7 @@ describe('.execute() & getHref', () => { useHashedUrl: false, getDashboardFilterFields: async () => [], }); + const getLocationSpy = jest.spyOn(definition, 'getLocation'); const drilldown = new EmbeddableToDashboardDrilldown({ start: ((() => ({ core: { @@ -147,9 +148,14 @@ describe('.execute() & getHref', () => { return { href, + getLocationSpy, }; } + afterEach(() => { + jest.clearAllMocks(); + }); + test('navigates to correct dashboard', async () => { const testDashboardId = 'dashboardId'; const { href } = await setupTestBed( @@ -183,7 +189,7 @@ describe('.execute() & getHref', () => { test('navigates with query if filters are enabled', async () => { const queryString = 'querystring'; const queryLanguage = 'kuery'; - const { href } = await setupTestBed( + const { getLocationSpy } = await setupTestBed( { useCurrentFilters: true, }, @@ -193,8 +199,12 @@ describe('.execute() & getHref', () => { [] ); - expect(href).toEqual(expect.stringContaining(queryString)); - expect(href).toEqual(expect.stringContaining(queryLanguage)); + const { + state: { query }, + } = await getLocationSpy.mock.results[0].value; + + expect(query.query).toBe(queryString); + expect(query.language).toBe(queryLanguage); }); test('when user chooses to keep current filters, current filters are set on destination dashboard', async () => { @@ -202,7 +212,7 @@ describe('.execute() & getHref', () => { const existingGlobalFilterKey = 'existingGlobalFilter'; const newAppliedFilterKey = 'newAppliedFilter'; - const { href } = await setupTestBed( + const { getLocationSpy } = await setupTestBed( { useCurrentFilters: true, }, @@ -212,9 +222,16 @@ describe('.execute() & getHref', () => { [getFilter(false, newAppliedFilterKey)] ); - expect(href).toEqual(expect.stringContaining(existingAppFilterKey)); - expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); - expect(href).toEqual(expect.stringContaining(newAppliedFilterKey)); + const { + state: { filters }, + } = await getLocationSpy.mock.results[0].value; + + expect(filters.length).toBe(3); + + const filtersString = JSON.stringify(filters); + expect(filtersString).toEqual(expect.stringContaining(existingAppFilterKey)); + expect(filtersString).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(filtersString).toEqual(expect.stringContaining(newAppliedFilterKey)); }); test('when user chooses to remove current filters, current app filters are remove on destination dashboard', async () => { @@ -222,7 +239,7 @@ describe('.execute() & getHref', () => { const existingGlobalFilterKey = 'existingGlobalFilter'; const newAppliedFilterKey = 'newAppliedFilter'; - const { href } = await setupTestBed( + const { getLocationSpy } = await setupTestBed( { useCurrentFilters: false, }, @@ -232,9 +249,16 @@ describe('.execute() & getHref', () => { [getFilter(false, newAppliedFilterKey)] ); - expect(href).not.toEqual(expect.stringContaining(existingAppFilterKey)); - expect(href).toEqual(expect.stringContaining(existingGlobalFilterKey)); - expect(href).toEqual(expect.stringContaining(newAppliedFilterKey)); + const { + state: { filters }, + } = await getLocationSpy.mock.results[0].value; + + expect(filters.length).toBe(2); + + const filtersString = JSON.stringify(filters); + expect(filtersString).not.toEqual(expect.stringContaining(existingAppFilterKey)); + expect(filtersString).toEqual(expect.stringContaining(existingGlobalFilterKey)); + expect(filtersString).toEqual(expect.stringContaining(newAppliedFilterKey)); }); test('when user chooses to keep current time range, current time range is passed in url', async () => { diff --git a/x-pack/plugins/reporting/common/job_utils.ts b/x-pack/plugins/reporting/common/job_utils.ts index 1a8699eeca025..d8b4503cfefba 100644 --- a/x-pack/plugins/reporting/common/job_utils.ts +++ b/x-pack/plugins/reporting/common/job_utils.ts @@ -8,4 +8,4 @@ // TODO: Remove this code once everyone is using the new PDF format, then we can also remove the legacy // export type entirely export const isJobV2Params = ({ sharingData }: { sharingData: Record }): boolean => - Array.isArray(sharingData.locatorParams); + sharingData.locatorParams != null; diff --git a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx index 811d5803895db..0b31083a0fe8d 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/register_pdf_png_reporting.tsx @@ -127,6 +127,7 @@ export const reportingScreenshotShareProvider = ({ }; const isV2Job = isJobV2Params(jobProviderOptions); + const requiresSavedState = !isV2Job; const pngReportType = isV2Job ? 'pngV2' : 'png'; @@ -149,7 +150,7 @@ export const reportingScreenshotShareProvider = ({ uiSettings={uiSettings} reportType={pngReportType} objectId={objectId} - requiresSavedState={true} + requiresSavedState={requiresSavedState} getJobParams={getJobParams(apiClient, jobProviderOptions, pngReportType)} isDirty={isDirty} onClose={onClose} @@ -183,7 +184,7 @@ export const reportingScreenshotShareProvider = ({ uiSettings={uiSettings} reportType={pdfReportType} objectId={objectId} - requiresSavedState={true} + requiresSavedState={requiresSavedState} layoutOption={objectType === 'dashboard' ? 'print' : undefined} getJobParams={getJobParams(apiClient, jobProviderOptions, pdfReportType)} isDirty={isDirty} diff --git a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx index 11169dd2d2fb7..64f1ecddcbb41 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/reporting_panel_content.tsx @@ -104,6 +104,10 @@ class ReportingPanelContentUi extends Component { window.addEventListener('resize', this.setAbsoluteReportGenerationUrl); } + private isNotSaved = () => { + return this.props.objectId === undefined || this.props.objectId === ''; + }; + public render() { if ( this.props.requiresSavedState && @@ -226,10 +230,6 @@ class ReportingPanelContentUi extends Component { this.setState({ isStale: true }); }; - private isNotSaved = () => { - return this.props.objectId === undefined || this.props.objectId === ''; - }; - private setAbsoluteReportGenerationUrl = () => { if (!this.mounted) { return; diff --git a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts index 881b847f1180b..312eba7bd6380 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/screenshots.ts @@ -74,15 +74,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('Print PDF button', () => { - it('is not available if new', async () => { + it('is available if new', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPdfReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); await (await testSubjects.find('kibanaChrome')).clickMouseButton(); // close popover }); - it('becomes available when saved', async () => { + it('is available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PDF Dashboard'); await PageObjects.reporting.openPdfReportingPanel(); expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); @@ -109,15 +109,15 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('Print PNG button', () => { - it('is not available if new', async () => { + it('is available if new', async () => { await PageObjects.common.navigateToApp('dashboard'); await PageObjects.dashboard.clickNewDashboard(); await PageObjects.reporting.openPngReportingPanel(); - expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be('true'); + expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); await (await testSubjects.find('kibanaChrome')).clickMouseButton(); // close popover }); - it('becomes available when saved', async () => { + it('is available when saved', async () => { await PageObjects.dashboard.saveDashboard('My PNG Dash'); await PageObjects.reporting.openPngReportingPanel(); expect(await PageObjects.reporting.isGenerateReportButtonDisabled()).to.be(null); From 9c922a078c79390692a8787ba60d15e6fb02269b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ester=20Mart=C3=AD=20Vilaseca?= Date: Thu, 26 Aug 2021 11:03:21 +0200 Subject: [PATCH 04/11] [Stack monitoring] Add global state context and routeInit component (#109790) * Add global state to stack monitoring react app * Add type for state * Add some todos * Add route_init migration Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../application/global_state_context.tsx | 66 +++++++++++++++++ .../public/application/hooks/use_clusters.ts | 5 +- .../monitoring/public/application/index.tsx | 51 +++++++++---- .../public/application/pages/loading_page.tsx | 2 +- .../application/preserve_query_history.ts | 38 ++++++++++ .../public/application/route_init.tsx | 71 +++++++++++++++++++ .../plugins/monitoring/public/legacy_shims.ts | 4 ++ .../public/lib/get_cluster_from_clusters.d.ts | 12 ++++ x-pack/plugins/monitoring/public/url_state.ts | 27 ++++--- 9 files changed, 249 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/monitoring/public/application/global_state_context.tsx create mode 100644 x-pack/plugins/monitoring/public/application/preserve_query_history.ts create mode 100644 x-pack/plugins/monitoring/public/application/route_init.tsx create mode 100644 x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx new file mode 100644 index 0000000000000..e6e18e279bbad --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { createContext } from 'react'; +import { GlobalState } from '../url_state'; +import { MonitoringStartPluginDependencies } from '../types'; + +interface GlobalStateProviderProps { + query: MonitoringStartPluginDependencies['data']['query']; + toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; + children: React.ReactNode; +} + +interface State { + cluster_uuid?: string; +} + +export const GlobalStateContext = createContext({} as State); + +export const GlobalStateProvider = ({ query, toasts, children }: GlobalStateProviderProps) => { + // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed + const fakeAngularRootScope: Partial = { + $on: ( + name: string, + listener: (event: ng.IAngularEvent, ...args: any[]) => any + ): (() => void) => () => {}, + $applyAsync: () => {}, + }; + + const fakeAngularLocation: Partial = { + search: () => { + return {} as any; + }, + replace: () => { + return {} as any; + }, + }; + + const localState: { [key: string]: unknown } = {}; + const state = new GlobalState( + query, + toasts, + fakeAngularRootScope, + fakeAngularLocation, + localState + ); + + const initialState: any = state.getState(); + for (const key in initialState) { + if (!initialState.hasOwnProperty(key)) { + continue; + } + localState[key] = initialState[key]; + } + + localState.save = () => { + const newState = { ...localState }; + delete newState.save; + state.setState(newState); + }; + + return {children}; +}; diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts index 49f6464b2ce3e..b970d8c84b5b9 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -8,8 +8,7 @@ import { useState, useEffect } from 'react'; import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; import { STANDALONE_CLUSTER_CLUSTER_UUID } from '../../../common/constants'; -export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, ccs?: any) { - const clusterUuid = fetchAllClusters ? null : ''; +export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: string[]) { const { services } = useKibana<{ data: any }>(); const bounds = services.data?.query.timefilter.timefilter.getBounds(); @@ -43,7 +42,7 @@ export function useClusters(codePaths?: string[], fetchAllClusters?: boolean, cc } catch (err) { // TODO: handle errors } finally { - setLoaded(null); + setLoaded(true); } }; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index a0c9afd73f0ce..ed74d342f7a8f 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -8,10 +8,13 @@ import { CoreStart, AppMountParameters } from 'kibana/public'; import React from 'react'; import ReactDOM from 'react-dom'; -import { Route, Switch, Redirect, HashRouter } from 'react-router-dom'; +import { Route, Switch, Redirect, Router } from 'react-router-dom'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { LoadingPage } from './pages/loading_page'; import { MonitoringStartPluginDependencies } from '../types'; +import { GlobalStateProvider } from './global_state_context'; +import { createPreserveQueryHistory } from './preserve_query_history'; +import { RouteInit } from './route_init'; export const renderApp = ( core: CoreStart, @@ -29,21 +32,37 @@ const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; }> = ({ core, plugins }) => { + const history = createPreserveQueryHistory(); + return ( - - - - - - - - - + + + + + + + + + + + + ); }; @@ -59,3 +78,7 @@ const Home: React.FC<{}> = () => { const ClusterOverview: React.FC<{}> = () => { return
Cluster overview page
; }; + +const License: React.FC<{}> = () => { + return
License page
; +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx index 4bd09f73ac75a..d5c1bcf80c23e 100644 --- a/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/loading_page.tsx @@ -16,7 +16,7 @@ import { CODE_PATH_ELASTICSEARCH } from '../../../common/constants'; const CODE_PATHS = [CODE_PATH_ELASTICSEARCH]; export const LoadingPage = () => { - const { clusters, loaded } = useClusters(CODE_PATHS, true); + const { clusters, loaded } = useClusters(null, undefined, CODE_PATHS); const title = i18n.translate('xpack.monitoring.loading.pageTitle', { defaultMessage: 'Loading', }); diff --git a/x-pack/plugins/monitoring/public/application/preserve_query_history.ts b/x-pack/plugins/monitoring/public/application/preserve_query_history.ts new file mode 100644 index 0000000000000..9e7858cf6e849 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/preserve_query_history.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { History, createHashHistory, LocationDescriptor, LocationDescriptorObject } from 'history'; + +function preserveQueryParameters( + history: History, + location: LocationDescriptorObject +): LocationDescriptorObject { + location.search = history.location.search; + return location; +} + +function createLocationDescriptorObject( + location: LocationDescriptor, + state?: History.LocationState +): LocationDescriptorObject { + return typeof location === 'string' ? { pathname: location, state } : location; +} + +export function createPreserveQueryHistory() { + const history = createHashHistory({ hashType: 'slash' }); + const oldPush = history.push; + const oldReplace = history.replace; + history.push = (path: LocationDescriptor, state?: History.LocationState) => + oldPush.apply(history, [ + preserveQueryParameters(history, createLocationDescriptorObject(path, state)), + ]); + history.replace = (path: LocationDescriptor, state?: History.LocationState) => + oldReplace.apply(history, [ + preserveQueryParameters(history, createLocationDescriptorObject(path, state)), + ]); + return history; +} diff --git a/x-pack/plugins/monitoring/public/application/route_init.tsx b/x-pack/plugins/monitoring/public/application/route_init.tsx new file mode 100644 index 0000000000000..cf3b0c6646d0f --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/route_init.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React, { useContext } from 'react'; +import { Route, Redirect, useLocation } from 'react-router-dom'; +import { useClusters } from './hooks/use_clusters'; +import { GlobalStateContext } from './global_state_context'; +import { getClusterFromClusters } from '../lib/get_cluster_from_clusters'; + +interface RouteInitProps { + path: string; + component: React.ComponentType; + codePaths: string[]; + fetchAllClusters: boolean; + unsetGlobalState?: boolean; +} + +export const RouteInit: React.FC = ({ + path, + component, + codePaths, + fetchAllClusters, + unsetGlobalState = false, +}) => { + const globalState = useContext(GlobalStateContext); + const clusterUuid = fetchAllClusters ? null : globalState.cluster_uuid; + const location = useLocation(); + + const { clusters, loaded } = useClusters(clusterUuid, undefined, codePaths); + + // TODO: we will need this when setup mode is migrated + // const inSetupMode = isInSetupMode(); + + const cluster = getClusterFromClusters(clusters, globalState, unsetGlobalState); + + // TODO: check for setupMode too when the setup mode is migrated + if (loaded && !cluster) { + return ; + } + + if (loaded && cluster) { + // check if we need to redirect because of license problems + if ( + location.pathname !== 'license' && + location.pathname !== 'home' && + isExpired(cluster.license) + ) { + return ; + } + + // check if we need to redirect because of attempt at unsupported multi-cluster monitoring + const clusterSupported = cluster.isSupported || clusters.length === 1; + if (location.pathname !== 'home' && !clusterSupported) { + return ; + } + } + + return loaded ? : null; +}; + +const isExpired = (license: any): boolean => { + const { expiry_date_in_millis: expiryDateInMillis } = license; + + if (expiryDateInMillis !== undefined) { + return new Date().getTime() >= expiryDateInMillis; + } + return false; +}; diff --git a/x-pack/plugins/monitoring/public/legacy_shims.ts b/x-pack/plugins/monitoring/public/legacy_shims.ts index 1d897b710d7fa..fe754a965e3f1 100644 --- a/x-pack/plugins/monitoring/public/legacy_shims.ts +++ b/x-pack/plugins/monitoring/public/legacy_shims.ts @@ -147,4 +147,8 @@ export class Legacy { } return Legacy._shims; } + + public static isInitializated(): boolean { + return Boolean(Legacy._shims); + } } diff --git a/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts new file mode 100644 index 0000000000000..5a310c977efae --- /dev/null +++ b/x-pack/plugins/monitoring/public/lib/get_cluster_from_clusters.d.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const getClusterFromClusters: ( + clusters: any, + globalState: State, + unsetGlobalState: boolean +) => any; diff --git a/x-pack/plugins/monitoring/public/url_state.ts b/x-pack/plugins/monitoring/public/url_state.ts index f490654d579ae..25086411c65a3 100644 --- a/x-pack/plugins/monitoring/public/url_state.ts +++ b/x-pack/plugins/monitoring/public/url_state.ts @@ -57,6 +57,7 @@ export interface MonitoringAppStateTransitions { const GLOBAL_STATE_KEY = '_g'; const objectEquals = (objA: any, objB: any) => JSON.stringify(objA) === JSON.stringify(objB); +// TODO: clean all angular references after angular is removed export class GlobalState { private readonly stateSyncRef: ISyncStateRef; private readonly stateContainer: StateContainer< @@ -74,8 +75,8 @@ export class GlobalState { constructor( queryService: MonitoringStartPluginDependencies['data']['query'], toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts'], - rootScope: ng.IRootScopeService, - ngLocation: ng.ILocationService, + rootScope: Partial, + ngLocation: Partial, externalState: RawObject ) { this.timefilterRef = queryService.timefilter.timefilter; @@ -102,11 +103,16 @@ export class GlobalState { this.stateContainerChangeSub = this.stateContainer.state$.subscribe(() => { this.lastAssignedState = this.getState(); if (!this.stateContainer.get() && this.lastKnownGlobalState) { - ngLocation.search(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); + ngLocation.search?.(`${GLOBAL_STATE_KEY}=${this.lastKnownGlobalState}`).replace(); } - Legacy.shims.breadcrumbs.update(); + + // TODO: check if this is not needed after https://github.com/elastic/kibana/pull/109132 is merged + if (Legacy.isInitializated()) { + Legacy.shims.breadcrumbs.update(); + } + this.syncExternalState(externalState); - rootScope.$applyAsync(); + rootScope.$applyAsync?.(); }); this.syncQueryStateWithUrlManager = syncQueryStateWithUrl(queryService, this.stateStorage); @@ -114,7 +120,7 @@ export class GlobalState { this.startHashSync(rootScope, ngLocation); this.lastAssignedState = this.getState(); - rootScope.$on('$destroy', () => this.destroy()); + rootScope.$on?.('$destroy', () => this.destroy()); } private syncExternalState(externalState: { [key: string]: unknown }) { @@ -131,15 +137,18 @@ export class GlobalState { } } - private startHashSync(rootScope: ng.IRootScopeService, ngLocation: ng.ILocationService) { - rootScope.$on( + private startHashSync( + rootScope: Partial, + ngLocation: Partial + ) { + rootScope.$on?.( '$routeChangeStart', (_: { preventDefault: () => void }, newState: Route, oldState: Route) => { const currentGlobalState = oldState?.params?._g; const nextGlobalState = newState?.params?._g; if (!nextGlobalState && currentGlobalState && typeof currentGlobalState === 'string') { newState.params._g = currentGlobalState; - ngLocation.search(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); + ngLocation.search?.(`${GLOBAL_STATE_KEY}=${currentGlobalState}`).replace(); } this.lastKnownGlobalState = (nextGlobalState || currentGlobalState) as string; } From 652470b9005b306e11dfd93f7bb4ebd402be2ab6 Mon Sep 17 00:00:00 2001 From: Miriam <31922082+MiriamAparicio@users.noreply.github.com> Date: Thu, 26 Aug 2021 10:21:15 +0100 Subject: [PATCH 05/11] [APM] Show hostname in JVM view (#109651) * [APM] Show hostname in JVM view * [APM] delete no needed param * [APM] fix linting * [APM] changes after review * [APM] changes after review part deux * [APM] fix snapshot * [APM] improve guard on api response --- .../app/service_node_overview/index.tsx | 20 ++++++++++++++++++- .../__snapshots__/queries.test.ts.snap | 12 +++++++++++ .../apm/server/lib/service_nodes/index.ts | 14 +++++++++++++ 3 files changed, 45 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx index f2f1e983471b9..1158a671bfe0a 100644 --- a/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_node_overview/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { EuiToolTip } from '@elastic/eui'; +import { EuiToolTip, EuiIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; @@ -78,6 +78,12 @@ function ServiceNodeOverview() { {i18n.translate('xpack.apm.jvmsTable.nameColumnLabel', { defaultMessage: 'Name', })} + ), @@ -110,11 +116,20 @@ function ServiceNodeOverview() { ); }, }, + { + name: i18n.translate('xpack.apm.jvmsTable.hostName', { + defaultMessage: 'Host name', + }), + field: 'hostName', + sortable: true, + render: (_, { hostName }) => hostName ?? '', + }, { name: i18n.translate('xpack.apm.jvmsTable.cpuColumnLabel', { defaultMessage: 'CPU avg', }), field: 'cpu', + dataType: 'number', sortable: true, render: (_, { cpu }) => asPercent(cpu, 1), }, @@ -123,6 +138,7 @@ function ServiceNodeOverview() { defaultMessage: 'Heap memory avg', }), field: 'heapMemory', + dataType: 'number', sortable: true, render: asDynamicBytes, }, @@ -131,6 +147,7 @@ function ServiceNodeOverview() { defaultMessage: 'Non-heap memory avg', }), field: 'nonHeapMemory', + dataType: 'number', sortable: true, render: asDynamicBytes, }, @@ -139,6 +156,7 @@ function ServiceNodeOverview() { defaultMessage: 'Thread count max', }), field: 'threadCount', + dataType: 'number', sortable: true, render: asInteger, }, diff --git a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap index 8e47b7298cc33..3550b9a602eda 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/service_nodes/__snapshots__/queries.test.ts.snap @@ -141,6 +141,18 @@ Object { "field": "jvm.memory.heap.used", }, }, + "latest": Object { + "top_metrics": Object { + "metrics": Array [ + Object { + "field": "host.hostname", + }, + ], + "sort": Object { + "@timestamp": "desc", + }, + }, + }, "nonHeapMemory": Object { "avg": Object { "field": "jvm.memory.non_heap.used", diff --git a/x-pack/plugins/apm/server/lib/service_nodes/index.ts b/x-pack/plugins/apm/server/lib/service_nodes/index.ts index 4eb6abddd81a6..77bd646f4da60 100644 --- a/x-pack/plugins/apm/server/lib/service_nodes/index.ts +++ b/x-pack/plugins/apm/server/lib/service_nodes/index.ts @@ -10,8 +10,10 @@ import { METRIC_JAVA_NON_HEAP_MEMORY_USED, METRIC_JAVA_THREAD_COUNT, METRIC_PROCESS_CPU_PERCENT, + HOST_NAME, } from '../../../common/elasticsearch_fieldnames'; import { SERVICE_NODE_NAME_MISSING } from '../../../common/service_nodes'; +import { asMutableArray } from '../../../common/utils/as_mutable_array'; import { getServiceNodesProjection } from '../../projections/service_nodes'; import { mergeProjection } from '../../projections/util/merge_projection'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; @@ -46,6 +48,14 @@ const getServiceNodes = async ({ missing: SERVICE_NODE_NAME_MISSING, }, aggs: { + latest: { + top_metrics: { + metrics: asMutableArray([{ field: HOST_NAME }] as const), + sort: { + '@timestamp': 'desc', + }, + }, + }, cpu: { avg: { field: METRIC_PROCESS_CPU_PERCENT, @@ -83,6 +93,10 @@ const getServiceNodes = async ({ name: bucket.key as string, cpu: bucket.cpu.value, heapMemory: bucket.heapMemory.value, + hostName: bucket.latest.top?.[0]?.metrics?.['host.hostname'] as + | string + | null + | undefined, nonHeapMemory: bucket.nonHeapMemory.value, threadCount: bucket.threadCount.value, })) From dc07f4d00aee2c11f7bd3c7a87f9507acd586a0c Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Thu, 26 Aug 2021 11:30:01 +0200 Subject: [PATCH 06/11] [Discover] Fix performance regression in sidebar (#109999) --- .../sidebar/discover_field.test.tsx | 14 +++++ .../components/sidebar/discover_field.tsx | 59 ++++++++++--------- .../discover_sidebar_responsive.test.tsx | 32 ++++++++++ .../sidebar/discover_sidebar_responsive.tsx | 13 ++-- 4 files changed, 86 insertions(+), 32 deletions(-) diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx index d0f343a641717..1dfc14d6c20b9 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.test.tsx @@ -15,6 +15,11 @@ import { IndexPatternField } from '../../../../../../../data/public'; import { stubIndexPattern } from '../../../../../../../data/common/stubs'; jest.mock('../../../../../kibana_services', () => ({ + getUiActions: jest.fn(() => { + return { + getTriggerCompatibleActions: jest.fn(() => []), + }; + }), getServices: () => ({ history: () => ({ location: { @@ -120,4 +125,13 @@ describe('discover sidebar field', function () { const dscField = findTestSubject(comp, 'field-troubled_field-showDetails'); expect(dscField.find('.kbnFieldButton__infoIcon').length).toEqual(1); }); + it('should not execute getDetails when rendered, since it can be expensive', function () { + const { props } = getComponent({}); + expect(props.getDetails.mock.calls.length).toEqual(0); + }); + it('should execute getDetails when show details is requested', function () { + const { props, comp } = getComponent({}); + findTestSubject(comp, 'field-bytes-showDetails').simulate('click'); + expect(props.getDetails.mock.calls.length).toEqual(1); + }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx index 301866c762fbd..707514073e23e 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_field.tsx @@ -358,7 +358,36 @@ function DiscoverFieldComponent({ ); - const details = getDetails(field); + const renderPopover = () => { + const details = getDetails(field); + return ( + <> + + {multiFields && ( + <> + + + + )} + + + ); + }; return ( - {infoIsOpen && ( - <> - - {multiFields && ( - <> - - - - )} - - - )} + {infoIsOpen && renderPopover()} ); } diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx index c7395c42bb2f1..fc1c09ec8c829 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.test.tsx @@ -12,6 +12,7 @@ import { ReactWrapper } from 'enzyme'; import { findTestSubject } from '@elastic/eui/lib/test'; // @ts-expect-error import realHits from '../../../../../__fixtures__/real_hits.js'; +import { act } from 'react-dom/test-utils'; import { mountWithIntl } from '@kbn/test/jest'; import React from 'react'; import { IndexPatternAttributes } from '../../../../../../../data/common'; @@ -49,10 +50,24 @@ const mockServices = ({ }, } as unknown) as DiscoverServices; +const mockfieldCounts: Record = {}; +const mockCalcFieldCounts = jest.fn(() => { + return mockfieldCounts; +}); + jest.mock('../../../../../kibana_services', () => ({ + getUiActions: jest.fn(() => { + return { + getTriggerCompatibleActions: jest.fn(() => []), + }; + }), getServices: () => mockServices, })); +jest.mock('../../utils/calc_field_counts', () => ({ + calcFieldCounts: () => mockCalcFieldCounts(), +})); + function getCompProps(): DiscoverSidebarResponsiveProps { const indexPattern = stubLogstashIndexPattern; @@ -67,6 +82,11 @@ function getCompProps(): DiscoverSidebarResponsiveProps { { id: '2', attributes: { title: 'c' } } as SavedObject, ]; + for (const hit of hits) { + for (const key of Object.keys(indexPattern.flattenHit(hit))) { + mockfieldCounts[key] = (mockfieldCounts[key] || 0) + 1; + } + } return { columns: ['extension'], documents$: new BehaviorSubject({ @@ -102,6 +122,7 @@ describe('discover responsive sidebar', function () { expect(popular.children().length).toBe(1); expect(unpopular.children().length).toBe(7); expect(selected.children().length).toBe(1); + expect(mockCalcFieldCounts.mock.calls.length).toBe(1); }); it('should allow selecting fields', function () { findTestSubject(comp, 'fieldToggle-bytes').simulate('click'); @@ -116,4 +137,15 @@ describe('discover responsive sidebar', function () { findTestSubject(comp, 'plus-extension-gif').simulate('click'); expect(props.onAddFilter).toHaveBeenCalled(); }); + it('should allow filtering by string, and calcFieldCount should just be executed once', function () { + expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(7); + act(() => { + findTestSubject(comp, 'fieldFilterSearchInput').simulate('change', { + target: { value: 'abc' }, + }); + }); + comp.update(); + expect(findTestSubject(comp, 'fieldList-unpopular').children().length).toBe(4); + expect(mockCalcFieldCounts.mock.calls.length).toBe(1); + }); }); diff --git a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx index bbc2328e057d3..7533a54ade405 100644 --- a/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx +++ b/src/plugins/discover/public/application/apps/main/components/sidebar/discover_sidebar_responsive.tsx @@ -120,9 +120,14 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) * needed for merging new with old field counts, high likely legacy, but kept this behavior * because not 100% sure in this case */ - const fieldCounts = useRef>( - calcFieldCounts({}, props.documents$.getValue().result, props.selectedIndexPattern) - ); + const fieldCounts = useRef | null>(null); + if (fieldCounts.current === null) { + fieldCounts.current = calcFieldCounts( + {}, + props.documents$.getValue().result, + props.selectedIndexPattern + ); + } const [documentState, setDocumentState] = useState(props.documents$.getValue()); useEffect(() => { @@ -130,7 +135,7 @@ export function DiscoverSidebarResponsive(props: DiscoverSidebarResponsiveProps) if (next.fetchStatus !== documentState.fetchStatus) { if (next.result) { fieldCounts.current = calcFieldCounts( - next.result.length ? fieldCounts.current : {}, + next.result.length && fieldCounts.current ? fieldCounts.current : {}, next.result, props.selectedIndexPattern! ); From ae17fe69ddd528697f87c215f1fb72cc73722446 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 26 Aug 2021 11:41:37 +0200 Subject: [PATCH 07/11] Update app services code owners (#110084) --- .github/CODEOWNERS | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 61b383f4e1ca5..74a206ea98e05 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -57,6 +57,8 @@ /src/plugins/data/ @elastic/kibana-app-services /src/plugins/embeddable/ @elastic/kibana-app-services /src/plugins/expressions/ @elastic/kibana-app-services +/src/plugins/field_formats/ @elastic/kibana-app-services +/src/plugins/index_pattern_editor/ @elastic/kibana-app-services /src/plugins/inspector/ @elastic/kibana-app-services /src/plugins/kibana_react/ @elastic/kibana-app-services /src/plugins/kibana_react/public/code_editor @elastic/kibana-presentation From c433893473b872b06ad52dc24cad229783769a0f Mon Sep 17 00:00:00 2001 From: Ashokaditya Date: Thu, 26 Aug 2021 12:03:39 +0200 Subject: [PATCH 08/11] Remove onClear on date range picker inputs (#110035) fixes elastic/kibana/issues/108864 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../activity_log_date_range_picker/index.tsx | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx index 60adbf3060f2d..e921078539303 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/activity_log_date_range_picker/index.tsx @@ -33,22 +33,6 @@ export const DateRangePicker = memo(() => { getActivityLogDataPaging ); - const onClear = useCallback( - ({ clearStart = false, clearEnd = false }: { clearStart?: boolean; clearEnd?: boolean }) => { - dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', - payload: { - disabled: false, - page, - pageSize, - startDate: clearStart ? undefined : startDate, - endDate: clearEnd ? undefined : endDate, - }, - }); - }, - [dispatch, endDate, startDate, page, pageSize] - ); - const onChangeStartDate = useCallback( (date) => { dispatch({ @@ -95,7 +79,6 @@ export const DateRangePicker = memo(() => { endDate={endDate ? moment(endDate) : undefined} isInvalid={isInvalidDateRange} onChange={onChangeStartDate} - onClear={() => onClear({ clearStart: true })} placeholderText={i18.ACTIVITY_LOG.datePicker.startDate} selected={startDate ? moment(startDate) : undefined} showTimeSelect @@ -108,7 +91,6 @@ export const DateRangePicker = memo(() => { endDate={endDate ? moment(endDate) : undefined} isInvalid={isInvalidDateRange} onChange={onChangeEndDate} - onClear={() => onClear({ clearEnd: true })} placeholderText={i18.ACTIVITY_LOG.datePicker.endDate} selected={endDate ? moment(endDate) : undefined} showTimeSelect From 1af200b5e04125c4ccb3eab621c625534f14618d Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 26 Aug 2021 12:26:02 +0200 Subject: [PATCH 09/11] [DataViews] Fix checking remote clusters in empty state (#110054) --- .../empty_prompts/empty_prompts.tsx | 21 ++++++------ .../index_pattern_editor_flyout_content.tsx | 32 ++++--------------- 2 files changed, 17 insertions(+), 36 deletions(-) diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx index 2f1631694e952..696194d8113c7 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React, { useState, useCallback, FC } from 'react'; +import React, { useState, FC, useEffect } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { useKibana } from '../../shared_imports'; @@ -47,6 +47,8 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo } = useKibana(); const [remoteClustersExist, setRemoteClustersExist] = useState(false); + const [hasCheckedRemoteClusters, setHasCheckedRemoteClusters] = useState(false); + const [goToForm, setGoToForm] = useState(false); const hasDataIndices = allSources.some(isUserDataIndex); @@ -54,9 +56,10 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo indexPatternService.hasUserIndexPattern().catch(() => true) ); - useCallback(() => { - let isMounted = true; - if (!hasDataIndices) + useEffect(() => { + if (!hasDataIndices && !hasCheckedRemoteClusters) { + setHasCheckedRemoteClusters(true); + getIndices({ http, isRollupIndex: () => false, @@ -64,14 +67,10 @@ export const EmptyPrompts: FC = ({ allSources, onCancel, children, loadSo showAllIndices: false, searchClient, }).then((dataSources) => { - if (isMounted) { - setRemoteClustersExist(!!dataSources.filter(removeAliases).length); - } + setRemoteClustersExist(!!dataSources.filter(removeAliases).length); }); - return () => { - isMounted = false; - }; - }, [http, hasDataIndices, searchClient]); + } + }, [http, hasDataIndices, searchClient, hasCheckedRemoteClusters]); if (hasUserIndexPattern.loading) return null; // return null to prevent UI flickering while loading diff --git a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx index 0eed74053f667..c4d8ed11fe7c2 100644 --- a/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx +++ b/src/plugins/index_pattern_editor/public/components/index_pattern_editor_flyout_content.tsx @@ -69,7 +69,6 @@ const IndexPatternEditorFlyoutContentComponent = ({ defaultTypeIsRollup, requireTimestampField = false, }: Props) => { - const isMounted = useRef(false); const { services: { http, indexPatternService, uiSettings, searchClient }, } = useKibana(); @@ -156,19 +155,14 @@ const IndexPatternEditorFlyoutContentComponent = ({ // loading list of index patterns useEffect(() => { - isMounted.current = true; loadSources(); const getTitles = async () => { const indexPatternTitles = await indexPatternService.getTitles(); - if (isMounted.current) { - setExistingIndexPatterns(indexPatternTitles); - setIsLoadingIndexPatterns(false); - } + + setExistingIndexPatterns(indexPatternTitles); + setIsLoadingIndexPatterns(false); }; getTitles(); - return () => { - isMounted.current = false; - }; }, [http, indexPatternService, loadSources]); // loading rollup info @@ -176,10 +170,8 @@ const IndexPatternEditorFlyoutContentComponent = ({ const getRollups = async () => { try { const response = await http.get('/api/rollup/indices'); - if (isMounted.current) { - if (response) { - setRollupIndicesCapabilities(response); - } + if (response) { + setRollupIndicesCapabilities(response); } } catch (e) { // Silently swallow failure responses such as expired trials @@ -214,10 +206,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ ); timestampOptions = extractTimeFields(fields, requireTimestampField); } - if ( - isMounted.current && - currentLoadingTimestampFieldsIdx === currentLoadingTimestampFieldsRef.current - ) { + if (currentLoadingTimestampFieldsIdx === currentLoadingTimestampFieldsRef.current) { setIsLoadingTimestampFields(false); setTimestampFieldOptions(timestampOptions); } @@ -266,10 +255,7 @@ const IndexPatternEditorFlyoutContentComponent = ({ exactMatched: [], }; - if ( - currentLoadingMatchedIndicesIdx === currentLoadingMatchedIndicesRef.current && - isMounted.current - ) { + if (currentLoadingMatchedIndicesIdx === currentLoadingMatchedIndicesRef.current) { // we are still interested in this result if (type === INDEX_PATTERN_TYPE.ROLLUP) { const rollupIndices = exactMatched.filter((index) => isRollupIndex(index.name)); @@ -291,10 +277,6 @@ const IndexPatternEditorFlyoutContentComponent = ({ [http, allowHidden, allSources, type, rollupIndicesCapabilities, searchClient, isLoadingSources] ); - useEffect(() => { - reloadMatchedIndices(title); - }, [allowHidden, reloadMatchedIndices, title]); - const onTypeChange = useCallback( (newType) => { form.setFieldValue('title', ''); From 44faefb71517584f5bf2e19f45192a31eaa6bdd1 Mon Sep 17 00:00:00 2001 From: Anton Dosov Date: Thu, 26 Aug 2021 12:33:16 +0200 Subject: [PATCH 10/11] [DataViews] filter apm- sources when deciding which empty state to show(#110094) --- .../empty_prompts/empty_prompts.test.tsx | 166 ++++++++++++++++++ .../empty_prompts/empty_prompts.tsx | 3 + 2 files changed, 169 insertions(+) diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx index 03902792371e7..ae395d7e2d335 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.test.tsx @@ -95,4 +95,170 @@ describe('isUserDataIndex', () => { }; expect(isUserDataIndex(fleetAssetIndex)).toBe(false); }); + + test('apm sources are not user sources', () => { + const apmSources: MatchedItem[] = [ + { + name: 'apm-7.14.1-error', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-error', + indices: ['apm-7.14.1-error-000001'], + }, + }, + { + name: 'apm-7.14.1-error-000001', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-error-000001', + aliases: ['apm-7.14.1-error'], + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: 'apm-7.14.1-metric', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-metric', + indices: ['apm-7.14.1-metric-000001'], + }, + }, + { + name: 'apm-7.14.1-metric-000001', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-metric-000001', + aliases: ['apm-7.14.1-metric'], + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: 'apm-7.14.1-onboarding-2021.08.25', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-onboarding-2021.08.25', + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: 'apm-7.14.1-profile', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-profile', + indices: ['apm-7.14.1-profile-000001'], + }, + }, + { + name: 'apm-7.14.1-profile-000001', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-profile-000001', + aliases: ['apm-7.14.1-profile'], + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: 'apm-7.14.1-span', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-span', + indices: ['apm-7.14.1-span-000001'], + }, + }, + { + name: 'apm-7.14.1-span-000001', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-span-000001', + aliases: ['apm-7.14.1-span'], + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + { + name: 'apm-7.14.1-transaction', + tags: [ + { + key: 'alias', + name: 'Alias', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-transaction', + indices: ['apm-7.14.1-transaction-000001'], + }, + }, + { + name: 'apm-7.14.1-transaction-000001', + tags: [ + { + key: 'index', + name: 'Index', + color: 'default', + }, + ], + item: { + name: 'apm-7.14.1-transaction-000001', + aliases: ['apm-7.14.1-transaction'], + attributes: [ResolveIndexResponseItemIndexAttrs.OPEN], + }, + }, + ]; + + expect(apmSources.some(isUserDataIndex)).toBe(false); + }); }); diff --git a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx index 696194d8113c7..246466680c86e 100644 --- a/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx +++ b/src/plugins/index_pattern_editor/public/components/empty_prompts/empty_prompts.tsx @@ -38,6 +38,9 @@ export function isUserDataIndex(source: MatchedItem) { if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_DATA_STREAM_TO_IGNORE) return false; if (source.name === FLEET_ASSETS_TO_IGNORE.METRICS_ENDPOINT_INDEX_TO_IGNORE) return false; + // filter out empty sources created by apm server + if (source.name.startsWith('apm-')) return false; + return true; } From c113c237f8868c78ddc15ce35525cdba0658222c Mon Sep 17 00:00:00 2001 From: Esteban Beltran Date: Thu, 26 Aug 2021 13:44:36 +0200 Subject: [PATCH 11/11] [Security solution] Correct memory protections label to memory manipulation protection (#110179) --- .../pages/policy/view/policy_forms/protections/memory.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx index 905fef43db5a7..0c50446f255c0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx @@ -28,13 +28,13 @@ export const MemoryProtection = React.memo(() => { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.memory', { - defaultMessage: 'Memory protections', + defaultMessage: 'Memory Manipulation Protection', } ); return (