Skip to content

Commit

Permalink
[Search Sessions] Make search session indicator UI opt-in, refactor p…
Browse files Browse the repository at this point in the history
…er-app capabilities (#88699)
  • Loading branch information
Dosant committed Jan 27, 2021
1 parent 64e9cf0 commit b8947e3
Show file tree
Hide file tree
Showing 24 changed files with 424 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@
| [isPartialResponse](./kibana-plugin-plugins-data-public.ispartialresponse.md) | |
| [isQuery](./kibana-plugin-plugins-data-public.isquery.md) | |
| [isTimeRange](./kibana-plugin-plugins-data-public.istimerange.md) | |
| [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md) | Message to display in case storing session session is disabled due to turned off capability |
| [parseSearchSourceJSON](./kibana-plugin-plugins-data-public.parsesearchsourcejson.md) | |
| [QueryStringInput](./kibana-plugin-plugins-data-public.querystringinput.md) | |
| [search](./kibana-plugin-plugins-data-public.search.md) | |
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) &gt; [noSearchSessionStorageCapabilityMessage](./kibana-plugin-plugins-data-public.nosearchsessionstoragecapabilitymessage.md)

## noSearchSessionStorageCapabilityMessage variable

Message to display in case storing session session is disabled due to turned off capability

<b>Signature:</b>

```typescript
noSearchSessionStorageCapabilityMessage: string
```
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,14 +89,15 @@ export interface InheritedChildInput extends IndexSignature {
export type DashboardReactContextValue = KibanaReactContextValue<DashboardContainerServices>;
export type DashboardReactContext = KibanaReactContext<DashboardContainerServices>;

const defaultCapabilities = {
const defaultCapabilities: DashboardCapabilities = {
show: false,
createNew: false,
saveQuery: false,
createShortUrl: false,
hideWriteControls: true,
mapsCapabilities: { save: false },
visualizeCapabilities: { save: false },
storeSearchSession: true,
};

export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { useKibana } from '../../services/kibana_react';
import {
connectToQueryState,
esFilters,
noSearchSessionStorageCapabilityMessage,
QueryState,
syncQueryStateWithUrl,
} from '../../services/data';
Expand Down Expand Up @@ -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);
Expand All @@ -192,6 +202,7 @@ export const useDashboardStateManager = (
toasts,
uiSettings,
usageCollection,
dashboardCapabilities.storeSearchSession,
]);

return { dashboardStateManager, viewMode, setViewMode };
Expand Down
1 change: 1 addition & 0 deletions src/plugins/dashboard/public/application/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export interface DashboardCapabilities {
saveQuery: boolean;
createNew: boolean;
show: boolean;
storeSearchSession: boolean;
}

export interface DashboardAppServices {
Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -384,6 +384,7 @@ export {
SearchTimeoutError,
TimeoutErrorMode,
PainlessError,
noSearchSessionStorageCapabilityMessage,
} from './search';

export type {
Expand Down
35 changes: 20 additions & 15 deletions src/plugins/data/public/public.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down
1 change: 1 addition & 0 deletions src/plugins/data/public/search/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export {
SearchSessionState,
SessionsClient,
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
} from './session';
export { getEsPreference } from './es_search';

Expand Down
20 changes: 20 additions & 0 deletions src/plugins/data/public/search/session/i18n.ts
Original file line number Diff line number Diff line change
@@ -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.",
}
);
1 change: 1 addition & 0 deletions src/plugins/data/public/search/session/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
4 changes: 3 additions & 1 deletion src/plugins/data/public/search/session/mocks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
getSessionId: jest.fn(),
getSession$: jest.fn(() => new BehaviorSubject(undefined).asObservable()),
state$: new BehaviorSubject<SearchSessionState>(SearchSessionState.None).asObservable(),
setSearchSessionInfoProvider: jest.fn(),
trackSearch: jest.fn((searchDescriptor) => () => {}),
destroy: jest.fn(),
onRefresh$: new Subject(),
Expand All @@ -40,5 +39,8 @@ export function getSessionServiceMock(): jest.Mocked<ISessionService> {
save: jest.fn(),
isCurrentSession: jest.fn(),
getSearchOptions: jest.fn(),
enableStorage: jest.fn(),
isSessionStorageReady: jest.fn(() => true),
getSearchSessionIndicatorUiConfig: jest.fn(() => ({ isDisabled: () => ({ disabled: false }) })),
};
}
60 changes: 59 additions & 1 deletion src/plugins/data/public/search/session/session_service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('Session service', () => {
sessionId,
});

sessionService.setSearchSessionInfoProvider({
sessionService.enableStorage({
getName: async () => 'Name',
getUrlGeneratorData: async () => ({
urlGeneratorId: 'id',
Expand Down Expand Up @@ -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]`);
});
});
59 changes: 47 additions & 12 deletions src/plugins/data/public/search/session/session_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,20 @@ export interface SearchSessionInfoProvider<ID extends UrlGeneratorId = UrlGenera
}>;
}

/**
* 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.
*/
Expand All @@ -51,6 +65,7 @@ export class SessionService {
private readonly state: SessionStateContainer<TrackSearchDescriptor>;

private searchSessionInfoProvider?: SearchSessionInfoProvider;
private searchSessionIndicatorUiConfig?: Partial<SearchSessionIndicatorUiConfig>;
private subscription = new Subscription();
private curApp?: string;

Expand Down Expand Up @@ -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<ID extends UrlGeneratorId = UrlGeneratorId>(
searchSessionInfoProvider: SearchSessionInfoProvider<ID> | undefined
) {
this.searchSessionInfoProvider = searchSessionInfoProvider;
}

/**
* Used to track pending searches within current session
*
Expand Down Expand Up @@ -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<void>();
Expand Down Expand Up @@ -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<ID extends UrlGeneratorId = UrlGeneratorId>(
searchSessionInfoProvider: SearchSessionInfoProvider<ID>,
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,
};
}
}
14 changes: 12 additions & 2 deletions src/plugins/discover/public/application/angular/discover.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
connectToQueryState,
esFilters,
indexPatterns as indexPatternsUtils,
noSearchSessionStorageCapabilityMessage,
syncQueryStateWithUrl,
} from '../../../../data/public';
import { getSortArray } from './doc_table';
Expand Down Expand Up @@ -284,12 +285,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) => {
Expand Down
Loading

0 comments on commit b8947e3

Please sign in to comment.