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
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/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!
);
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 2f1631694e952..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
@@ -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';
@@ -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;
}
@@ -47,6 +50,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 +59,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 +70,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', '');
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,
}))
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/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');
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;
}
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/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`);
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
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 (
{
- 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);