[Search Sessions] Make search session indicator UI opt-in, refactor per-app capabilities (#88699)

This commit is contained in:
Anton Dosov 2021-01-27 11:52:13 +01:00 committed by GitHub
parent 64e9cf0440
commit b8947e3e15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 424 additions and 75 deletions

View file

@ -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) | |

View file

@ -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
```

View file

@ -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),
},
};

View file

@ -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> {

View file

@ -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 };

View file

@ -55,6 +55,7 @@ export interface DashboardCapabilities {
saveQuery: boolean;
createNew: boolean;
show: boolean;
storeSearchSession: boolean;
}
export interface DashboardAppServices {

View file

@ -384,6 +384,7 @@ export {
SearchTimeoutError,
TimeoutErrorMode,
PainlessError,
noSearchSessionStorageCapabilityMessage,
} from './search';
export type {

View file

@ -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

View file

@ -37,6 +37,7 @@ export {
SearchSessionState,
SessionsClient,
ISessionsClient,
noSearchSessionStorageCapabilityMessage,
} from './session';
export { getEsPreference } from './es_search';

View 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.",
}
);

View file

@ -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';

View file

@ -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 }) })),
};
}

View file

@ -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]`);
});
});

View file

@ -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,
};
}
}

View file

@ -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) => {

View file

@ -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,

View file

@ -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}>

View file

@ -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: {

View file

@ -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,
};

View file

@ -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`)

View file

@ -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();
});
});
});
}

View file

@ -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();
});
});
});

View file

@ -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'));
});
}

View file

@ -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();
});
});
}