{savedDashboard && dashboardStateManager && dashboardContainer && viewMode && (
@@ -242,7 +286,7 @@ export function DashboardApp({
// The user can still request a reload in the query bar, even if the
// query is the same, and in that case, we have to explicitly ask for
// a reload, since no state changes will cause it.
- setLastReloadTime(() => new Date().getTime());
+ triggerRefresh$.next({ force: true });
}
}}
/>
diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx
index 9141f2e592fd7..5206c76f50be2 100644
--- a/src/plugins/dashboard/public/application/dashboard_router.tsx
+++ b/src/plugins/dashboard/public/application/dashboard_router.tsx
@@ -104,6 +104,7 @@ export async function mountApp({
mapsCapabilities: { save: Boolean(coreStart.application.capabilities.maps?.save) },
createShortUrl: Boolean(coreStart.application.capabilities.dashboard.createShortUrl),
visualizeCapabilities: { save: Boolean(coreStart.application.capabilities.visualize?.save) },
+ storeSearchSession: Boolean(coreStart.application.capabilities.dashboard.storeSearchSession),
},
};
diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts
index 90706a11b8ce2..c52bd1b4d47b8 100644
--- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts
+++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts
@@ -72,7 +72,7 @@ export class DashboardStateManager {
>;
private readonly stateContainerChangeSub: Subscription;
private readonly STATE_STORAGE_KEY = '_a';
- private readonly kbnUrlStateStorage: IKbnUrlStateStorage;
+ public readonly kbnUrlStateStorage: IKbnUrlStateStorage;
private readonly stateSyncRef: ISyncStateRef;
private readonly history: History;
private readonly usageCollection: UsageCollectionSetup | undefined;
@@ -596,7 +596,7 @@ export class DashboardStateManager {
this.toUrlState(this.stateContainer.get())
);
// immediately forces scheduled updates and changes location
- return this.kbnUrlStateStorage.flush({ replace });
+ return !!this.kbnUrlStateStorage.kbnUrlControls.flush(replace);
}
// TODO: find nicer solution for this
diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
index fa520ec22497b..780eb1bad8c2b 100644
--- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
+++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx
@@ -89,7 +89,7 @@ export interface InheritedChildInput extends IndexSignature {
export type DashboardReactContextValue = KibanaReactContextValue
;
export type DashboardReactContext = KibanaReactContext;
-const defaultCapabilities = {
+const defaultCapabilities: DashboardCapabilities = {
show: false,
createNew: false,
saveQuery: false,
@@ -97,6 +97,7 @@ const defaultCapabilities = {
hideWriteControls: true,
mapsCapabilities: { save: false },
visualizeCapabilities: { save: false },
+ storeSearchSession: true,
};
export class DashboardContainer extends Container {
diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
index a4044e8668e59..93fbb50950850 100644
--- a/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
+++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_state_manager.ts
@@ -16,6 +16,7 @@ import { useKibana } from '../../services/kibana_react';
import {
connectToQueryState,
esFilters,
+ noSearchSessionStorageCapabilityMessage,
QueryState,
syncQueryStateWithUrl,
} from '../../services/data';
@@ -159,13 +160,22 @@ export const useDashboardStateManager = (
stateManager.isNew()
);
- searchSession.setSearchSessionInfoProvider(
+ searchSession.enableStorage(
createSessionRestorationDataProvider({
data: dataPlugin,
getDashboardTitle: () => dashboardTitle,
getDashboardId: () => savedDashboard?.id || '',
getAppState: () => stateManager.getAppState(),
- })
+ }),
+ {
+ isDisabled: () =>
+ dashboardCapabilities.storeSearchSession
+ ? { disabled: false }
+ : {
+ disabled: true,
+ reasonText: noSearchSessionStorageCapabilityMessage,
+ },
+ }
);
setDashboardStateManager(stateManager);
@@ -192,6 +202,7 @@ export const useDashboardStateManager = (
toasts,
uiSettings,
usageCollection,
+ dashboardCapabilities.storeSearchSession,
]);
return { dashboardStateManager, viewMode, setViewMode };
diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
index bce8a661634f6..faec6b4f6f24b 100644
--- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
+++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap
@@ -4,10 +4,16 @@ exports[`after fetch When given a title that matches multiple dashboards, filter
- getTableColumns((id) => redirectTo({ destination: 'dashboard', id }), savedObjectsTagging),
- [savedObjectsTagging, redirectTo]
+ getTableColumns(
+ core.application,
+ kbnUrlStateStorage,
+ core.uiSettings.get('state:storeInSessionStorage'),
+ savedObjectsTagging
+ ),
+ [core.application, core.uiSettings, kbnUrlStateStorage, savedObjectsTagging]
);
const noItemsFragment = useMemo(
@@ -99,7 +103,6 @@ export const DashboardListing = ({
(filter: string) => {
let searchTerm = filter;
let references: SavedObjectsFindOptionsReference[] | undefined;
-
if (savedObjectsTagging) {
const parsed = savedObjectsTagging.ui.parseSearchQuery(filter, {
useName: true,
@@ -164,7 +167,9 @@ export const DashboardListing = ({
};
const getTableColumns = (
- redirectTo: (id?: string) => void,
+ application: ApplicationStart,
+ kbnUrlStateStorage: IKbnUrlStateStorage,
+ useHash: boolean,
savedObjectsTagging?: SavedObjectsTaggingApi
) => {
return [
@@ -172,9 +177,15 @@ const getTableColumns = (
field: 'title',
name: dashboardListingTable.getTitleColumnName(),
sortable: true,
- render: (field: string, record: { id: string; title: string }) => (
+ render: (field: string, record: { id: string; title: string; timeRestore: boolean }) => (
redirectTo(record.id)}
+ href={getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ useHash,
+ record.id,
+ record.timeRestore
+ )}
data-test-subj={`dashboardListingTitleLink-${record.title.split(' ').join('-')}`}
>
{field}
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts
new file mode 100644
index 0000000000000..6dbc76803af90
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.test.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { getDashboardListItemLink } from './get_dashboard_list_item_link';
+import { ApplicationStart } from 'kibana/public';
+import { esFilters } from '../../../../data/public';
+import { createHashHistory } from 'history';
+import { createKbnUrlStateStorage } from '../../../../kibana_utils/public';
+import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
+
+const DASHBOARD_ID = '13823000-99b9-11ea-9eb6-d9e8adceb647';
+
+const application = ({
+ getUrlForApp: jest.fn((appId: string, options?: { path?: string; absolute?: boolean }) => {
+ return `/app/${appId}${options?.path}`;
+ }),
+} as unknown) as ApplicationStart;
+
+const history = createHashHistory();
+const kbnUrlStateStorage = createKbnUrlStateStorage({
+ history,
+ useHash: false,
+});
+kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, { time: { from: 'now-7d', to: 'now' } });
+
+describe('listing dashboard link', () => {
+ test('creates a link to a dashboard without the timerange query if time is saved on the dashboard', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ true
+ );
+ expect(url).toMatchInlineSnapshot(`"/app/dashboards#/view/${DASHBOARD_ID}?_g=()"`);
+ });
+
+ test('creates a link to a dashboard with the timerange query if time is not saved on the dashboard', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:now-7d,to:now))"`
+ );
+ });
+});
+
+describe('when global time changes', () => {
+ beforeEach(() => {
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ time: {
+ from: '2021-01-05T11:45:53.375Z',
+ to: '2021-01-21T11:46:00.990Z',
+ },
+ });
+ });
+
+ test('propagates the correct time on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(time:(from:'2021-01-05T11:45:53.375Z',to:'2021-01-21T11:46:00.990Z'))"`
+ );
+ });
+});
+
+describe('when global refreshInterval changes', () => {
+ beforeEach(() => {
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ refreshInterval: { pause: false, value: 300 },
+ });
+ });
+
+ test('propagates the refreshInterval on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(refreshInterval:(pause:!f,value:300))"`
+ );
+ });
+});
+
+describe('when global filters change', () => {
+ beforeEach(() => {
+ const filters = [
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'q1' },
+ },
+ {
+ meta: {
+ alias: null,
+ disabled: false,
+ negate: false,
+ },
+ query: { query: 'q1' },
+ $state: {
+ store: esFilters.FilterStateStore.GLOBAL_STATE,
+ },
+ },
+ ];
+ kbnUrlStateStorage.set(GLOBAL_STATE_STORAGE_KEY, {
+ filters,
+ });
+ });
+
+ test('propagates the filters on the query', async () => {
+ const url = getDashboardListItemLink(
+ application,
+ kbnUrlStateStorage,
+ false,
+ DASHBOARD_ID,
+ false
+ );
+ expect(url).toMatchInlineSnapshot(
+ `"/app/dashboards#/view/${DASHBOARD_ID}?_g=(filters:!((meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1)),('$state':(store:globalState),meta:(alias:!n,disabled:!f,negate:!f),query:(query:q1))))"`
+ );
+ });
+});
diff --git a/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
new file mode 100644
index 0000000000000..d14638b9e231f
--- /dev/null
+++ b/src/plugins/dashboard/public/application/listing/get_dashboard_list_item_link.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+import { ApplicationStart } from 'kibana/public';
+import { QueryState } from '../../../../data/public';
+import { setStateToKbnUrl } from '../../../../kibana_utils/public';
+import { createDashboardEditUrl, DashboardConstants } from '../../dashboard_constants';
+import { GLOBAL_STATE_STORAGE_KEY } from '../../url_generator';
+import { IKbnUrlStateStorage } from '../../services/kibana_utils';
+
+export const getDashboardListItemLink = (
+ application: ApplicationStart,
+ kbnUrlStateStorage: IKbnUrlStateStorage,
+ useHash: boolean,
+ id: string,
+ timeRestore: boolean
+) => {
+ let url = application.getUrlForApp(DashboardConstants.DASHBOARDS_ID, {
+ path: `#${createDashboardEditUrl(id)}`,
+ });
+ const globalStateInUrl = kbnUrlStateStorage.get(GLOBAL_STATE_STORAGE_KEY) || {};
+
+ if (timeRestore) {
+ delete globalStateInUrl.time;
+ delete globalStateInUrl.refreshInterval;
+ }
+ url = setStateToKbnUrl(GLOBAL_STATE_STORAGE_KEY, globalStateInUrl, { useHash }, url);
+ return url;
+};
diff --git a/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
new file mode 100644
index 0000000000000..d4703d14627a4
--- /dev/null
+++ b/src/plugins/dashboard/public/application/top_nav/show_share_modal.test.tsx
@@ -0,0 +1,51 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { Capabilities } from 'src/core/public';
+import { showPublicUrlSwitch } from './show_share_modal';
+
+describe('showPublicUrlSwitch', () => {
+ test('returns false if "dashboard" app is not available', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns false if "dashboard" app is not accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ dashboard: {
+ show: false,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(false);
+ });
+
+ test('returns true if "dashboard" app is not available an accessible', () => {
+ const anonymousUserCapabilities: Capabilities = {
+ catalogue: {},
+ management: {},
+ navLinks: {},
+ dashboard: {
+ show: true,
+ },
+ };
+ const result = showPublicUrlSwitch(anonymousUserCapabilities);
+
+ expect(result).toBe(true);
+ });
+});
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 660e7635eb99d..fe4f8ea411289 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,6 +6,7 @@
* Public License, v 1.
*/
+import { Capabilities } from 'src/core/public';
import { EuiCheckboxGroup } from '@elastic/eui';
import React from 'react';
import { ReactElement, useState } from 'react';
@@ -27,6 +28,14 @@ interface ShowShareModalProps {
dashboardStateManager: DashboardStateManager;
}
+export const showPublicUrlSwitch = (anonymousUserCapabilities: Capabilities) => {
+ if (!anonymousUserCapabilities.dashboard) return false;
+
+ const dashboard = (anonymousUserCapabilities.dashboard as unknown) as DashboardCapabilities;
+
+ return !!dashboard.show;
+};
+
export function ShowShareModal({
share,
anchorElement,
@@ -94,7 +103,7 @@ export function ShowShareModal({
share.toggleShareContextMenu({
anchorElement,
allowEmbed: true,
- allowShortUrl: !dashboardCapabilities.hideWriteControls || dashboardCapabilities.createShortUrl,
+ allowShortUrl: dashboardCapabilities.createShortUrl,
shareableUrl: setStateToKbnUrl(
'_a',
dashboardStateManager.getAppState(),
@@ -113,5 +122,6 @@ export function ShowShareModal({
component: EmbedUrlParamExtension,
},
],
+ showPublicUrlSwitch,
});
}
diff --git a/src/plugins/dashboard/public/application/types.ts b/src/plugins/dashboard/public/application/types.ts
index 61e16beed61f4..e4f9388a919d1 100644
--- a/src/plugins/dashboard/public/application/types.ts
+++ b/src/plugins/dashboard/public/application/types.ts
@@ -55,6 +55,7 @@ export interface DashboardCapabilities {
saveQuery: boolean;
createNew: boolean;
show: boolean;
+ storeSearchSession: boolean;
}
export interface DashboardAppServices {
diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts
index ff3e2ebc89a41..fc8c44e8d1870 100644
--- a/src/plugins/data/public/index.ts
+++ b/src/plugins/data/public/index.ts
@@ -384,6 +384,7 @@ export {
SearchTimeoutError,
TimeoutErrorMode,
PainlessError,
+ noSearchSessionStorageCapabilityMessage,
} from './search';
export type {
diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md
index 28997de4517e7..9e493f46b0781 100644
--- a/src/plugins/data/public/public.api.md
+++ b/src/plugins/data/public/public.api.md
@@ -218,11 +218,11 @@ export class AggConfigs {
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
- }, "enabled" | "schema" | "id" | "params"> & Pick<{
+ }, "schema" | "enabled" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
- }, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined, opts: AggConfigsOptions);
+ }, never>, "schema" | "type" | "enabled" | "id" | "params">[] | undefined, opts: AggConfigsOptions);
// (undocumented)
aggs: IAggConfig[];
// (undocumented)
@@ -1834,6 +1834,11 @@ export enum METRIC_TYPES {
TOP_HITS = "top_hits"
}
+// Warning: (ae-missing-release-tag) "noSearchSessionStorageCapabilityMessage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
+//
+// @public
+export const noSearchSessionStorageCapabilityMessage: string;
+
// Warning: (ae-missing-release-tag) "OptionedParamType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
@@ -2235,8 +2240,8 @@ export const search: {
// Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
-export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "indexPatterns" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
- WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
+export const SearchBar: React.ComponentClass, "query" | "isLoading" | "indexPatterns" | "filters" | "dataTestSubj" | "refreshInterval" | "screenTitle" | "onRefresh" | "onRefreshChange" | "showQueryInput" | "showDatePicker" | "showAutoRefreshOnly" | "dateRangeFrom" | "dateRangeTo" | "isRefreshPaused" | "customSubmitButton" | "timeHistory" | "indicateNoData" | "onFiltersUpdated" | "trackUiMetric" | "savedQuery" | "showSaveQuery" | "onClearSavedQuery" | "showQueryBar" | "showFilterBar" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated">, any> & {
+ WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>;
};
// Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts
@@ -2629,21 +2634,21 @@ export const UI_SETTINGS: {
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "validateIndexPattern" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:399:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:401:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:411:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:418:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:422:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
-// src/plugins/data/public/index.ts:426:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:400:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:402:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:412:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:413:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:414:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:415:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:419:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:420:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:423:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:424:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts
+// src/plugins/data/public/index.ts:427:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/query/state_sync/connect_to_query_state.ts:34:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts
// src/plugins/data/public/search/session/session_service.ts:41:5 - (ae-forgotten-export) The symbol "UrlGeneratorStateMapping" needs to be exported by the entry point index.d.ts
diff --git a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
index 19035eb8a9328..991f3063cac0a 100644
--- a/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
+++ b/src/plugins/data/public/query/state_sync/sync_state_with_url.test.ts
@@ -90,7 +90,7 @@ describe('sync_query_state_with_url', () => {
test('url is actually changed when data in services changes', () => {
const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage);
filterManager.setFilters([gF, aF]);
- kbnUrlStateStorage.flush(); // sync force location change
+ kbnUrlStateStorage.kbnUrlControls.flush(); // sync force location change
expect(history.location.hash).toMatchInlineSnapshot(
`"#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!t,index:'logstash-*',key:query,negate:!t,type:custom,value:'%7B%22match%22:%7B%22key1%22:%22value1%22%7D%7D'),query:(match:(key1:value1)))),refreshInterval:(pause:!t,value:0),time:(from:now-15m,to:now))"`
);
@@ -126,7 +126,7 @@ describe('sync_query_state_with_url', () => {
test('when url is changed, filters synced back to filterManager', () => {
const { stop } = syncQueryStateWithUrl(queryServiceStart, kbnUrlStateStorage);
- kbnUrlStateStorage.cancel(); // stop initial syncing pending update
+ kbnUrlStateStorage.kbnUrlControls.cancel(); // stop initial syncing pending update
history.push(pathWithFilter);
expect(filterManager.getGlobalFilters()).toHaveLength(1);
stop();
diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts
index 1deffc9c2d55e..3d87411883a67 100644
--- a/src/plugins/data/public/search/index.ts
+++ b/src/plugins/data/public/search/index.ts
@@ -37,6 +37,7 @@ export {
SearchSessionState,
SessionsClient,
ISessionsClient,
+ noSearchSessionStorageCapabilityMessage,
} from './session';
export { getEsPreference } from './es_search';
diff --git a/src/plugins/data/public/search/session/i18n.ts b/src/plugins/data/public/search/session/i18n.ts
new file mode 100644
index 0000000000000..2ee36b46dfd5a
--- /dev/null
+++ b/src/plugins/data/public/search/session/i18n.ts
@@ -0,0 +1,20 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { i18n } from '@kbn/i18n';
+
+/**
+ * Message to display in case storing
+ * session session is disabled due to turned off capability
+ */
+export const noSearchSessionStorageCapabilityMessage = i18n.translate(
+ 'data.searchSessionIndicator.noCapability',
+ {
+ defaultMessage: "You don't have permissions to create search sessions.",
+ }
+);
diff --git a/src/plugins/data/public/search/session/index.ts b/src/plugins/data/public/search/session/index.ts
index ab311d56fe096..f3f0c34c1be75 100644
--- a/src/plugins/data/public/search/session/index.ts
+++ b/src/plugins/data/public/search/session/index.ts
@@ -9,3 +9,4 @@
export { SessionService, ISessionService, SearchSessionInfoProvider } from './session_service';
export { SearchSessionState } from './search_session_state';
export { SessionsClient, ISessionsClient } from './sessions_client';
+export { noSearchSessionStorageCapabilityMessage } from './i18n';
diff --git a/src/plugins/data/public/search/session/mocks.ts b/src/plugins/data/public/search/session/mocks.ts
index 6a7a207b90d46..679898e3e51dd 100644
--- a/src/plugins/data/public/search/session/mocks.ts
+++ b/src/plugins/data/public/search/session/mocks.ts
@@ -29,7 +29,6 @@ export function getSessionServiceMock(): jest.Mocked {
getSessionId: jest.fn(),
getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()),
state$: new BehaviorSubject(SearchSessionState.None).asObservable(),
- setSearchSessionInfoProvider: jest.fn(),
trackSearch: jest.fn((searchDescriptor) => () => {}),
destroy: jest.fn(),
onRefresh$: new Subject(),
@@ -40,5 +39,8 @@ export function getSessionServiceMock(): jest.Mocked {
save: jest.fn(),
isCurrentSession: jest.fn(),
getSearchOptions: jest.fn(),
+ enableStorage: jest.fn(),
+ isSessionStorageReady: jest.fn(() => true),
+ getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })),
};
}
diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts
index 7797d92c4e07d..21c38c68e6a83 100644
--- a/src/plugins/data/public/search/session/session_service.test.ts
+++ b/src/plugins/data/public/search/session/session_service.test.ts
@@ -113,7 +113,7 @@ describe('Session service', () => {
sessionId,
});
- sessionService.setSearchSessionInfoProvider({
+ sessionService.enableStorage({
getName: async () => 'Name',
getUrlGeneratorData: async () => ({
urlGeneratorId: 'id',
@@ -156,4 +156,62 @@ describe('Session service', () => {
expect(sessionService.isCurrentSession('some-other')).toBeFalsy();
expect(sessionService.isCurrentSession(sessionId)).toBeTruthy();
});
+
+ test('enableStorage() enables storage capabilities', async () => {
+ sessionService.start();
+ await expect(() => sessionService.save()).rejects.toThrowErrorMatchingInlineSnapshot(
+ `"No info provider for current session"`
+ );
+
+ expect(sessionService.isSessionStorageReady()).toBe(false);
+
+ sessionService.enableStorage({
+ getName: async () => 'Name',
+ getUrlGeneratorData: async () => ({
+ urlGeneratorId: 'id',
+ initialState: {},
+ restoreState: {},
+ }),
+ });
+
+ expect(sessionService.isSessionStorageReady()).toBe(true);
+
+ await expect(() => sessionService.save()).resolves;
+
+ sessionService.clear();
+ expect(sessionService.isSessionStorageReady()).toBe(false);
+ });
+
+ test('can provide config for search session indicator', () => {
+ expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(false);
+ sessionService.enableStorage(
+ {
+ getName: async () => 'Name',
+ getUrlGeneratorData: async () => ({
+ urlGeneratorId: 'id',
+ initialState: {},
+ restoreState: {},
+ }),
+ },
+ {
+ isDisabled: () => ({ disabled: true, reasonText: 'text' }),
+ }
+ );
+
+ expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(true);
+
+ sessionService.clear();
+ expect(sessionService.getSearchSessionIndicatorUiConfig().isDisabled().disabled).toBe(false);
+ });
+
+ test('save() throws in case getUrlGeneratorData returns throws', async () => {
+ sessionService.enableStorage({
+ getName: async () => 'Name',
+ getUrlGeneratorData: async () => {
+ throw new Error('Haha');
+ },
+ });
+ sessionService.start();
+ await expect(() => sessionService.save()).rejects.toMatchInlineSnapshot(`[Error: Haha]`);
+ });
});
diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts
index 6269398036e76..23129a9afc460 100644
--- a/src/plugins/data/public/search/session/session_service.ts
+++ b/src/plugins/data/public/search/session/session_service.ts
@@ -43,6 +43,20 @@ export interface SearchSessionInfoProvider;
}
+/**
+ * Configure a "Search session indicator" UI
+ */
+export interface SearchSessionIndicatorUiConfig {
+ /**
+ * App controls if "Search session indicator" UI should be disabled.
+ * reasonText will appear in a tooltip.
+ *
+ * Could be used, for example, to disable "Search session indicator" UI
+ * in case user doesn't have permissions to store a search session
+ */
+ isDisabled: () => { disabled: true; reasonText: string } | { disabled: false };
+}
+
/**
* Responsible for tracking a current search session. Supports only a single session at a time.
*/
@@ -51,6 +65,7 @@ export class SessionService {
private readonly state: SessionStateContainer;
private searchSessionInfoProvider?: SearchSessionInfoProvider;
+ private searchSessionIndicatorUiConfig?: Partial;
private subscription = new Subscription();
private curApp?: string;
@@ -102,17 +117,6 @@ export class SessionService {
});
}
- /**
- * Set a provider of info about current session
- * This will be used for creating a search session saved object
- * @param searchSessionInfoProvider
- */
- public setSearchSessionInfoProvider(
- searchSessionInfoProvider: SearchSessionInfoProvider | undefined
- ) {
- this.searchSessionInfoProvider = searchSessionInfoProvider;
- }
-
/**
* Used to track pending searches within current session
*
@@ -185,7 +189,8 @@ export class SessionService {
*/
public clear() {
this.state.transitions.clear();
- this.setSearchSessionInfoProvider(undefined);
+ this.searchSessionInfoProvider = undefined;
+ this.searchSessionIndicatorUiConfig = undefined;
}
private refresh$ = new Subject();
@@ -269,4 +274,34 @@ export class SessionService {
isStored: isCurrentSession ? this.isStored() : false,
};
}
+
+ /**
+ * Provide an info about current session which is needed for storing a search session.
+ * To opt-into "Search session indicator" UI app has to call {@link enableStorage}.
+ *
+ * @param searchSessionInfoProvider - info provider for saving a search session
+ * @param searchSessionIndicatorUiConfig - config for "Search session indicator" UI
+ */
+ public enableStorage(
+ searchSessionInfoProvider: SearchSessionInfoProvider,
+ searchSessionIndicatorUiConfig?: SearchSessionIndicatorUiConfig
+ ) {
+ this.searchSessionInfoProvider = searchSessionInfoProvider;
+ this.searchSessionIndicatorUiConfig = searchSessionIndicatorUiConfig;
+ }
+
+ /**
+ * If the current app explicitly called {@link enableStorage} and provided all configuration needed
+ * for storing its search sessions
+ */
+ public isSessionStorageReady(): boolean {
+ return !!this.searchSessionInfoProvider;
+ }
+
+ public getSearchSessionIndicatorUiConfig(): SearchSessionIndicatorUiConfig {
+ return {
+ isDisabled: () => ({ disabled: false }),
+ ...this.searchSessionIndicatorUiConfig,
+ };
+ }
}
diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx
index 426302689c8f0..b7d9be485a303 100644
--- a/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx
+++ b/src/plugins/data/public/ui/query_string_input/query_string_input.test.tsx
@@ -84,12 +84,15 @@ function wrapQueryStringInputInContext(testProps: any, storage?: any) {
);
}
-describe('QueryStringInput', () => {
+// FAILING: https://github.com/elastic/kibana/issues/85715
+// FAILING: https://github.com/elastic/kibana/issues/89603
+// FAILING: https://github.com/elastic/kibana/issues/89641
+describe.skip('QueryStringInput', () => {
beforeEach(() => {
jest.clearAllMocks();
});
- it('Should render the given query', async () => {
+ it.skip('Should render the given query', async () => {
const { getByText } = render(
wrapQueryStringInputInContext({
query: kqlQuery,
diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md
index 6a96fd8209a8d..9789f3354e9ef 100644
--- a/src/plugins/data/server/server.api.md
+++ b/src/plugins/data/server/server.api.md
@@ -1138,7 +1138,7 @@ export class Plugin implements Plugin_2 Promise;
};
indexPatterns: {
- indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise;
+ indexPatternsServiceFactory: (savedObjectsClient: Pick, elasticsearchClient: import("../../../core/server").ElasticsearchClient) => Promise;
};
search: ISearchStart>;
};
diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts
index 73523b218df7c..e8c2f1d397ba5 100644
--- a/src/plugins/discover/public/application/angular/context_state.ts
+++ b/src/plugins/discover/public/application/angular/context_state.ts
@@ -206,7 +206,7 @@ export function getState({
}
},
// helper function just needed for testing
- flushToUrl: (replace?: boolean) => stateStorage.flush({ replace }),
+ flushToUrl: (replace?: boolean) => stateStorage.kbnUrlControls.flush(replace),
};
}
diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js
index 946baa7f4ecb1..dcf86babaa5e1 100644
--- a/src/plugins/discover/public/application/angular/discover.js
+++ b/src/plugins/discover/public/application/angular/discover.js
@@ -18,12 +18,12 @@ import {
connectToQueryState,
esFilters,
indexPatterns as indexPatternsUtils,
+ noSearchSessionStorageCapabilityMessage,
syncQueryStateWithUrl,
} from '../../../../data/public';
import { getSortArray } from './doc_table';
import * as columnActions from './doc_table/actions/columns';
import indexTemplateLegacy from './discover_legacy.html';
-import indexTemplateGrid from './discover_datagrid.html';
import { addHelpMenuToAppChrome } from '../components/help_menu/help_menu_util';
import { discoverResponseHandler } from './response_handler';
import {
@@ -47,8 +47,6 @@ import { popularizeField } from '../helpers/popularize_field';
import { getSwitchIndexPatternAppState } from '../helpers/get_switch_index_pattern_app_state';
import { addFatalError } from '../../../../kibana_legacy/public';
import { METRIC_TYPE } from '@kbn/analytics';
-import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
-import { getQueryParams, removeQueryParam } from '../../../../kibana_utils/public';
import {
DEFAULT_COLUMNS_SETTING,
MODIFY_COLUMNS_ON_SWITCH,
@@ -62,6 +60,7 @@ import { getTopNavLinks } from '../components/top_nav/get_top_nav_links';
import { updateSearchSource } from '../helpers/update_search_source';
import { calcFieldCounts } from '../helpers/calc_field_counts';
import { getDefaultSort } from './doc_table/lib/get_default_sort';
+import { DiscoverSearchSessionManager } from './discover_search_session';
const services = getServices();
@@ -86,9 +85,6 @@ const fetchStatuses = {
ERROR: 'error',
};
-const getSearchSessionIdFromURL = (history) =>
- getQueryParams(history.location)[SEARCH_SESSION_ID_QUERY_PARAM];
-
const app = getAngularModule();
app.config(($routeProvider) => {
@@ -115,9 +111,7 @@ app.config(($routeProvider) => {
};
const discoverRoute = {
...defaults,
- template: getServices().uiSettings.get('doc_table:legacy', true)
- ? indexTemplateLegacy
- : indexTemplateGrid,
+ template: indexTemplateLegacy,
reloadOnSearch: false,
resolve: {
savedObjects: function ($route, Promise) {
@@ -179,7 +173,9 @@ function discoverController($route, $scope, Promise) {
const { isDefault: isDefaultType } = indexPatternsUtils;
const subscriptions = new Subscription();
const refetch$ = new Subject();
+
let inspectorRequest;
+ let isChangingIndexPattern = false;
const savedSearch = $route.current.locals.savedObjects.savedSearch;
$scope.searchSource = savedSearch.searchSource;
$scope.indexPattern = resolveIndexPattern(
@@ -197,15 +193,10 @@ function discoverController($route, $scope, Promise) {
};
const history = getHistory();
- // used for restoring a search session
- let isInitialSearch = true;
-
- // search session requested a data refresh
- subscriptions.add(
- data.search.session.onRefresh$.subscribe(() => {
- refetch$.next();
- })
- );
+ const searchSessionManager = new DiscoverSearchSessionManager({
+ history,
+ session: data.search.session,
+ });
const state = getState({
getStateDefaults,
@@ -257,6 +248,7 @@ function discoverController($route, $scope, Promise) {
$scope.$evalAsync(async () => {
if (oldStatePartial.index !== newStatePartial.index) {
//in case of index pattern switch the route has currently to be reloaded, legacy
+ isChangingIndexPattern = true;
$route.reload();
return;
}
@@ -284,12 +276,21 @@ function discoverController($route, $scope, Promise) {
}
});
- data.search.session.setSearchSessionInfoProvider(
+ data.search.session.enableStorage(
createSearchSessionRestorationDataProvider({
appStateContainer,
data,
getSavedSearch: () => savedSearch,
- })
+ }),
+ {
+ isDisabled: () =>
+ capabilities.discover.storeSearchSession
+ ? { disabled: false }
+ : {
+ disabled: true,
+ reasonText: noSearchSessionStorageCapabilityMessage,
+ },
+ }
);
$scope.setIndexPattern = async (id) => {
@@ -344,7 +345,12 @@ function discoverController($route, $scope, Promise) {
if (abortController) abortController.abort();
savedSearch.destroy();
subscriptions.unsubscribe();
- data.search.session.clear();
+ if (!isChangingIndexPattern) {
+ // HACK:
+ // do not clear session when changing index pattern due to how state management around it is setup
+ // it will be cleared by searchSessionManager on controller reload instead
+ data.search.session.clear();
+ }
appStateUnsubscribe();
stopStateSync();
stopSyncingGlobalStateWithUrl();
@@ -468,7 +474,8 @@ function discoverController($route, $scope, Promise) {
return (
config.get(SEARCH_ON_PAGE_LOAD_SETTING) ||
savedSearch.id !== undefined ||
- timefilter.getRefreshInterval().pause === false
+ timefilter.getRefreshInterval().pause === false ||
+ searchSessionManager.hasSearchSessionIdInURL()
);
};
@@ -479,7 +486,8 @@ function discoverController($route, $scope, Promise) {
filterManager.getFetches$(),
timefilter.getFetch$(),
timefilter.getAutoRefreshFetch$(),
- data.query.queryString.getUpdates$()
+ data.query.queryString.getUpdates$(),
+ searchSessionManager.newSearchSessionIdFromURL$
).pipe(debounceTime(100));
subscriptions.add(
@@ -505,6 +513,13 @@ function discoverController($route, $scope, Promise) {
)
);
+ subscriptions.add(
+ data.search.session.onRefresh$.subscribe(() => {
+ searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
+ refetch$.next();
+ })
+ );
+
$scope.changeInterval = (interval) => {
if (interval) {
setAppState({ interval });
@@ -584,20 +599,7 @@ function discoverController($route, $scope, Promise) {
if (abortController) abortController.abort();
abortController = new AbortController();
- const searchSessionId = (() => {
- const searchSessionIdFromURL = getSearchSessionIdFromURL(history);
- if (searchSessionIdFromURL) {
- if (isInitialSearch) {
- data.search.session.restore(searchSessionIdFromURL);
- isInitialSearch = false;
- return searchSessionIdFromURL;
- } else {
- // navigating away from background search
- removeQueryParam(history, SEARCH_SESSION_ID_QUERY_PARAM);
- }
- }
- return data.search.session.start();
- })();
+ const searchSessionId = searchSessionManager.getNextSearchSessionId();
$scope
.updateDataSource()
@@ -624,6 +626,7 @@ function discoverController($route, $scope, Promise) {
$scope.handleRefresh = function (_payload, isUpdate) {
if (isUpdate === false) {
+ searchSessionManager.removeSearchSessionIdFromURL({ replace: false });
refetch$.next();
}
};
diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html
index 9383980fd9fd6..76e5c568ffde6 100644
--- a/src/plugins/discover/public/application/angular/discover_legacy.html
+++ b/src/plugins/discover/public/application/angular/discover_legacy.html
@@ -1,5 +1,5 @@
-
-
+
diff --git a/src/plugins/discover/public/application/angular/discover_search_session.test.ts b/src/plugins/discover/public/application/angular/discover_search_session.test.ts
new file mode 100644
index 0000000000000..abec6aedeaf5c
--- /dev/null
+++ b/src/plugins/discover/public/application/angular/discover_search_session.test.ts
@@ -0,0 +1,96 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { DiscoverSearchSessionManager } from './discover_search_session';
+import { createMemoryHistory } from 'history';
+import { dataPluginMock } from '../../../../data/public/mocks';
+import { DataPublicPluginStart } from '../../../../data/public';
+
+describe('DiscoverSearchSessionManager', () => {
+ const history = createMemoryHistory();
+ const session = dataPluginMock.createStartContract().search.session as jest.Mocked<
+ DataPublicPluginStart['search']['session']
+ >;
+ const searchSessionManager = new DiscoverSearchSessionManager({
+ history,
+ session,
+ });
+
+ beforeEach(() => {
+ history.push('/');
+ session.start.mockReset();
+ session.restore.mockReset();
+ session.getSessionId.mockReset();
+ session.isCurrentSession.mockReset();
+ session.isRestore.mockReset();
+ });
+
+ describe('getNextSearchSessionId', () => {
+ test('starts a new session', () => {
+ const nextId = 'id';
+ session.start.mockImplementationOnce(() => nextId);
+
+ const id = searchSessionManager.getNextSearchSessionId();
+ expect(id).toEqual(nextId);
+ expect(session.start).toBeCalled();
+ });
+
+ test('restores a session using query param from the URL', () => {
+ const nextId = 'id_from_url';
+ history.push(`/?searchSessionId=${nextId}`);
+
+ const id = searchSessionManager.getNextSearchSessionId();
+ expect(id).toEqual(nextId);
+ expect(session.restore).toBeCalled();
+ });
+
+ test('removes query param from the URL when navigating away from a restored session', () => {
+ const idFromUrl = 'id_from_url';
+ history.push(`/?searchSessionId=${idFromUrl}`);
+
+ const nextId = 'id';
+ session.start.mockImplementationOnce(() => nextId);
+ session.isCurrentSession.mockImplementationOnce(() => true);
+ session.isRestore.mockImplementationOnce(() => true);
+
+ const id = searchSessionManager.getNextSearchSessionId();
+ expect(id).toEqual(nextId);
+ expect(session.start).toBeCalled();
+ expect(history.location.search).toMatchInlineSnapshot(`""`);
+ });
+ });
+
+ describe('newSearchSessionIdFromURL$', () => {
+ test('notifies about searchSessionId changes in the URL', () => {
+ const emits: Array = [];
+
+ const sub = searchSessionManager.newSearchSessionIdFromURL$.subscribe((newId) => {
+ emits.push(newId);
+ });
+
+ history.push(`/?searchSessionId=id1`);
+ history.push(`/?searchSessionId=id1`);
+ session.isCurrentSession.mockImplementationOnce(() => true);
+ history.replace(`/?searchSessionId=id2`); // should skip current this
+ history.replace(`/`);
+ history.push(`/?searchSessionId=id1`);
+ history.push(`/`);
+
+ expect(emits).toMatchInlineSnapshot(`
+ Array [
+ "id1",
+ null,
+ "id1",
+ null,
+ ]
+ `);
+
+ sub.unsubscribe();
+ });
+ });
+});
diff --git a/src/plugins/discover/public/application/angular/discover_search_session.ts b/src/plugins/discover/public/application/angular/discover_search_session.ts
new file mode 100644
index 0000000000000..a53d7d6d2c333
--- /dev/null
+++ b/src/plugins/discover/public/application/angular/discover_search_session.ts
@@ -0,0 +1,85 @@
+/*
+ * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
+ * or more contributor license agreements. Licensed under the Elastic License
+ * and the Server Side Public License, v 1; you may not use this file except in
+ * compliance with, at your election, the Elastic License or the Server Side
+ * Public License, v 1.
+ */
+
+import { History } from 'history';
+import { filter } from 'rxjs/operators';
+import { DataPublicPluginStart } from '../../../../data/public';
+import {
+ createQueryParamObservable,
+ getQueryParams,
+ removeQueryParam,
+} from '../../../../kibana_utils/public';
+import { SEARCH_SESSION_ID_QUERY_PARAM } from '../../url_generator';
+
+export interface DiscoverSearchSessionManagerDeps {
+ history: History;
+ session: DataPublicPluginStart['search']['session'];
+}
+
+/**
+ * Helps with state management of search session and {@link SEARCH_SESSION_ID_QUERY_PARAM} in the URL
+ */
+export class DiscoverSearchSessionManager {
+ /**
+ * Notifies about `searchSessionId` changes in the URL,
+ * skips if `searchSessionId` matches current search session id
+ */
+ readonly newSearchSessionIdFromURL$ = createQueryParamObservable(
+ this.deps.history,
+ SEARCH_SESSION_ID_QUERY_PARAM
+ ).pipe(
+ filter((searchSessionId) => {
+ if (!searchSessionId) return true;
+ return !this.deps.session.isCurrentSession(searchSessionId);
+ })
+ );
+
+ constructor(private readonly deps: DiscoverSearchSessionManagerDeps) {}
+
+ /**
+ * Get next session id by either starting or restoring a session.
+ * When navigating away from the restored session {@link SEARCH_SESSION_ID_QUERY_PARAM} is removed from the URL using history.replace
+ */
+ getNextSearchSessionId() {
+ let searchSessionIdFromURL = this.getSearchSessionIdFromURL();
+ if (searchSessionIdFromURL) {
+ if (
+ this.deps.session.isRestore() &&
+ this.deps.session.isCurrentSession(searchSessionIdFromURL)
+ ) {
+ // navigating away from a restored session
+ this.removeSearchSessionIdFromURL({ replace: true });
+ searchSessionIdFromURL = undefined;
+ } else {
+ this.deps.session.restore(searchSessionIdFromURL);
+ }
+ }
+
+ return searchSessionIdFromURL ?? this.deps.session.start();
+ }
+
+ /**
+ * Removes Discovers {@link SEARCH_SESSION_ID_QUERY_PARAM} from the URL
+ * @param replace - methods to change the URL
+ */
+ removeSearchSessionIdFromURL({ replace = true }: { replace?: boolean } = { replace: true }) {
+ if (this.hasSearchSessionIdInURL()) {
+ removeQueryParam(this.deps.history, SEARCH_SESSION_ID_QUERY_PARAM, replace);
+ }
+ }
+
+ /**
+ * If there is a {@link SEARCH_SESSION_ID_QUERY_PARAM} currently in the URL
+ */
+ hasSearchSessionIdInURL(): boolean {
+ return !!this.getSearchSessionIdFromURL();
+ }
+
+ private getSearchSessionIdFromURL = () =>
+ getQueryParams(this.deps.history.location)[SEARCH_SESSION_ID_QUERY_PARAM] as string | undefined;
+}
diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts
index c769e263655ab..65a8dded11092 100644
--- a/src/plugins/discover/public/application/angular/discover_state.ts
+++ b/src/plugins/discover/public/application/angular/discover_state.ts
@@ -200,7 +200,7 @@ export function getState({
setState(appStateContainerModified, defaultState);
},
getPreviousAppState: () => previousAppState,
- flushToUrl: () => stateStorage.flush(),
+ flushToUrl: () => stateStorage.kbnUrlControls.flush(),
isAppStateDirty: () => !isEqualState(initialAppState, appStateContainer.getState()),
};
}
diff --git a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
index 06b6e504832e4..cbd93feb835a0 100644
--- a/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
+++ b/src/plugins/discover/public/application/angular/doc_table/create_doc_table_react.tsx
@@ -9,8 +9,11 @@
import angular, { auto, ICompileService, IScope } from 'angular';
import { render } from 'react-dom';
import React, { useRef, useEffect } from 'react';
+import { EuiButtonEmpty } from '@elastic/eui';
+import { FormattedMessage } from '@kbn/i18n/react';
import { getServices, IIndexPattern } from '../../../kibana_services';
import { IndexPatternField } from '../../../../../data/common/index_patterns';
+
export type AngularScope = IScope;
export interface AngularDirective {
@@ -83,9 +86,11 @@ export interface DocTableLegacyProps {
indexPattern: IIndexPattern;
minimumVisibleRows: number;
onAddColumn?: (column: string) => void;
+ onBackToTop: () => void;
onSort?: (sort: string[][]) => void;
onMoveColumn?: (columns: string, newIdx: number) => void;
onRemoveColumn?: (column: string) => void;
+ sampleSize: number;
sort?: string[][];
useNewFieldsApi?: boolean;
}
@@ -120,5 +125,31 @@ export function DocTableLegacy(renderProps: DocTableLegacyProps) {
return renderFn(ref.current, renderProps);
}
}, [renderFn, renderProps]);
- return ;
+ return (
+
+
+ {renderProps.rows.length === renderProps.sampleSize ? (
+
+
+
+
+
+
+ ) : (
+
+
+
+ )}
+
+ );
}
diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts
index 42b99b635a791..10439488f4bc7 100644
--- a/src/plugins/discover/public/application/components/create_discover_directive.ts
+++ b/src/plugins/discover/public/application/components/create_discover_directive.ts
@@ -17,18 +17,21 @@ export function createDiscoverDirective(reactDirective: any) {
['histogramData', { watchDepth: 'reference' }],
['hits', { watchDepth: 'reference' }],
['indexPattern', { watchDepth: 'reference' }],
+ ['minimumVisibleRows', { watchDepth: 'reference' }],
['onAddColumn', { watchDepth: 'reference' }],
['onAddFilter', { watchDepth: 'reference' }],
['onChangeInterval', { watchDepth: 'reference' }],
+ ['onMoveColumn', { watchDepth: 'reference' }],
['onRemoveColumn', { watchDepth: 'reference' }],
['onSetColumns', { watchDepth: 'reference' }],
+ ['onSkipBottomButtonClick', { watchDepth: 'reference' }],
['onSort', { watchDepth: 'reference' }],
['opts', { watchDepth: 'reference' }],
['resetQuery', { watchDepth: 'reference' }],
['resultState', { watchDepth: 'reference' }],
['rows', { watchDepth: 'reference' }],
+ ['savedSearch', { watchDepth: 'reference' }],
['searchSource', { watchDepth: 'reference' }],
- ['setColumns', { watchDepth: 'reference' }],
['setIndexPattern', { watchDepth: 'reference' }],
['showSaveQuery', { watchDepth: 'reference' }],
['state', { watchDepth: 'reference' }],
diff --git a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts b/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts
deleted file mode 100644
index b2b9fd38f73b1..0000000000000
--- a/src/plugins/discover/public/application/components/create_discover_legacy_directive.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-/*
- * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
- * or more contributor license agreements. Licensed under the Elastic License
- * and the Server Side Public License, v 1; you may not use this file except in
- * compliance with, at your election, the Elastic License or the Server Side
- * Public License, v 1.
- */
-
-import { DiscoverLegacy } from './discover_legacy';
-
-export function createDiscoverLegacyDirective(reactDirective: any) {
- return reactDirective(DiscoverLegacy, [
- ['fetch', { watchDepth: 'reference' }],
- ['fetchCounter', { watchDepth: 'reference' }],
- ['fetchError', { watchDepth: 'reference' }],
- ['fieldCounts', { watchDepth: 'reference' }],
- ['histogramData', { watchDepth: 'reference' }],
- ['hits', { watchDepth: 'reference' }],
- ['indexPattern', { watchDepth: 'reference' }],
- ['minimumVisibleRows', { watchDepth: 'reference' }],
- ['onAddColumn', { watchDepth: 'reference' }],
- ['onAddFilter', { watchDepth: 'reference' }],
- ['onChangeInterval', { watchDepth: 'reference' }],
- ['onMoveColumn', { watchDepth: 'reference' }],
- ['onRemoveColumn', { watchDepth: 'reference' }],
- ['onSetColumns', { watchDepth: 'reference' }],
- ['onSkipBottomButtonClick', { watchDepth: 'reference' }],
- ['onSort', { watchDepth: 'reference' }],
- ['opts', { watchDepth: 'reference' }],
- ['resetQuery', { watchDepth: 'reference' }],
- ['resultState', { watchDepth: 'reference' }],
- ['rows', { watchDepth: 'reference' }],
- ['savedSearch', { watchDepth: 'reference' }],
- ['searchSource', { watchDepth: 'reference' }],
- ['setIndexPattern', { watchDepth: 'reference' }],
- ['showSaveQuery', { watchDepth: 'reference' }],
- ['state', { watchDepth: 'reference' }],
- ['timefilterUpdateHandler', { watchDepth: 'reference' }],
- ['timeRange', { watchDepth: 'reference' }],
- ['topNavMenu', { watchDepth: 'reference' }],
- ['updateQuery', { watchDepth: 'reference' }],
- ['updateSavedQueryId', { watchDepth: 'reference' }],
- ['useNewFieldsApi', { watchDepth: 'reference' }],
- ]);
-}
diff --git a/src/plugins/discover/public/application/components/discover_legacy.test.tsx b/src/plugins/discover/public/application/components/discover.test.tsx
similarity index 89%
rename from src/plugins/discover/public/application/components/discover_legacy.test.tsx
rename to src/plugins/discover/public/application/components/discover.test.tsx
index 04f294912d49e..3088ca45f7941 100644
--- a/src/plugins/discover/public/application/components/discover_legacy.test.tsx
+++ b/src/plugins/discover/public/application/components/discover.test.tsx
@@ -8,7 +8,7 @@
import React from 'react';
import { shallowWithIntl } from '@kbn/test/jest';
-import { DiscoverLegacy } from './discover_legacy';
+import { Discover } from './discover';
import { inspectorPluginMock } from '../../../../inspector/public/mocks';
import { esHits } from '../../__mocks__/es_hits';
import { indexPatternMock } from '../../__mocks__/index_pattern';
@@ -19,7 +19,7 @@ import { savedSearchMock } from '../../__mocks__/saved_search';
import { createSearchSourceMock } from '../../../../data/common/search/search_source/mocks';
import { dataPluginMock } from '../../../../data/public/mocks';
import { createFilterManagerMock } from '../../../../data/public/query/filter_manager/filter_manager.mock';
-import { uiSettingsMock } from '../../__mocks__/ui_settings';
+import { uiSettingsMock as mockUiSettings } from '../../__mocks__/ui_settings';
import { IndexPattern, IndexPatternAttributes } from '../../../../data/common/index_patterns';
import { SavedObject } from '../../../../../core/types';
import { navigationPluginMock } from '../../../../navigation/public/mocks';
@@ -40,6 +40,7 @@ jest.mock('../../kibana_services', () => {
},
},
navigation: mockNavigation,
+ uiSettings: mockUiSettings,
}),
};
});
@@ -53,6 +54,7 @@ function getProps(indexPattern: IndexPattern) {
save: true,
},
},
+ uiSettings: mockUiSettings,
} as unknown) as DiscoverServices;
return {
@@ -72,7 +74,7 @@ function getProps(indexPattern: IndexPattern) {
onSkipBottomButtonClick: jest.fn(),
onSort: jest.fn(),
opts: {
- config: uiSettingsMock,
+ config: mockUiSettings,
data: dataPluginMock.createStartContract(),
fixedScroll: jest.fn(),
filterManager: createFilterManagerMock(),
@@ -105,15 +107,13 @@ function getProps(indexPattern: IndexPattern) {
};
}
-describe('Descover legacy component', () => {
+describe('Discover component', () => {
test('selected index pattern without time field displays no chart toggle', () => {
- const component = shallowWithIntl();
+ const component = shallowWithIntl();
expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(0);
});
test('selected index pattern with time field displays chart toggle', () => {
- const component = shallowWithIntl(
-
- );
+ const component = shallowWithIntl();
expect(component.find('[data-test-subj="discoverChartToggle"]').length).toBe(1);
});
});
diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx
index 704e7a9c02e1b..5653ef4f57435 100644
--- a/src/plugins/discover/public/application/components/discover.tsx
+++ b/src/plugins/discover/public/application/components/discover.tsx
@@ -26,25 +26,30 @@ import classNames from 'classnames';
import { HitsCounter } from './hits_counter';
import { TimechartHeader } from './timechart_header';
import { getServices } from '../../kibana_services';
-import { DiscoverUninitialized, DiscoverHistogram } from '../angular/directives';
+import { DiscoverHistogram, DiscoverUninitialized } from '../angular/directives';
import { DiscoverNoResults } from './no_results';
import { LoadingSpinner } from './loading_spinner/loading_spinner';
+import { DocTableLegacy, DocTableLegacyProps } from '../angular/doc_table/create_doc_table_react';
+import { SkipBottomButton } from './skip_bottom_button';
import { search } from '../../../../data/public';
import {
DiscoverSidebarResponsive,
DiscoverSidebarResponsiveProps,
} from './sidebar/discover_sidebar_responsive';
-import { DiscoverProps } from './discover_legacy';
+import { DiscoverProps } from './types';
+import { getDisplayedColumns } from '../helpers/columns';
import { SortPairArr } from '../angular/doc_table/lib/get_sort';
import { DiscoverGrid, DiscoverGridProps } from './discover_grid/discover_grid';
+import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common';
-export const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => (
+const DocTableLegacyMemoized = React.memo((props: DocTableLegacyProps) => (
+
+));
+const SidebarMemoized = React.memo((props: DiscoverSidebarResponsiveProps) => (
));
-export const DataGridMemoized = React.memo((props: DiscoverGridProps) => (
-
-));
+const DataGridMemoized = React.memo((props: DiscoverGridProps) => );
export function Discover({
fetch,
@@ -54,11 +59,14 @@ export function Discover({
histogramData,
hits,
indexPattern,
+ minimumVisibleRows,
onAddColumn,
onAddFilter,
onChangeInterval,
+ onMoveColumn,
onRemoveColumn,
onSetColumns,
+ onSkipBottomButtonClick,
onSort,
opts,
resetQuery,
@@ -66,7 +74,6 @@ export function Discover({
rows,
searchSource,
setIndexPattern,
- showSaveQuery,
state,
timefilterUpdateHandler,
timeRange,
@@ -76,6 +83,11 @@ export function Discover({
}: DiscoverProps) {
const scrollableDesktop = useRef(null);
const collapseIcon = useRef(null);
+ const isMobile = () => {
+ // collapse icon isn't displayed in mobile view, use it to detect which view is displayed
+ return collapseIcon && !collapseIcon.current;
+ };
+
const [toggleOn, toggleChart] = useState(true);
const [isSidebarClosed, setIsSidebarClosed] = useState(false);
const services = getServices();
@@ -88,18 +100,8 @@ export function Discover({
? bucketAggConfig.buckets?.getInterval()
: undefined;
const contentCentered = resultState === 'uninitialized';
- const showTimeCol = !config.get('doc_table:hideTimeColumn', false) && indexPattern.timeFieldName;
- const columns =
- state.columns &&
- state.columns.length > 0 &&
- // check if all columns where removed except the configured timeField (this can't be removed)
- !(state.columns.length === 1 && state.columns[0] === indexPattern.timeFieldName)
- ? state.columns
- : ['_source'];
- // if columns include _source this is considered as default view, so you can't remove columns
- // until you add a column using Discover's sidebar
- const defaultColumns = columns.includes('_source');
-
+ const isLegacy = services.uiSettings.get('doc_table:legacy');
+ const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE);
return (
@@ -114,7 +116,7 @@ export function Discover({
savedQueryId={state.savedQuery}
screenTitle={savedSearch.title}
showDatePicker={indexPattern.isTimeBased()}
- showSaveQuery={showSaveQuery}
+ showSaveQuery={!!services.capabilities.discover.saveQuery}
showSearchBar={true}
useDefaultBehaviors={true}
/>
@@ -137,6 +139,7 @@ export function Discover({
setIndexPattern={setIndexPattern}
isClosed={isSidebarClosed}
trackUiMetric={trackUiMetric}
+ useNewFieldsApi={useNewFieldsApi}
/>
@@ -207,24 +210,28 @@ export function Discover({
/>
)}
-
- {
- toggleChart(!toggleOn);
- }}
- >
- {toggleOn
- ? i18n.translate('discover.hideChart', {
- defaultMessage: 'Hide chart',
- })
- : i18n.translate('discover.showChart', {
- defaultMessage: 'Show chart',
- })}
-
-
+ {opts.timefield && (
+
+ {
+ toggleChart(!toggleOn);
+ }}
+ data-test-subj="discoverChartToggle"
+ >
+ {toggleOn
+ ? i18n.translate('discover.hideChart', {
+ defaultMessage: 'Hide chart',
+ })
+ : i18n.translate('discover.showChart', {
+ defaultMessage: 'Show chart',
+ })}
+
+
+ )}
+ {isLegacy && }
{toggleOn && opts.timefield && (
@@ -238,7 +245,10 @@ export function Discover({
className="dscTimechart"
>
{opts.chartAggConfigs && histogramData && rows.length !== 0 && (
-
+