mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Search Sessions] Make search session indicator UI opt-in, refactor per-app capabilities (#88699)
This commit is contained in:
parent
64e9cf0440
commit
b8947e3e15
24 changed files with 424 additions and 75 deletions
|
@ -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) | |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [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
|
||||
```
|
|
@ -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),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -89,7 +89,7 @@ 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,
|
||||
|
@ -97,6 +97,7 @@ const defaultCapabilities = {
|
|||
hideWriteControls: true,
|
||||
mapsCapabilities: { save: false },
|
||||
visualizeCapabilities: { save: false },
|
||||
storeSearchSession: true,
|
||||
};
|
||||
|
||||
export class DashboardContainer extends Container<InheritedChildInput, DashboardContainerInput> {
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -55,6 +55,7 @@ export interface DashboardCapabilities {
|
|||
saveQuery: boolean;
|
||||
createNew: boolean;
|
||||
show: boolean;
|
||||
storeSearchSession: boolean;
|
||||
}
|
||||
|
||||
export interface DashboardAppServices {
|
||||
|
|
|
@ -384,6 +384,7 @@ export {
|
|||
SearchTimeoutError,
|
||||
TimeoutErrorMode,
|
||||
PainlessError,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
} from './search';
|
||||
|
||||
export type {
|
||||
|
|
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
|
|
@ -37,6 +37,7 @@ export {
|
|||
SearchSessionState,
|
||||
SessionsClient,
|
||||
ISessionsClient,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
} from './session';
|
||||
export { getEsPreference } from './es_search';
|
||||
|
||||
|
|
20
src/plugins/data/public/search/session/i18n.ts
Normal file
20
src/plugins/data/public/search/session/i18n.ts
Normal file
|
@ -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.",
|
||||
}
|
||||
);
|
|
@ -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';
|
||||
|
|
|
@ -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(),
|
||||
|
@ -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 }) })),
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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]`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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
|
||||
*
|
||||
|
@ -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>();
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
connectToQueryState,
|
||||
esFilters,
|
||||
indexPatterns as indexPatternsUtils,
|
||||
noSearchSessionStorageCapabilityMessage,
|
||||
syncQueryStateWithUrl,
|
||||
} from '../../../../data/public';
|
||||
import { getSortArray } from './doc_table';
|
||||
|
@ -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) => {
|
||||
|
|
|
@ -28,6 +28,12 @@ timeFilter.getRefreshInterval.mockImplementation(() => refreshInterval$.getValue
|
|||
|
||||
beforeEach(() => {
|
||||
refreshInterval$.next({ value: 0, pause: true });
|
||||
sessionService.isSessionStorageReady.mockImplementation(() => true);
|
||||
sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({
|
||||
isDisabled: () => ({
|
||||
disabled: false,
|
||||
}),
|
||||
}));
|
||||
});
|
||||
|
||||
test("shouldn't show indicator in case no active search session", async () => {
|
||||
|
@ -45,6 +51,22 @@ test("shouldn't show indicator in case no active search session", async () => {
|
|||
expect(container).toMatchInlineSnapshot(`<div />`);
|
||||
});
|
||||
|
||||
test("shouldn't show indicator in case app hasn't opt-in", async () => {
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService,
|
||||
application: coreStart.application,
|
||||
timeFilter,
|
||||
});
|
||||
const { getByTestId, container } = render(<SearchSessionIndicator />);
|
||||
sessionService.isSessionStorageReady.mockImplementation(() => false);
|
||||
|
||||
// make sure `searchSessionIndicator` isn't appearing after some time (lazy-loading)
|
||||
await expect(
|
||||
waitFor(() => getByTestId('searchSessionIndicator'), { timeout: 100 })
|
||||
).rejects.toThrow();
|
||||
expect(container).toMatchInlineSnapshot(`<div />`);
|
||||
});
|
||||
|
||||
test('should show indicator in case there is an active search session', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
|
@ -57,7 +79,7 @@ test('should show indicator in case there is an active search session', async ()
|
|||
await waitFor(() => getByTestId('searchSessionIndicator'));
|
||||
});
|
||||
|
||||
test('should be disabled when permissions are off', async () => {
|
||||
test('should be disabled in case uiConfig says so ', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
coreStart.application.currentAppId$ = new BehaviorSubject('discover');
|
||||
(coreStart.application.capabilities as any) = {
|
||||
|
@ -65,6 +87,12 @@ test('should be disabled when permissions are off', async () => {
|
|||
storeSearchSession: false,
|
||||
},
|
||||
};
|
||||
sessionService.getSearchSessionIndicatorUiConfig.mockImplementation(() => ({
|
||||
isDisabled: () => ({
|
||||
disabled: true,
|
||||
reasonText: 'reason',
|
||||
}),
|
||||
}));
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
|
@ -80,12 +108,7 @@ test('should be disabled when permissions are off', async () => {
|
|||
|
||||
test('should be disabled during auto-refresh', async () => {
|
||||
const state$ = new BehaviorSubject(SearchSessionState.Loading);
|
||||
coreStart.application.currentAppId$ = new BehaviorSubject('discover');
|
||||
(coreStart.application.capabilities as any) = {
|
||||
discover: {
|
||||
storeSearchSession: true,
|
||||
},
|
||||
};
|
||||
|
||||
const SearchSessionIndicator = createConnectedSearchSessionIndicator({
|
||||
sessionService: { ...sessionService, state$ },
|
||||
application: coreStart.application,
|
||||
|
|
|
@ -29,35 +29,14 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
.getRefreshIntervalUpdate$()
|
||||
.pipe(map(isAutoRefreshEnabled), distinctUntilChanged());
|
||||
|
||||
const getCapabilitiesByAppId = (
|
||||
capabilities: ApplicationStart['capabilities'],
|
||||
appId?: string
|
||||
) => {
|
||||
switch (appId) {
|
||||
case 'dashboards':
|
||||
return capabilities.dashboard;
|
||||
case 'discover':
|
||||
return capabilities.discover;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
return () => {
|
||||
const state = useObservable(sessionService.state$.pipe(debounceTime(500)));
|
||||
const autoRefreshEnabled = useObservable(isAutoRefreshEnabled$, isAutoRefreshEnabled());
|
||||
const appId = useObservable(application.currentAppId$, undefined);
|
||||
const isDisabledByApp = sessionService.getSearchSessionIndicatorUiConfig().isDisabled();
|
||||
|
||||
let disabled = false;
|
||||
let disabledReasonText: string = '';
|
||||
|
||||
if (getCapabilitiesByAppId(application.capabilities, appId)?.storeSearchSession !== true) {
|
||||
disabled = true;
|
||||
disabledReasonText = i18n.translate('xpack.data.searchSessionIndicator.noCapability', {
|
||||
defaultMessage: "You don't have permissions to send to background.",
|
||||
});
|
||||
}
|
||||
|
||||
if (autoRefreshEnabled) {
|
||||
disabled = true;
|
||||
disabledReasonText = i18n.translate(
|
||||
|
@ -68,6 +47,12 @@ export const createConnectedSearchSessionIndicator = ({
|
|||
);
|
||||
}
|
||||
|
||||
if (isDisabledByApp.disabled) {
|
||||
disabled = true;
|
||||
disabledReasonText = isDisabledByApp.reasonText;
|
||||
}
|
||||
|
||||
if (!sessionService.isSessionStorageReady()) return null;
|
||||
if (!state) return null;
|
||||
return (
|
||||
<RedirectAppLinks application={application}>
|
||||
|
|
|
@ -24,6 +24,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
|||
resolve(__dirname, './tests/apps/dashboard/async_search'),
|
||||
resolve(__dirname, './tests/apps/discover'),
|
||||
resolve(__dirname, './tests/apps/management/search_sessions'),
|
||||
resolve(__dirname, './tests/apps/lens'),
|
||||
],
|
||||
|
||||
kbnTestServer: {
|
||||
|
|
|
@ -5,9 +5,9 @@
|
|||
*/
|
||||
|
||||
import { services as functionalServices } from '../../functional/services';
|
||||
import { SendToBackgroundProvider } from './send_to_background';
|
||||
import { SearchSessionsProvider } from './search_sessions';
|
||||
|
||||
export const services = {
|
||||
...functionalServices,
|
||||
searchSessions: SendToBackgroundProvider,
|
||||
searchSessions: SearchSessionsProvider,
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { SavedObjectsFindResponse } from 'src/core/server';
|
||||
import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
@ -20,7 +21,7 @@ type SessionStateType =
|
|||
| 'restored'
|
||||
| 'canceled';
|
||||
|
||||
export function SendToBackgroundProvider({ getService }: FtrProviderContext) {
|
||||
export function SearchSessionsProvider({ getService }: FtrProviderContext) {
|
||||
const testSubjects = getService('testSubjects');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
|
@ -36,6 +37,17 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) {
|
|||
return testSubjects.exists(SEARCH_SESSION_INDICATOR_TEST_SUBJ);
|
||||
}
|
||||
|
||||
public async missingOrFail(): Promise<void> {
|
||||
return testSubjects.missingOrFail(SEARCH_SESSION_INDICATOR_TEST_SUBJ);
|
||||
}
|
||||
|
||||
public async disabledOrFail() {
|
||||
await this.exists();
|
||||
await expect(await (await (await this.find()).findByTagName('button')).isEnabled()).to.be(
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
public async expectState(state: SessionStateType) {
|
||||
return retry.waitFor(`searchSessions indicator to get into state = ${state}`, async () => {
|
||||
const currentState = await (
|
||||
|
@ -93,12 +105,12 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) {
|
|||
}
|
||||
|
||||
/*
|
||||
* This cleanup function should be used by tests that create new background sesions.
|
||||
* Tests should not end with new background sessions remaining in storage since that interferes with functional tests that check the _find API.
|
||||
* Alternatively, a test can navigate to `Managment > Search Sessions` and use the UI to delete any created tests.
|
||||
* This cleanup function should be used by tests that create new search sessions.
|
||||
* Tests should not end with new search sessions remaining in storage since that interferes with functional tests that check the _find API.
|
||||
* Alternatively, a test can navigate to `Management > Search Sessions` and use the UI to delete any created tests.
|
||||
*/
|
||||
public async deleteAllSearchSessions() {
|
||||
log.debug('Deleting created background sessions');
|
||||
log.debug('Deleting created searcg sessions');
|
||||
// ignores 409 errs and keeps retrying
|
||||
await retry.tryForTime(10000, async () => {
|
||||
const { body } = await supertest
|
||||
|
@ -109,10 +121,10 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) {
|
|||
.expect(200);
|
||||
|
||||
const { saved_objects: savedObjects } = body as SavedObjectsFindResponse;
|
||||
log.debug(`Found created background sessions: ${savedObjects.map(({ id }) => id)}`);
|
||||
log.debug(`Found created search sessions: ${savedObjects.map(({ id }) => id)}`);
|
||||
await Promise.all(
|
||||
savedObjects.map(async (so) => {
|
||||
log.debug(`Deleting background session: ${so.id}`);
|
||||
log.debug(`Deleting search session: ${so.id}`);
|
||||
await supertest
|
||||
.delete(`/internal/session/${so.id}`)
|
||||
.set(`kbn-xsrf`, `anything`)
|
|
@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const searchSessions = getService('searchSessions');
|
||||
|
||||
describe('dashboard in space', () => {
|
||||
describe('Send to background in space', () => {
|
||||
describe('Storing search sessions in space', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/session_in_space');
|
||||
|
||||
|
@ -92,5 +92,60 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
await testSubjects.missingOrFail('embeddableErrorLabel');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Disabled storing search sessions', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/session_in_space');
|
||||
|
||||
await security.role.create('data_analyst', {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['logstash-*'], privileges: ['all'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
dashboard: ['minimal_read'],
|
||||
},
|
||||
spaces: ['another-space'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await security.user.create('analyst', {
|
||||
password: 'analyst-password',
|
||||
roles: ['data_analyst'],
|
||||
full_name: 'test user',
|
||||
});
|
||||
|
||||
await PageObjects.security.forceLogout();
|
||||
|
||||
await PageObjects.security.login('analyst', 'analyst-password', {
|
||||
expectSpaceSelector: false,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.role.delete('data_analyst');
|
||||
await security.user.delete('analyst');
|
||||
|
||||
await esArchiver.unload('dashboard/session_in_space');
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it("Doesn't allow to store a session", async () => {
|
||||
await PageObjects.common.navigateToApp('dashboard', { basePath: 's/another-space' });
|
||||
await PageObjects.dashboard.loadSavedDashboard('A Dashboard in another space');
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Sep 1, 2015 @ 00:00:00.000',
|
||||
'Oct 1, 2015 @ 00:00:00.000'
|
||||
);
|
||||
|
||||
await PageObjects.dashboard.waitForRenderComplete();
|
||||
|
||||
await searchSessions.expectState('completed');
|
||||
await searchSessions.disabledOrFail();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -23,7 +23,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
const searchSessions = getService('searchSessions');
|
||||
|
||||
describe('discover in space', () => {
|
||||
describe('Send to background in space', () => {
|
||||
describe('Storing search sessions in space', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/session_in_space');
|
||||
|
||||
|
@ -93,7 +93,62 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) {
|
|||
|
||||
// Check that session is restored
|
||||
await searchSessions.expectState('restored');
|
||||
await testSubjects.missingOrFail('embeddableErrorLabel');
|
||||
await testSubjects.missingOrFail('discoverNoResultsError'); // expect error because of fake searchSessionId
|
||||
});
|
||||
});
|
||||
describe('Disabled storing search sessions in space', () => {
|
||||
before(async () => {
|
||||
await esArchiver.load('dashboard/session_in_space');
|
||||
|
||||
await security.role.create('data_analyst', {
|
||||
elasticsearch: {
|
||||
indices: [{ names: ['logstash-*'], privileges: ['all'] }],
|
||||
},
|
||||
kibana: [
|
||||
{
|
||||
feature: {
|
||||
discover: ['read'],
|
||||
},
|
||||
spaces: ['another-space'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await security.user.create('analyst', {
|
||||
password: 'analyst-password',
|
||||
roles: ['data_analyst'],
|
||||
full_name: 'test user',
|
||||
});
|
||||
|
||||
await PageObjects.security.forceLogout();
|
||||
|
||||
await PageObjects.security.login('analyst', 'analyst-password', {
|
||||
expectSpaceSelector: false,
|
||||
});
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await security.role.delete('data_analyst');
|
||||
await security.user.delete('analyst');
|
||||
|
||||
await esArchiver.unload('dashboard/session_in_space');
|
||||
await PageObjects.security.forceLogout();
|
||||
});
|
||||
|
||||
it("Doesn't allow to store a session", async () => {
|
||||
await PageObjects.common.navigateToApp('discover', { basePath: 's/another-space' });
|
||||
|
||||
await PageObjects.discover.selectIndexPattern('logstash-*');
|
||||
|
||||
await PageObjects.timePicker.setAbsoluteRange(
|
||||
'Sep 1, 2015 @ 00:00:00.000',
|
||||
'Oct 1, 2015 @ 00:00:00.000'
|
||||
);
|
||||
|
||||
await PageObjects.discover.waitForDocTableLoadingComplete();
|
||||
|
||||
await searchSessions.expectState('completed');
|
||||
await searchSessions.disabledOrFail();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile, getService }: FtrProviderContext) {
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('lens search sessions', function () {
|
||||
this.tags('ciGroup3');
|
||||
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' });
|
||||
});
|
||||
|
||||
loadTestFile(require.resolve('./search_sessions.ts'));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../../ftr_provider_context';
|
||||
|
||||
export default function ({ getPageObjects, getService }: FtrProviderContext) {
|
||||
const esArchiver = getService('esArchiver');
|
||||
const searchSession = getService('searchSessions');
|
||||
const PageObjects = getPageObjects(['visualize', 'lens', 'common', 'timePicker', 'header']);
|
||||
const listingTable = getService('listingTable');
|
||||
|
||||
describe('lens search sessions', () => {
|
||||
before(async () => {
|
||||
await esArchiver.loadIfNeeded('logstash_functional');
|
||||
await esArchiver.loadIfNeeded('lens/basic');
|
||||
});
|
||||
after(async () => {
|
||||
await esArchiver.unload('lens/basic');
|
||||
});
|
||||
|
||||
it("doesn't shows search sessions indicator UI", async () => {
|
||||
await PageObjects.visualize.gotoVisualizationLandingPage();
|
||||
await listingTable.searchForItemWithName('lnsXYvis');
|
||||
await PageObjects.lens.clickVisualizeListItemTitle('lnsXYvis');
|
||||
await PageObjects.lens.goToTimeRange();
|
||||
await PageObjects.header.waitUntilLoadingHasFinished();
|
||||
expect(await PageObjects.lens.isShowingNoResults()).to.be(false);
|
||||
|
||||
await searchSession.missingOrFail();
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue