From ef408068055cd553e2409d5ab0fe686cca695584 Mon Sep 17 00:00:00 2001 From: Tim Sullivan Date: Wed, 20 Jan 2021 09:32:42 -0700 Subject: [PATCH] [Data/Search Sessions] Management UI (#81707) * logging and error handling in session client routes * [Data] Background Search Session Management UI * functional tests * fix ci * new functional tests * fix fn tests * cleanups * cleanup * restore test * configurable refresh and fetch timeout * more tests * feedback items * take expiresSoon field out of the interface * move helper to common * remove bg sessions w/find and delete * add storybook * fix tests * storybook actions * refactor expiration status calculation * isViewable as calculated field * refreshInterval 10s default * list newest first * "Type" => "App" * remove inline view action * in-progress status tooltip shows expire date * move date_string to public * fix tests * Adds management to tsconfig refs * removes preemptive script fix * view action was removed * rename the feature to Search Sessions * Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx Co-authored-by: Liza Katz * Update x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx Co-authored-by: Liza Katz * add TODO * use RedirectAppLinks * code review and react improvements * config * fix test * Fix merge * Fix management test * @Dosant code review * code review * Deleteed story * some more code review stuffs * fix ts * Code review and cleanup * Added functional tests for restoring, reloading and canceling a dashboard Renamed search session test service * Don't show expiration time for canceled, expired or errored sessions * fix jest * Moved UISession to public * @tsullivan code review * Fixed import Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Christiane Heiligers Co-authored-by: Liza Katz Co-authored-by: Liza K --- .../public/search/session/sessions_client.ts | 4 +- .../common/search/session/types.ts | 5 +- x-pack/plugins/data_enhanced/config.ts | 6 + x-pack/plugins/data_enhanced/kibana.json | 11 +- x-pack/plugins/data_enhanced/public/plugin.ts | 16 +- .../search/sessions_mgmt/__mocks__/index.tsx | 18 + .../sessions_mgmt/application/index.tsx | 78 + .../sessions_mgmt/application/render.tsx | 39 + .../components/actions/cancel_button.tsx | 89 + .../components/actions/extend_button.tsx | 89 + .../components/actions/get_action.tsx | 53 + .../components/actions/index.tsx | 8 + .../components/actions/popover_actions.tsx | 135 + .../components/actions/reload_button.tsx | 32 + .../sessions_mgmt/components/actions/types.ts | 13 + .../search/sessions_mgmt/components/index.tsx | 37 + .../sessions_mgmt/components/main.test.tsx | 93 + .../search/sessions_mgmt/components/main.tsx | 79 + .../sessions_mgmt/components/status.test.tsx | 132 + .../sessions_mgmt/components/status.tsx | 203 ++ .../components/table/app_filter.tsx | 27 + .../sessions_mgmt/components/table/index.ts | 7 + .../components/table/status_filter.tsx | 31 + .../components/table/table.test.tsx | 192 ++ .../sessions_mgmt/components/table/table.tsx | 122 + .../sessions_mgmt/icons/extend_session.svg | 3 + .../public/search/sessions_mgmt/index.ts | 64 + .../search/sessions_mgmt/lib/api.test.ts | 214 ++ .../public/search/sessions_mgmt/lib/api.ts | 182 ++ .../search/sessions_mgmt/lib/date_string.ts | 22 + .../search/sessions_mgmt/lib/documentation.ts | 22 + .../sessions_mgmt/lib/get_columns.test.tsx | 208 ++ .../search/sessions_mgmt/lib/get_columns.tsx | 233 ++ .../lib/get_expiration_status.ts | 47 + .../public/search/sessions_mgmt/types.ts | 22 + .../search_session_indicator.tsx | 2 +- x-pack/plugins/data_enhanced/server/plugin.ts | 2 +- .../server/routes/session.test.ts | 6 +- .../data_enhanced/server/routes/session.ts | 9 +- .../search/session/session_service.test.ts | 1 + x-pack/plugins/data_enhanced/tsconfig.json | 2 + .../data/search_sessions/data.json.gz | Bin 0 -> 1956 bytes .../data/search_sessions/mappings.json | 2596 +++++++++++++++++ x-pack/test/functional/page_objects/index.ts | 2 + .../search_sessions_management_page.ts | 60 + .../config.ts | 1 + .../services/index.ts | 2 +- .../services/send_to_background.ts | 65 +- .../async_search/send_to_background.ts | 22 +- .../send_to_background_relative_time.ts | 10 +- .../async_search/sessions_in_space.ts | 10 +- .../tests/apps/discover/sessions_in_space.ts | 10 +- .../apps/management/search_sessions/index.ts | 24 + .../search_sessions/sessions_management.ts | 148 + 54 files changed, 5448 insertions(+), 60 deletions(-) create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts create mode 100644 x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts create mode 100644 x-pack/test/functional/es_archives/data/search_sessions/data.json.gz create mode 100644 x-pack/test/functional/es_archives/data/search_sessions/mappings.json create mode 100644 x-pack/test/functional/page_objects/search_sessions_management_page.ts create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts create mode 100644 x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts diff --git a/src/plugins/data/public/search/session/sessions_client.ts b/src/plugins/data/public/search/session/sessions_client.ts index f4ad2df530d12..5b0ba51c2f344 100644 --- a/src/plugins/data/public/search/session/sessions_client.ts +++ b/src/plugins/data/public/search/session/sessions_client.ts @@ -56,8 +56,8 @@ export class SessionsClient { }); } - public find(options: SavedObjectsFindOptions): Promise { - return this.http!.post(`/internal/session`, { + public find(options: Omit): Promise { + return this.http!.post(`/internal/session/_find`, { body: JSON.stringify(options), }); } diff --git a/x-pack/plugins/data_enhanced/common/search/session/types.ts b/x-pack/plugins/data_enhanced/common/search/session/types.ts index ada7988c31f30..9eefdf43aa245 100644 --- a/x-pack/plugins/data_enhanced/common/search/session/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/session/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SearchSessionStatus } from './'; + export interface SearchSessionSavedObjectAttributes { /** * User-facing session name to be displayed in session management @@ -24,7 +26,7 @@ export interface SearchSessionSavedObjectAttributes { /** * status */ - status: string; + status: SearchSessionStatus; /** * urlGeneratorId */ @@ -44,7 +46,6 @@ export interface SearchSessionSavedObjectAttributes { */ idMapping: Record; } - export interface SearchSessionRequestInfo { /** * ID of the async search request diff --git a/x-pack/plugins/data_enhanced/config.ts b/x-pack/plugins/data_enhanced/config.ts index 4c90b1fb4c81d..981c398019832 100644 --- a/x-pack/plugins/data_enhanced/config.ts +++ b/x-pack/plugins/data_enhanced/config.ts @@ -15,6 +15,12 @@ export const configSchema = schema.object({ inMemTimeout: schema.duration({ defaultValue: '1m' }), maxUpdateRetries: schema.number({ defaultValue: 3 }), defaultExpiration: schema.duration({ defaultValue: '7d' }), + management: schema.object({ + maxSessions: schema.number({ defaultValue: 10000 }), + refreshInterval: schema.duration({ defaultValue: '10s' }), + refreshTimeout: schema.duration({ defaultValue: '1m' }), + expiresSoonWarning: schema.duration({ defaultValue: '1d' }), + }), }), }), }); diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index 3951468f6e569..037f52fcb4b05 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -2,15 +2,8 @@ "id": "dataEnhanced", "version": "8.0.0", "kibanaVersion": "kibana", - "configPath": [ - "xpack", "data_enhanced" - ], - "requiredPlugins": [ - "bfetch", - "data", - "features", - "taskManager" - ], + "configPath": ["xpack", "data_enhanced"], + "requiredPlugins": ["bfetch", "data", "features", "management", "share", "taskManager"], "optionalPlugins": ["kibanaUtils", "usageCollection"], "server": true, "ui": true, diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index fed2b4e71ab50..add7a966fee34 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -8,10 +8,13 @@ import React from 'react'; import { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/public'; import { DataPublicPluginSetup, DataPublicPluginStart } from '../../../../src/plugins/data/public'; import { BfetchPublicSetup } from '../../../../src/plugins/bfetch/public'; +import { ManagementSetup } from '../../../../src/plugins/management/public'; +import { SharePluginStart } from '../../../../src/plugins/share/public'; import { setAutocompleteService } from './services'; import { setupKqlQuerySuggestionProvider, KUERY_LANGUAGE_NAME } from './autocomplete'; import { EnhancedSearchInterceptor } from './search/search_interceptor'; +import { registerSearchSessionsMgmt } from './search/sessions_mgmt'; import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; import { createConnectedSearchSessionIndicator } from './search'; import { ConfigSchema } from '../config'; @@ -19,9 +22,11 @@ import { ConfigSchema } from '../config'; export interface DataEnhancedSetupDependencies { bfetch: BfetchPublicSetup; data: DataPublicPluginSetup; + management: ManagementSetup; } export interface DataEnhancedStartDependencies { data: DataPublicPluginStart; + share: SharePluginStart; } export type DataEnhancedSetup = ReturnType; @@ -30,12 +35,13 @@ export type DataEnhancedStart = ReturnType; export class DataEnhancedPlugin implements Plugin { private enhancedSearchInterceptor!: EnhancedSearchInterceptor; + private config!: ConfigSchema; constructor(private initializerContext: PluginInitializerContext) {} public setup( core: CoreSetup, - { bfetch, data }: DataEnhancedSetupDependencies + { bfetch, data, management }: DataEnhancedSetupDependencies ) { data.autocomplete.addQuerySuggestionProvider( KUERY_LANGUAGE_NAME, @@ -57,12 +63,18 @@ export class DataEnhancedPlugin searchInterceptor: this.enhancedSearchInterceptor, }, }); + + this.config = this.initializerContext.config.get(); + if (this.config.search.sessions.enabled) { + const { management: sessionsMgmtConfig } = this.config.search.sessions; + registerSearchSessionsMgmt(core, sessionsMgmtConfig, { management }); + } } public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { setAutocompleteService(plugins.data.autocomplete); - if (this.initializerContext.config.get().search.sessions.enabled) { + if (this.config.search.sessions.enabled) { core.chrome.setBreadcrumbsAppendExtension({ content: toMountPoint( React.createElement( diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx new file mode 100644 index 0000000000000..e9fc8e6ac6bf9 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/__mocks__/index.tsx @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { ReactNode } from 'react'; +import { IntlProvider } from 'react-intl'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { UrlGeneratorsStart } from '../../../../../../../src/plugins/share/public/url_generators'; + +export function LocaleWrapper({ children }: { children?: ReactNode }) { + return {children}; +} + +export const mockUrls = ({ + getUrlGenerator: (id: string) => ({ createUrl: () => `hello-cool-${id}-url` }), +} as unknown) as UrlGeneratorsStart; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx new file mode 100644 index 0000000000000..27f1482a4d20d --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/index.tsx @@ -0,0 +1,78 @@ +/* + * 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 { CoreSetup } from 'kibana/public'; +import type { ManagementAppMountParams } from 'src/plugins/management/public'; +import type { + AppDependencies, + IManagementSectionsPluginsSetup, + IManagementSectionsPluginsStart, + SessionsMgmtConfigSchema, +} from '../'; +import { APP } from '../'; +import { SearchSessionsMgmtAPI } from '../lib/api'; +import { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { renderApp } from './render'; + +export class SearchSessionsMgmtApp { + constructor( + private coreSetup: CoreSetup, + private config: SessionsMgmtConfigSchema, + private params: ManagementAppMountParams, + private pluginsSetup: IManagementSectionsPluginsSetup + ) {} + + public async mountManagementSection() { + const { coreSetup, params, pluginsSetup } = this; + const [coreStart, pluginsStart] = await coreSetup.getStartServices(); + + const { + chrome: { docTitle }, + http, + docLinks, + i18n, + notifications, + uiSettings, + application, + } = coreStart; + const { data, share } = pluginsStart; + + const pluginName = APP.getI18nName(); + docTitle.change(pluginName); + params.setBreadcrumbs([{ text: pluginName }]); + + const { sessionsClient } = data.search; + const api = new SearchSessionsMgmtAPI(sessionsClient, this.config, { + notifications, + urls: share.urlGenerators, + application, + }); + + const documentation = new AsyncSearchIntroDocumentation(docLinks); + + const dependencies: AppDependencies = { + plugins: pluginsSetup, + config: this.config, + documentation, + core: coreStart, + api, + http, + i18n, + uiSettings, + share, + }; + + const { element } = params; + const unmountAppCb = renderApp(element, dependencies); + + return () => { + docTitle.reset(); + unmountAppCb(); + }; + } +} + +export { renderApp }; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx new file mode 100644 index 0000000000000..f5ee35fcff9a9 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/application/render.tsx @@ -0,0 +1,39 @@ +/* + * 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 React from 'react'; +import { render, unmountComponentAtNode } from 'react-dom'; +import { AppDependencies } from '../'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { SearchSessionsMgmtMain } from '../components/main'; + +export const renderApp = ( + elem: HTMLElement | null, + { i18n, uiSettings, ...homeDeps }: AppDependencies +) => { + if (!elem) { + return () => undefined; + } + + const { Context: I18nContext } = i18n; + // uiSettings is required by the listing table to format dates in the timezone from Settings + const { Provider: KibanaReactContextProvider } = createKibanaReactContext({ + uiSettings, + }); + + render( + + + + + , + elem + ); + + return () => { + unmountComponentAtNode(elem); + }; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx new file mode 100644 index 0000000000000..8f4c8845de235 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/cancel_button.tsx @@ -0,0 +1,89 @@ +/* + * 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 { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { TableText } from '../'; +import { OnActionComplete } from './types'; + +interface CancelButtonProps { + id: string; + name: string; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +const CancelConfirm = ({ + onConfirmDismiss, + ...props +}: CancelButtonProps & { onConfirmDismiss: () => void }) => { + const { id, name, api, onActionComplete } = props; + const [isLoading, setIsLoading] = useState(false); + + const title = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.title', { + defaultMessage: 'Cancel search session', + }); + const confirm = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.cancelButton', { + defaultMessage: 'Cancel', + }); + const cancel = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.dontCancelButton', { + defaultMessage: 'Dismiss', + }); + const message = i18n.translate('xpack.data.mgmt.searchSessions.cancelModal.message', { + defaultMessage: `Canceling the search session \'{name}\' will expire any cached results, so that quick restore will no longer be available. You will still be able to re-run it, using the reload action.`, + values: { + name, + }, + }); + + return ( + + { + setIsLoading(true); + await api.sendCancel(id); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={cancel} + defaultFocusedButton="confirm" + buttonColor="danger" + > + {message} + + + ); +}; + +export const CancelButton = (props: CancelButtonProps) => { + const [showConfirm, setShowConfirm] = useState(false); + + const onClick = () => { + setShowConfirm(true); + }; + + const onConfirmDismiss = () => { + setShowConfirm(false); + }; + + return ( + <> + + + + {showConfirm ? : null} + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx new file mode 100644 index 0000000000000..4c8a7b0217688 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/extend_button.tsx @@ -0,0 +1,89 @@ +/* + * 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 { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import React, { useState } from 'react'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { TableText } from '../'; +import { OnActionComplete } from './types'; + +interface ExtendButtonProps { + id: string; + name: string; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +const ExtendConfirm = ({ + onConfirmDismiss, + ...props +}: ExtendButtonProps & { onConfirmDismiss: () => void }) => { + const { id, name, api, onActionComplete } = props; + const [isLoading, setIsLoading] = useState(false); + + const title = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.title', { + defaultMessage: 'Extend search session expiration', + }); + const confirm = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendButton', { + defaultMessage: 'Extend', + }); + const extend = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.dontExtendButton', { + defaultMessage: 'Cancel', + }); + const message = i18n.translate('xpack.data.mgmt.searchSessions.extendModal.extendMessage', { + defaultMessage: "When would you like the search session '{name}' to expire?", + values: { + name, + }, + }); + + return ( + + { + setIsLoading(true); + await api.sendExtend(id, '1'); + onActionComplete(); + }} + confirmButtonText={confirm} + confirmButtonDisabled={isLoading} + cancelButtonText={extend} + defaultFocusedButton="confirm" + buttonColor="primary" + > + {message} + + + ); +}; + +export const ExtendButton = (props: ExtendButtonProps) => { + const [showConfirm, setShowConfirm] = useState(false); + + const onClick = () => { + setShowConfirm(true); + }; + + const onConfirmDismiss = () => { + setShowConfirm(false); + }; + + return ( + <> + + + + {showConfirm ? : null} + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx new file mode 100644 index 0000000000000..5bf0fbda5b5cc --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/get_action.tsx @@ -0,0 +1,53 @@ +/* + * 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 React from 'react'; +import { IClickActionDescriptor } from '../'; +import extendSessionIcon from '../../icons/extend_session.svg'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { UISession } from '../../types'; +import { CancelButton } from './cancel_button'; +import { ExtendButton } from './extend_button'; +import { ReloadButton } from './reload_button'; +import { ACTION, OnActionComplete } from './types'; + +export const getAction = ( + api: SearchSessionsMgmtAPI, + actionType: string, + { id, name, reloadUrl }: UISession, + onActionComplete: OnActionComplete +): IClickActionDescriptor | null => { + switch (actionType) { + case ACTION.CANCEL: + return { + iconType: 'crossInACircleFilled', + textColor: 'default', + label: , + }; + + case ACTION.RELOAD: + return { + iconType: 'refresh', + textColor: 'default', + label: , + }; + + case ACTION.EXTEND: + return { + iconType: extendSessionIcon, + textColor: 'default', + label: , + }; + + default: + // eslint-disable-next-line no-console + console.error(`Unknown action: ${actionType}`); + } + + // Unknown action: do not show + + return null; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx new file mode 100644 index 0000000000000..82b4d84aa7ea2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/index.tsx @@ -0,0 +1,8 @@ +/* + * 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. + */ + +export { PopoverActionsMenu } from './popover_actions'; +export * from './types'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx new file mode 100644 index 0000000000000..b9b915c0b17cb --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/popover_actions.tsx @@ -0,0 +1,135 @@ +/* + * 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. + */ + +/* + * 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 { + EuiButtonIcon, + EuiContextMenu, + EuiContextMenuPanelDescriptor, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiPopover, + EuiTextProps, + EuiToolTip, +} from '@elastic/eui'; +import { + EuiContextMenuPanelItemDescriptorEntry, + EuiContextMenuPanelItemSeparator, +} from '@elastic/eui/src/components/context_menu/context_menu'; +import { i18n } from '@kbn/i18n'; +import React, { ReactElement, useState } from 'react'; +import { TableText } from '../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { UISession } from '../../types'; +import { getAction } from './get_action'; +import { ACTION, OnActionComplete } from './types'; + +// interfaces +interface PopoverActionProps { + textColor?: EuiTextProps['color']; + iconType: string; + children: string | ReactElement; +} + +interface PopoverActionItemsProps { + session: UISession; + api: SearchSessionsMgmtAPI; + onActionComplete: OnActionComplete; +} + +// helper +const PopoverAction = ({ textColor, iconType, children, ...props }: PopoverActionProps) => ( + + + + + + {children} + + +); + +export const PopoverActionsMenu = ({ api, onActionComplete, session }: PopoverActionItemsProps) => { + const [isPopoverOpen, setPopover] = useState(false); + + const onPopoverClick = () => { + setPopover(!isPopoverOpen); + }; + + const closePopover = () => { + setPopover(false); + }; + + const renderPopoverButton = () => ( + + + + ); + + const actions = session.actions || []; + // Generic set of actions - up to the API to return what is available + const items = actions.reduce((itemSet, actionType) => { + const actionDef = getAction(api, actionType, session, onActionComplete); + if (actionDef) { + const { label, textColor, iconType } = actionDef; + + // add a line above the delete action (when there are multiple) + // NOTE: Delete action MUST be the final action[] item + if (actions.length > 1 && actionType === ACTION.CANCEL) { + itemSet.push({ isSeparator: true, key: 'separadorable' }); + } + + return [ + ...itemSet, + { + key: `action-${actionType}`, + name: ( + + {label} + + ), + }, + ]; + } + return itemSet; + }, [] as Array); + + const panels: EuiContextMenuPanelDescriptor[] = [{ id: 0, items }]; + + return actions.length ? ( + + + + ) : null; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx new file mode 100644 index 0000000000000..9a98ab2044770 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/reload_button.tsx @@ -0,0 +1,32 @@ +/* + * 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 { FormattedMessage } from '@kbn/i18n/react'; +import React from 'react'; +import { TableText } from '../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; + +interface ReloadButtonProps { + api: SearchSessionsMgmtAPI; + reloadUrl: string; +} + +export const ReloadButton = (props: ReloadButtonProps) => { + function onClick() { + props.api.reloadSearchSession(props.reloadUrl); + } + + return ( + <> + + + + + ); +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts new file mode 100644 index 0000000000000..4b81fd7fda9a0 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/actions/types.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export type OnActionComplete = () => void; + +export enum ACTION { + EXTEND = 'extend', + CANCEL = 'cancel', + RELOAD = 'reload', +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx new file mode 100644 index 0000000000000..ffb0992469a8a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/index.tsx @@ -0,0 +1,37 @@ +/* + * 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 { EuiLinkProps, EuiText, EuiTextProps } from '@elastic/eui'; +import React from 'react'; +import extendSessionIcon from '../icons/extend_session.svg'; + +export { OnActionComplete, PopoverActionsMenu } from './actions'; + +export const TableText = ({ children, ...props }: EuiTextProps) => { + return ( + + {children} + + ); +}; + +export interface IClickActionDescriptor { + label: string | React.ReactElement; + iconType: 'trash' | 'cancel' | typeof extendSessionIcon; + textColor: EuiTextProps['color']; +} + +export interface IHrefActionDescriptor { + label: string; + props: EuiLinkProps; +} + +export interface StatusDef { + textColor?: EuiTextProps['color']; + icon?: React.ReactElement; + label: React.ReactElement; + toolTipContent: string; +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx new file mode 100644 index 0000000000000..e01d1a28c5e54 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.test.tsx @@ -0,0 +1,93 @@ +/* + * 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 { MockedKeys } from '@kbn/utility-types/jest'; +import { mount, ReactWrapper } from 'enzyme'; +import { CoreSetup, CoreStart, DocLinksStart } from 'kibana/public'; +import moment from 'moment'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SessionsMgmtConfigSchema } from '..'; +import { SearchSessionsMgmtAPI } from '../lib/api'; +import { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { LocaleWrapper, mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtMain } from './main'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: MockedKeys; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; +let api: SearchSessionsMgmtAPI; + +describe('Background Search Session Management Main', () => { + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + }); + + describe('renders', () => { + const docLinks: DocLinksStart = { + ELASTIC_WEBSITE_URL: 'boo/', + DOC_LINK_VERSION: '#foo', + links: {} as any, + }; + + let main: ReactWrapper; + + beforeEach(async () => { + mockCoreSetup.uiSettings.get.mockImplementation((key: string) => { + return key === 'dateFormat:tz' ? 'UTC' : null; + }); + + await act(async () => { + main = mount( + + + + ); + }); + }); + + test('page title', () => { + expect(main.find('h1').text()).toBe('Search Sessions'); + }); + + test('documentation link', () => { + const docLink = main.find('a[href]').first(); + expect(docLink.text()).toBe('Documentation'); + expect(docLink.prop('href')).toBe( + 'boo/guide/en/elasticsearch/reference/#foo/async-search-intro.html' + ); + }); + + test('table is present', () => { + expect(main.find(`[data-test-subj="search-sessions-mgmt-table"]`).exists()).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx new file mode 100644 index 0000000000000..80c6a580dd183 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/main.tsx @@ -0,0 +1,79 @@ +/* + * 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 { + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiHorizontalRule, + EuiPageBody, + EuiPageContent, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { CoreStart, HttpStart } from 'kibana/public'; +import React from 'react'; +import type { SessionsMgmtConfigSchema } from '../'; +import type { SearchSessionsMgmtAPI } from '../lib/api'; +import type { AsyncSearchIntroDocumentation } from '../lib/documentation'; +import { TableText } from './'; +import { SearchSessionsMgmtTable } from './table'; + +interface Props { + documentation: AsyncSearchIntroDocumentation; + core: CoreStart; + api: SearchSessionsMgmtAPI; + http: HttpStart; + timezone: string; + config: SessionsMgmtConfigSchema; +} + +export function SearchSessionsMgmtMain({ documentation, ...tableProps }: Props) { + return ( + + + + + +

+ +

+
+
+ + + + + +
+ + +

+ +

+
+ + + + +
+
+ ); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx new file mode 100644 index 0000000000000..706001ac42146 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.test.tsx @@ -0,0 +1,132 @@ +/* + * 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 { EuiTextProps, EuiToolTipProps } from '@elastic/eui'; +import { mount } from 'enzyme'; +import React from 'react'; +import { SearchSessionStatus } from '../../../../common/search'; +import { UISession } from '../types'; +import { LocaleWrapper } from '../__mocks__'; +import { getStatusText, StatusIndicator } from './status'; + +let tz: string; +let session: UISession; + +const mockNowTime = new Date(); +mockNowTime.setTime(1607026176061); + +describe('Background Search Session management status labels', () => { + beforeEach(() => { + tz = 'Browser'; + session = { + name: 'amazing test', + id: 'wtywp9u2802hahgp-gsla', + restoreUrl: '/app/great-app-url/#45', + reloadUrl: '/app/great-app-url/#45', + appId: 'security', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }; + }); + + describe('getStatusText', () => { + test('in progress', () => { + expect(getStatusText(SearchSessionStatus.IN_PROGRESS)).toBe('In progress'); + }); + test('expired', () => { + expect(getStatusText(SearchSessionStatus.EXPIRED)).toBe('Expired'); + }); + test('cancelled', () => { + expect(getStatusText(SearchSessionStatus.CANCELLED)).toBe('Cancelled'); + }); + test('complete', () => { + expect(getStatusText(SearchSessionStatus.COMPLETE)).toBe('Complete'); + }); + test('error', () => { + expect(getStatusText('error')).toBe('Error'); + }); + }); + + describe('StatusIndicator', () => { + test('render in progress', () => { + const statusIndicator = mount( + + + + ); + + const label = statusIndicator.find( + `.euiText[data-test-subj="sessionManagementStatusLabel"][data-test-status="in_progress"]` + ); + expect(label.text()).toMatchInlineSnapshot(`"In progress"`); + }); + + test('complete', () => { + session.status = SearchSessionStatus.COMPLETE; + + const statusIndicator = mount( + + + + ); + + const label = statusIndicator + .find(`[data-test-subj="sessionManagementStatusLabel"][data-test-status="complete"]`) + .first(); + expect((label.props() as EuiTextProps).color).toBe('secondary'); + expect(label.text()).toBe('Complete'); + }); + + test('complete - expires soon', () => { + session.status = SearchSessionStatus.COMPLETE; + + const statusIndicator = mount( + + + + ); + + const tooltip = statusIndicator.find('EuiToolTip'); + expect((tooltip.first().props() as EuiToolTipProps).content).toMatchInlineSnapshot( + `"Expires on 6 Dec, 2020, 19:19:32"` + ); + }); + + test('expired', () => { + session.status = SearchSessionStatus.EXPIRED; + + const statusIndicator = mount( + + + + ); + + const label = statusIndicator + .find(`[data-test-subj="sessionManagementStatusLabel"][data-test-status="expired"]`) + .first(); + expect(label.text()).toBe('Expired'); + }); + + test('error handling', () => { + session.status = SearchSessionStatus.COMPLETE; + (session as any).created = null; + (session as any).expires = null; + + const statusIndicator = mount( + + + + ); + + // no unhandled errors + const tooltip = statusIndicator.find('EuiToolTip'); + expect((tooltip.first().props() as EuiToolTipProps).content).toMatchInlineSnapshot( + `"Expires on unknown"` + ); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx new file mode 100644 index 0000000000000..8e0946c140287 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/status.tsx @@ -0,0 +1,203 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React, { ReactElement } from 'react'; +import { SearchSessionStatus } from '../../../../common/search'; +import { dateString } from '../lib/date_string'; +import { UISession } from '../types'; +import { StatusDef as StatusAttributes, TableText } from './'; + +// Shared helper function +export const getStatusText = (statusType: string): string => { + switch (statusType) { + case SearchSessionStatus.IN_PROGRESS: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.inProgress', { + defaultMessage: 'In progress', + }); + case SearchSessionStatus.EXPIRED: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.expired', { + defaultMessage: 'Expired', + }); + case SearchSessionStatus.CANCELLED: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.cancelled', { + defaultMessage: 'Cancelled', + }); + case SearchSessionStatus.COMPLETE: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.complete', { + defaultMessage: 'Complete', + }); + case SearchSessionStatus.ERROR: + return i18n.translate('xpack.data.mgmt.searchSessions.status.label.error', { + defaultMessage: 'Error', + }); + default: + // eslint-disable-next-line no-console + console.error(`Unknown status ${statusType}`); + return statusType; + } +}; + +interface StatusIndicatorProps { + now?: string; + session: UISession; + timezone: string; +} + +// Get the fields needed to show each status type +// can throw errors around date conversions +const getStatusAttributes = ({ + now, + session, + timezone, +}: StatusIndicatorProps): StatusAttributes | null => { + let expireDate: string; + if (session.expires) { + expireDate = dateString(session.expires!, timezone); + } else { + expireDate = i18n.translate('xpack.data.mgmt.searchSessions.status.expireDateUnknown', { + defaultMessage: 'unknown', + }); + } + + switch (session.status) { + case SearchSessionStatus.IN_PROGRESS: + try { + return { + textColor: 'default', + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate( + 'xpack.data.mgmt.searchSessions.status.message.createdOn', + { + defaultMessage: 'Expires on {expireDate}', + values: { expireDate }, + } + ), + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error(`Could not instantiate a createdDate object from: ${session.created}`); + } + + case SearchSessionStatus.EXPIRED: + try { + const toolTipContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.message.expiredOn', + { + defaultMessage: 'Expired on {expireDate}', + values: { expireDate }, + } + ); + + return { + icon: , + label: {getStatusText(session.status)}, + toolTipContent, + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error(`Could not instantiate an expiration Date object from: ${session.expires}`); + } + + case SearchSessionStatus.CANCELLED: + return { + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate('xpack.data.mgmt.searchSessions.status.message.cancelled', { + defaultMessage: 'Cancelled by user', + }), + }; + + case SearchSessionStatus.ERROR: + return { + textColor: 'danger', + icon: , + label: {getStatusText(session.status)}, + toolTipContent: i18n.translate('xpack.data.mgmt.searchSessions.status.message.error', { + defaultMessage: 'Error: {error}', + values: { error: (session as any).error || 'unknown' }, + }), + }; + + case SearchSessionStatus.COMPLETE: + try { + const toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresOn', { + defaultMessage: 'Expires on {expireDate}', + values: { expireDate }, + }); + + return { + textColor: 'secondary', + icon: , + label: {getStatusText(session.status)}, + toolTipContent, + }; + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + throw new Error( + `Could not instantiate an expiration Date object for completed session from: ${session.expires}` + ); + } + + // Error was thrown + return null; + + default: + throw new Error(`Unknown status: ${session.status}`); + } +}; + +export const StatusIndicator = (props: StatusIndicatorProps) => { + try { + const statusDef = getStatusAttributes(props); + const { session } = props; + + if (statusDef) { + const { toolTipContent } = statusDef; + let icon: ReactElement | undefined = statusDef.icon; + let label: ReactElement = statusDef.label; + + if (icon && toolTipContent) { + icon = {icon}; + } + if (toolTipContent) { + label = ( + + + {statusDef.label} + + + ); + } + + return ( + + {icon} + + + {label} + + + + ); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + } + + // Exception has been caught + return {props.session.status}; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx new file mode 100644 index 0000000000000..236fc492031c0 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/app_filter.tsx @@ -0,0 +1,27 @@ +/* + * 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 { FieldValueOptionType, SearchFilterConfig } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { capitalize } from 'lodash'; +import { UISession } from '../../types'; + +export const getAppFilter: (tableData: UISession[]) => SearchFilterConfig = (tableData) => ({ + type: 'field_value_selection', + name: i18n.translate('xpack.data.mgmt.searchSessions.search.filterApp', { + defaultMessage: 'App', + }), + field: 'appId', + multiSelect: 'or', + options: tableData.reduce((options: FieldValueOptionType[], { appId }) => { + const existingOption = options.find((o) => o.value === appId); + if (!existingOption) { + return [...options, { value: appId, view: capitalize(appId) }]; + } + + return options; + }, []), +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts new file mode 100644 index 0000000000000..83ca1c223dfc4 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/index.ts @@ -0,0 +1,7 @@ +/* + * 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. + */ + +export { SearchSessionsMgmtTable } from './table'; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx new file mode 100644 index 0000000000000..04421ad66e588 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/status_filter.tsx @@ -0,0 +1,31 @@ +/* + * 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 { FieldValueOptionType, SearchFilterConfig } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { TableText } from '../'; +import { UISession } from '../../types'; +import { getStatusText } from '../status'; + +export const getStatusFilter: (tableData: UISession[]) => SearchFilterConfig = (tableData) => ({ + type: 'field_value_selection', + name: i18n.translate('xpack.data.mgmt.searchSessions.search.filterStatus', { + defaultMessage: 'Status', + }), + field: 'status', + multiSelect: 'or', + options: tableData.reduce((options: FieldValueOptionType[], session) => { + const { status: statusType } = session; + const existingOption = options.find((o) => o.value === statusType); + if (!existingOption) { + const view = {getStatusText(session.status)}; + return [...options, { value: statusType, view }]; + } + + return options; + }, []), +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx new file mode 100644 index 0000000000000..357f17649394b --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.test.tsx @@ -0,0 +1,192 @@ +/* + * 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 { MockedKeys } from '@kbn/utility-types/jest'; +import { act, waitFor } from '@testing-library/react'; +import { mount, ReactWrapper } from 'enzyme'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import React from 'react'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SearchSessionStatus } from '../../../../../common/search'; +import { SessionsMgmtConfigSchema } from '../../'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { LocaleWrapper, mockUrls } from '../../__mocks__'; +import { SearchSessionsMgmtTable } from './table'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: CoreStart; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; +let api: SearchSessionsMgmtAPI; + +describe('Background Search Session Management Table', () => { + beforeEach(async () => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + }); + + describe('renders', () => { + let table: ReactWrapper; + + const getInitialResponse = () => { + return { + saved_objects: [ + { + id: 'wtywp9u2802hahgp-flps', + attributes: { + name: 'very background search', + id: 'wtywp9u2802hahgp-flps', + url: '/app/great-app-url/#48', + appId: 'canvas', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }, + }, + ], + }; + }; + + test('table header cells', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return getInitialResponse(); + }); + + await act(async () => { + table = mount( + + + + ); + }); + + expect(table.find('thead th').map((node) => node.text())).toMatchInlineSnapshot(` + Array [ + "AppClick to sort in ascending order", + "NameClick to sort in ascending order", + "StatusClick to sort in ascending order", + "CreatedClick to unsort", + "ExpirationClick to sort in ascending order", + ] + `); + }); + + test('table body cells', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return getInitialResponse(); + }); + + await act(async () => { + table = mount( + + + + ); + }); + table.update(); + + expect(table.find('tbody td').map((node) => node.text())).toMatchInlineSnapshot(` + Array [ + "App", + "Namevery background search", + "StatusIn progress", + "Created2 Dec, 2020, 00:19:32", + "Expiration7 Dec, 2020, 00:19:32", + "", + "", + ] + `); + }); + }); + + describe('fetching sessions data', () => { + test('re-fetches data', async () => { + jest.useFakeTimers(); + sessionsClient.find = jest.fn(); + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(10, 'seconds'), + }; + + await act(async () => { + mount( + + + + ); + jest.advanceTimersByTime(20000); + }); + + // 1 for initial load + 2 refresh calls + expect(sessionsClient.find).toBeCalledTimes(3); + + jest.useRealTimers(); + }); + + test('refresh button uses the session client', async () => { + sessionsClient.find = jest.fn(); + + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(1, 'day'), + refreshTimeout: moment.duration(2, 'days'), + }; + + await act(async () => { + const table = mount( + + + + ); + + const buttonSelector = `[data-test-subj="sessionManagementRefreshBtn"] button`; + + await waitFor(() => { + table.find(buttonSelector).first().simulate('click'); + table.update(); + }); + }); + + // initial call + click + expect(sessionsClient.find).toBeCalledTimes(2); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx new file mode 100644 index 0000000000000..f7aecdbd58a23 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/components/table/table.tsx @@ -0,0 +1,122 @@ +/* + * 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 { EuiButton, EuiInMemoryTable, EuiSearchBarProps } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { CoreStart } from 'kibana/public'; +import moment from 'moment'; +import React, { useCallback, useMemo, useRef, useEffect, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import useInterval from 'react-use/lib/useInterval'; +import { TableText } from '../'; +import { SessionsMgmtConfigSchema } from '../..'; +import { SearchSessionsMgmtAPI } from '../../lib/api'; +import { getColumns } from '../../lib/get_columns'; +import { UISession } from '../../types'; +import { OnActionComplete } from '../actions'; +import { getAppFilter } from './app_filter'; +import { getStatusFilter } from './status_filter'; + +const TABLE_ID = 'searchSessionsMgmtTable'; + +interface Props { + core: CoreStart; + api: SearchSessionsMgmtAPI; + timezone: string; + config: SessionsMgmtConfigSchema; +} + +export function SearchSessionsMgmtTable({ core, api, timezone, config, ...props }: Props) { + const [tableData, setTableData] = useState([]); + const [isLoading, setIsLoading] = useState(false); + const [debouncedIsLoading, setDebouncedIsLoading] = useState(false); + const [pagination, setPagination] = useState({ pageIndex: 0 }); + const showLatestResultsHandler = useRef(); + const refreshInterval = useMemo(() => moment.duration(config.refreshInterval).asMilliseconds(), [ + config.refreshInterval, + ]); + + // Debounce rendering the state of the Refresh button + useDebounce( + () => { + setDebouncedIsLoading(isLoading); + }, + 250, + [isLoading] + ); + + // refresh behavior + const doRefresh = useCallback(async () => { + setIsLoading(true); + const renderResults = (results: UISession[]) => { + setTableData(results); + }; + showLatestResultsHandler.current = renderResults; + let results: UISession[] = []; + try { + results = await api.fetchTableData(); + } catch (e) {} // eslint-disable-line no-empty + + if (showLatestResultsHandler.current === renderResults) { + renderResults(results); + setIsLoading(false); + } + }, [api]); + + // initial data load + useEffect(() => { + doRefresh(); + }, [doRefresh]); + + useInterval(doRefresh, refreshInterval); + + const onActionComplete: OnActionComplete = () => { + doRefresh(); + }; + + // table config: search / filters + const search: EuiSearchBarProps = { + box: { incremental: true }, + filters: [getStatusFilter(tableData), getAppFilter(tableData)], + toolsRight: ( + + + + + + ), + }; + + return ( + + {...props} + id={TABLE_ID} + data-test-subj={TABLE_ID} + rowProps={() => ({ + 'data-test-subj': 'searchSessionsRow', + })} + columns={getColumns(core, api, config, timezone, onActionComplete)} + items={tableData} + pagination={pagination} + search={search} + sorting={{ sort: { field: 'created', direction: 'desc' } }} + onTableChange={({ page: { index } }) => { + setPagination({ pageIndex: index }); + }} + tableLayout="auto" + /> + ); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg new file mode 100644 index 0000000000000..7cb9f7e6a24c2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/icons/extend_session.svg @@ -0,0 +1,3 @@ + + + diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts new file mode 100644 index 0000000000000..76a5d440cd898 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/index.ts @@ -0,0 +1,64 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { CoreStart, HttpStart, I18nStart, IUiSettingsClient } from 'kibana/public'; +import { CoreSetup } from 'kibana/public'; +import type { DataPublicPluginStart } from 'src/plugins/data/public'; +import type { ManagementSetup } from 'src/plugins/management/public'; +import type { SharePluginStart } from 'src/plugins/share/public'; +import type { ConfigSchema } from '../../../config'; +import type { DataEnhancedStartDependencies } from '../../plugin'; +import type { SearchSessionsMgmtAPI } from './lib/api'; +import type { AsyncSearchIntroDocumentation } from './lib/documentation'; + +export interface IManagementSectionsPluginsSetup { + management: ManagementSetup; +} + +export interface IManagementSectionsPluginsStart { + data: DataPublicPluginStart; + share: SharePluginStart; +} + +export interface AppDependencies { + plugins: IManagementSectionsPluginsSetup; + share: SharePluginStart; + uiSettings: IUiSettingsClient; + documentation: AsyncSearchIntroDocumentation; + core: CoreStart; // for RedirectAppLinks + api: SearchSessionsMgmtAPI; + http: HttpStart; + i18n: I18nStart; + config: SessionsMgmtConfigSchema; +} + +export const APP = { + id: 'search_sessions', + getI18nName: (): string => + i18n.translate('xpack.data.mgmt.searchSessions.appTitle', { + defaultMessage: 'Search Sessions', + }), +}; + +export type SessionsMgmtConfigSchema = ConfigSchema['search']['sessions']['management']; + +export function registerSearchSessionsMgmt( + coreSetup: CoreSetup, + config: SessionsMgmtConfigSchema, + services: IManagementSectionsPluginsSetup +) { + services.management.sections.section.kibana.registerApp({ + id: APP.id, + title: APP.getI18nName(), + order: 2, + mount: async (params) => { + const { SearchSessionsMgmtApp: MgmtApp } = await import('./application'); + const mgmtApp = new MgmtApp(coreSetup, config, params, services); + return mgmtApp.mountManagementSection(); + }, + }); +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts new file mode 100644 index 0000000000000..5b337dfd03eb1 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.test.ts @@ -0,0 +1,214 @@ +/* + * 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 type { MockedKeys } from '@kbn/utility-types/jest'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import { coreMock } from 'src/core/public/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { SavedObjectsFindResponse } from 'src/core/server'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import type { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtAPI } from './api'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: MockedKeys; +let mockConfig: SessionsMgmtConfigSchema; +let sessionsClient: SessionsClient; + +describe('Search Sessions Management API', () => { + beforeEach(() => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration('1d'), + maxSessions: 2000, + refreshInterval: moment.duration('1s'), + refreshTimeout: moment.duration('10m'), + }; + + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + }); + + describe('listing', () => { + test('fetchDataTable calls the listing endpoint', async () => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: 'complete' }, + }, + ], + } as SavedObjectsFindResponse; + }); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + expect(await api.fetchTableData()).toMatchInlineSnapshot(` + Array [ + Object { + "actions": Array [ + "reload", + "extend", + "cancel", + ], + "appId": "pizza", + "created": undefined, + "expires": undefined, + "id": "hello-pizza-123", + "name": "Veggie", + "reloadUrl": "hello-cool-undefined-url", + "restoreUrl": "hello-cool-undefined-url", + "status": "complete", + }, + ] + `); + }); + + test('handle error from sessionsClient response', async () => { + sessionsClient.find = jest.fn().mockRejectedValue(new Error('implementation is so bad')); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.fetchTableData(); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('implementation is so bad'), + { title: 'Failed to refresh the page!' } + ); + }); + + test('handle timeout error', async () => { + mockConfig = { + ...mockConfig, + refreshInterval: moment.duration(1, 'hours'), + refreshTimeout: moment.duration(1, 'seconds'), + }; + + sessionsClient.find = jest.fn().mockImplementation(async () => { + return new Promise((resolve) => { + setTimeout(resolve, 2000); + }); + }); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.fetchTableData(); + + expect(mockCoreStart.notifications.toasts.addDanger).toHaveBeenCalledWith( + 'Fetching the Search Session info timed out after 1 seconds' + ); + }); + }); + + describe('cancel', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: 'baked' }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send cancel calls the cancel endpoint with a session ID', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendCancel('abc-123-cool-session-ID'); + + expect(mockCoreStart.notifications.toasts.addSuccess).toHaveBeenCalledWith({ + title: 'The search session was canceled and expired.', + }); + }); + + test('error if deleting shows a toast message', async () => { + sessionsClient.delete = jest.fn().mockRejectedValue(new Error('implementation is so bad')); + + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendCancel('abc-123-cool-session-ID'); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalledWith( + new Error('implementation is so bad'), + { title: 'Failed to cancel the search session!' } + ); + }); + }); + + describe('reload', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: SearchSessionStatus.COMPLETE }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send cancel calls the cancel endpoint with a session ID', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.reloadSearchSession('www.myurl.com'); + + expect(mockCoreStart.application.navigateToUrl).toHaveBeenCalledWith('www.myurl.com'); + }); + }); + + describe('extend', () => { + beforeEach(() => { + sessionsClient.find = jest.fn().mockImplementation(async () => { + return { + saved_objects: [ + { + id: 'hello-pizza-123', + attributes: { name: 'Veggie', appId: 'pizza', status: SearchSessionStatus.COMPLETE }, + }, + ], + } as SavedObjectsFindResponse; + }); + }); + + test('send extend throws an error for now', async () => { + const api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + await api.sendExtend('my-id', '5d'); + + expect(mockCoreStart.notifications.toasts.addError).toHaveBeenCalled(); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts new file mode 100644 index 0000000000000..a2bd6b1a549be --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/api.ts @@ -0,0 +1,182 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import type { ApplicationStart, NotificationsStart, SavedObject } from 'kibana/public'; +import moment from 'moment'; +import { from, race, timer } from 'rxjs'; +import { mapTo, tap } from 'rxjs/operators'; +import type { SharePluginStart } from 'src/plugins/share/public'; +import { SessionsMgmtConfigSchema } from '../'; +import type { ISessionsClient } from '../../../../../../../src/plugins/data/public'; +import type { SearchSessionSavedObjectAttributes } from '../../../../common'; +import { SearchSessionStatus } from '../../../../common/search'; +import { ACTION } from '../components/actions'; +import { UISession } from '../types'; + +type UrlGeneratorsStart = SharePluginStart['urlGenerators']; + +function getActions(status: SearchSessionStatus) { + const actions: ACTION[] = []; + actions.push(ACTION.RELOAD); + if (status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE) { + actions.push(ACTION.EXTEND); + actions.push(ACTION.CANCEL); + } + return actions; +} + +async function getUrlFromState( + urls: UrlGeneratorsStart, + urlGeneratorId: string, + state: Record +) { + let url = '/'; + try { + url = await urls.getUrlGenerator(urlGeneratorId).createUrl(state); + } catch (err) { + // eslint-disable-next-line no-console + console.error('Could not create URL from restoreState'); + // eslint-disable-next-line no-console + console.error(err); + } + return url; +} + +// Helper: factory for a function to map server objects to UI objects +const mapToUISession = ( + urls: UrlGeneratorsStart, + { expiresSoonWarning }: SessionsMgmtConfigSchema +) => async (savedObject: SavedObject): Promise => { + const { + name, + appId, + created, + expires, + status, + urlGeneratorId, + initialState, + restoreState, + } = savedObject.attributes; + + const actions = getActions(status); + + // TODO: initialState should be saved without the searchSessionID + if (initialState) delete initialState.searchSessionId; + // derive the URL and add it in + const reloadUrl = await getUrlFromState(urls, urlGeneratorId, initialState); + const restoreUrl = await getUrlFromState(urls, urlGeneratorId, restoreState); + + return { + id: savedObject.id, + name, + appId, + created, + expires, + status, + actions, + restoreUrl, + reloadUrl, + }; +}; + +interface SearcgSessuibManagementDeps { + urls: UrlGeneratorsStart; + notifications: NotificationsStart; + application: ApplicationStart; +} + +export class SearchSessionsMgmtAPI { + constructor( + private sessionsClient: ISessionsClient, + private config: SessionsMgmtConfigSchema, + private deps: SearcgSessuibManagementDeps + ) {} + + public async fetchTableData(): Promise { + interface FetchResult { + saved_objects: object[]; + } + + const refreshTimeout = moment.duration(this.config.refreshTimeout); + + const fetch$ = from( + this.sessionsClient.find({ + page: 1, + perPage: this.config.maxSessions, + sortField: 'created', + sortOrder: 'asc', + }) + ); + const timeout$ = timer(refreshTimeout.asMilliseconds()).pipe( + tap(() => { + this.deps.notifications.toasts.addDanger( + i18n.translate('xpack.data.mgmt.searchSessions.api.fetchTimeout', { + defaultMessage: 'Fetching the Search Session info timed out after {timeout} seconds', + values: { timeout: refreshTimeout.asSeconds() }, + }) + ); + }), + mapTo(null) + ); + + // fetch the search sessions before timeout triggers + try { + const result = await race(fetch$, timeout$).toPromise(); + if (result && result.saved_objects) { + const savedObjects = result.saved_objects as Array< + SavedObject + >; + return await Promise.all(savedObjects.map(mapToUISession(this.deps.urls, this.config))); + } + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.fetchError', { + defaultMessage: 'Failed to refresh the page!', + }), + }); + } + + return []; + } + + public reloadSearchSession(reloadUrl: string) { + this.deps.application.navigateToUrl(reloadUrl); + } + + // Cancel and expire + public async sendCancel(id: string): Promise { + try { + await this.sessionsClient.delete(id); + + this.deps.notifications.toasts.addSuccess({ + title: i18n.translate('xpack.data.mgmt.searchSessions.api.canceled', { + defaultMessage: 'The search session was canceled and expired.', + }), + }); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + + this.deps.notifications.toasts.addError(err, { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.cancelError', { + defaultMessage: 'Failed to cancel the search session!', + }), + }); + } + } + + // Extend + public async sendExtend(id: string, ttl: string): Promise { + this.deps.notifications.toasts.addError(new Error('Not implemented'), { + title: i18n.translate('xpack.data.mgmt.searchSessions.api.extendError', { + defaultMessage: 'Failed to extend the session expiration!', + }), + }); + } +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts new file mode 100644 index 0000000000000..7640d8b80766e --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/date_string.ts @@ -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 moment from 'moment'; +import { DATE_STRING_FORMAT } from '../types'; + +export const dateString = (inputString: string, tz: string): string => { + if (inputString == null) { + throw new Error('Invalid date string!'); + } + let returnString: string; + if (tz === 'Browser') { + returnString = moment.utc(inputString).tz(moment.tz.guess()).format(DATE_STRING_FORMAT); + } else { + returnString = moment(inputString).tz(tz).format(DATE_STRING_FORMAT); + } + + return returnString; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts new file mode 100644 index 0000000000000..eac3245dfe2bc --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/documentation.ts @@ -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 { DocLinksStart } from 'kibana/public'; + +export class AsyncSearchIntroDocumentation { + private docsBasePath: string = ''; + + constructor(docs: DocLinksStart) { + const { DOC_LINK_VERSION, ELASTIC_WEBSITE_URL } = docs; + const docsBase = `${ELASTIC_WEBSITE_URL}guide/en`; + // TODO: There should be Kibana documentation link about Search Sessions in Kibana + this.docsBasePath = `${docsBase}/elasticsearch/reference/${DOC_LINK_VERSION}`; + } + + public getElasticsearchDocLink() { + return `${this.docsBasePath}/async-search-intro.html`; + } +} diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx new file mode 100644 index 0000000000000..ce441efea7385 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.test.tsx @@ -0,0 +1,208 @@ +/* + * 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 { EuiTableFieldDataColumnType } from '@elastic/eui'; +import { MockedKeys } from '@kbn/utility-types/jest'; +import { mount } from 'enzyme'; +import { CoreSetup, CoreStart } from 'kibana/public'; +import moment from 'moment'; +import { ReactElement } from 'react'; +import { coreMock } from 'src/core/public/mocks'; +import { SessionsClient } from 'src/plugins/data/public/search'; +import { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { OnActionComplete } from '../components'; +import { UISession } from '../types'; +import { mockUrls } from '../__mocks__'; +import { SearchSessionsMgmtAPI } from './api'; +import { getColumns } from './get_columns'; + +let mockCoreSetup: MockedKeys; +let mockCoreStart: CoreStart; +let mockConfig: SessionsMgmtConfigSchema; +let api: SearchSessionsMgmtAPI; +let sessionsClient: SessionsClient; +let handleAction: OnActionComplete; +let mockSession: UISession; + +let tz = 'UTC'; + +describe('Search Sessions Management table column factory', () => { + beforeEach(async () => { + mockCoreSetup = coreMock.createSetup(); + mockCoreStart = coreMock.createStart(); + mockConfig = { + expiresSoonWarning: moment.duration(1, 'days'), + maxSessions: 2000, + refreshInterval: moment.duration(1, 'seconds'), + refreshTimeout: moment.duration(10, 'minutes'), + }; + sessionsClient = new SessionsClient({ http: mockCoreSetup.http }); + + api = new SearchSessionsMgmtAPI(sessionsClient, mockConfig, { + urls: mockUrls, + notifications: mockCoreStart.notifications, + application: mockCoreStart.application, + }); + tz = 'UTC'; + + handleAction = () => { + throw new Error('not testing handle action'); + }; + + mockSession = { + name: 'Cool mock session', + id: 'wtywp9u2802hahgp-thao', + reloadUrl: '/app/great-app-url', + restoreUrl: '/app/great-app-url/#42', + appId: 'discovery', + status: SearchSessionStatus.IN_PROGRESS, + created: '2020-12-02T00:19:32Z', + expires: '2020-12-07T00:19:32Z', + }; + }); + + test('returns columns', () => { + const columns = getColumns(mockCoreStart, api, mockConfig, tz, handleAction); + expect(columns).toMatchInlineSnapshot(` + Array [ + Object { + "field": "appId", + "name": "App", + "render": [Function], + "sortable": true, + }, + Object { + "field": "name", + "name": "Name", + "render": [Function], + "sortable": true, + "width": "20%", + }, + Object { + "field": "status", + "name": "Status", + "render": [Function], + "sortable": true, + }, + Object { + "field": "created", + "name": "Created", + "render": [Function], + "sortable": true, + }, + Object { + "field": "expires", + "name": "Expiration", + "render": [Function], + "sortable": true, + }, + Object { + "field": "status", + "name": "", + "render": [Function], + "sortable": false, + }, + Object { + "field": "actions", + "name": "", + "render": [Function], + "sortable": false, + }, + ] + `); + }); + + describe('name', () => { + test('rendering', () => { + const [, nameColumn] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + const name = mount(nameColumn.render!(mockSession.name, mockSession) as ReactElement); + + expect(name.text()).toBe('Cool mock session'); + }); + }); + + // Status column + describe('status', () => { + test('render in_progress', () => { + const [, , status] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement); + expect( + statusLine.find('.euiText[data-test-subj="sessionManagementStatusTooltip"]').text() + ).toMatchInlineSnapshot(`"In progress"`); + }); + + test('error handling', () => { + const [, , status] = getColumns(mockCoreStart, api, mockConfig, tz, handleAction) as Array< + EuiTableFieldDataColumnType + >; + + mockSession.status = 'INVALID' as SearchSessionStatus; + const statusLine = mount(status.render!(mockSession.status, mockSession) as ReactElement); + + // no unhandled error + + expect(statusLine.text()).toMatchInlineSnapshot(`"INVALID"`); + }); + }); + + // Start Date column + describe('startedDate', () => { + test('render using Browser timezone', () => { + tz = 'Browser'; + + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + expect(date.text()).toBe('1 Dec, 2020, 19:19:32'); + }); + + test('render using AK timezone', () => { + tz = 'US/Alaska'; + + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + expect(date.text()).toBe('1 Dec, 2020, 15:19:32'); + }); + + test('error handling', () => { + const [, , , createdDateCol] = getColumns( + mockCoreStart, + api, + mockConfig, + tz, + handleAction + ) as Array>; + + mockSession.created = 'INVALID'; + const date = mount(createdDateCol.render!(mockSession.created, mockSession) as ReactElement); + + // no unhandled error + expect(date.text()).toBe('Invalid date'); + }); + }); +}); diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx new file mode 100644 index 0000000000000..090336c37a98f --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_columns.tsx @@ -0,0 +1,233 @@ +/* + * 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 { + EuiBadge, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiIcon, + EuiIconTip, + EuiLink, + EuiToolTip, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { CoreStart } from 'kibana/public'; +import { capitalize } from 'lodash'; +import React from 'react'; +import { FormattedMessage } from 'react-intl'; +import { RedirectAppLinks } from '../../../../../../../src/plugins/kibana_react/public'; +import { SessionsMgmtConfigSchema } from '../'; +import { SearchSessionStatus } from '../../../../common/search'; +import { TableText } from '../components'; +import { OnActionComplete, PopoverActionsMenu } from '../components'; +import { StatusIndicator } from '../components/status'; +import { dateString } from '../lib/date_string'; +import { SearchSessionsMgmtAPI } from './api'; +import { getExpirationStatus } from './get_expiration_status'; +import { UISession } from '../types'; + +// Helper function: translate an app string to EuiIcon-friendly string +const appToIcon = (app: string) => { + if (app === 'dashboards') { + return 'dashboard'; + } + return app; +}; + +function isSessionRestorable(status: SearchSessionStatus) { + return status === SearchSessionStatus.IN_PROGRESS || status === SearchSessionStatus.COMPLETE; +} + +export const getColumns = ( + core: CoreStart, + api: SearchSessionsMgmtAPI, + config: SessionsMgmtConfigSchema, + timezone: string, + onActionComplete: OnActionComplete +): Array> => { + // Use a literal array of table column definitions to detail a UISession object + return [ + // App + { + field: 'appId', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerType', { + defaultMessage: 'App', + }), + sortable: true, + render: (appId: UISession['appId'], { id }) => { + const app = `${appToIcon(appId)}`; + return ( + + + + ); + }, + }, + + // Name, links to app and displays the search session data + { + field: 'name', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerName', { + defaultMessage: 'Name', + }), + sortable: true, + width: '20%', + render: (name: UISession['name'], { restoreUrl, reloadUrl, status }) => { + const isRestorable = isSessionRestorable(status); + const notRestorableWarning = isRestorable ? null : ( + <> + {' '} + + } + /> + + ); + return ( + + + + {name} + {notRestorableWarning} + + + + ); + }, + }, + + // Session status + { + field: 'status', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerStatus', { + defaultMessage: 'Status', + }), + sortable: true, + render: (statusType: UISession['status'], session) => ( + + ), + }, + + // Started date + { + field: 'created', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerStarted', { + defaultMessage: 'Created', + }), + sortable: true, + render: (created: UISession['created'], { id }) => { + try { + const startedOn = dateString(created, timezone); + return ( + + {startedOn} + + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + return {created}; + } + }, + }, + + // Expiration date + { + field: 'expires', + name: i18n.translate('xpack.data.mgmt.searchSessions.table.headerExpiration', { + defaultMessage: 'Expiration', + }), + sortable: true, + render: (expires: UISession['expires'], { id, status }) => { + if ( + expires && + status !== SearchSessionStatus.EXPIRED && + status !== SearchSessionStatus.CANCELLED && + status !== SearchSessionStatus.ERROR + ) { + try { + const expiresOn = dateString(expires, timezone); + + // return + return ( + + {expiresOn} + + ); + } catch (err) { + // eslint-disable-next-line no-console + console.error(err); + return {expires}; + } + } + return ( + + -- + + ); + }, + }, + + // Highlight Badge, if completed session expires soon + { + field: 'status', + name: '', + sortable: false, + render: (status, { expires }) => { + const expirationStatus = getExpirationStatus(config, expires); + if (expirationStatus) { + const { toolTipContent, statusContent } = expirationStatus; + + return ( + + + {statusContent} + + + ); + } + + return ; + }, + }, + + // Action(s) in-line in the row, additional action(s) in the popover, no column header + { + field: 'actions', + name: '', + sortable: false, + render: (actions: UISession['actions'], session) => { + if (actions && actions.length) { + return ( + + + + + + ); + } + }, + }, + ]; +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts new file mode 100644 index 0000000000000..3c167d6dbe41a --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/lib/get_expiration_status.ts @@ -0,0 +1,47 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { SessionsMgmtConfigSchema } from '../'; + +export const getExpirationStatus = (config: SessionsMgmtConfigSchema, expires: string | null) => { + const tNow = moment.utc().valueOf(); + const tFuture = moment.utc(expires).valueOf(); + + // NOTE this could end up negative. If server time is off from the browser's clock + // and the session was early expired when the browser refreshed the listing + const durationToExpire = moment.duration(tFuture - tNow); + const expiresInDays = Math.floor(durationToExpire.asDays()); + const sufficientDays = Math.ceil(moment.duration(config.expiresSoonWarning).asDays()); + + let toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresSoonInDays', { + defaultMessage: 'Expires in {numDays} days', + values: { numDays: expiresInDays }, + }); + let statusContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.expiresSoonInDaysTooltip', + { defaultMessage: '{numDays} days', values: { numDays: expiresInDays } } + ); + + if (expiresInDays === 0) { + // switch to show expires in hours + const expiresInHours = Math.floor(durationToExpire.asHours()); + + toolTipContent = i18n.translate('xpack.data.mgmt.searchSessions.status.expiresSoonInHours', { + defaultMessage: 'This session expires in {numHours} hours', + values: { numHours: expiresInHours }, + }); + statusContent = i18n.translate( + 'xpack.data.mgmt.searchSessions.status.expiresSoonInHoursTooltip', + { defaultMessage: '{numHours} hours', values: { numHours: expiresInHours } } + ); + } + + if (durationToExpire.valueOf() > 0 && expiresInDays <= sufficientDays) { + return { toolTipContent, statusContent }; + } +}; diff --git a/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts new file mode 100644 index 0000000000000..78b91f7ca8ac2 --- /dev/null +++ b/x-pack/plugins/data_enhanced/public/search/sessions_mgmt/types.ts @@ -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 { SearchSessionStatus } from '../../../common'; +import { ACTION } from './components/actions'; + +export const DATE_STRING_FORMAT = 'D MMM, YYYY, HH:mm:ss'; + +export interface UISession { + id: string; + name: string; + appId: string; + created: string; + expires: string | null; + status: SearchSessionStatus; + actions?: ACTION[]; + reloadUrl: string; + restoreUrl: string; +} diff --git a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx index ed022e18c34d7..361688581b4f1 100644 --- a/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx +++ b/x-pack/plugins/data_enhanced/public/search/ui/search_session_indicator/search_session_indicator.tsx @@ -66,7 +66,7 @@ const ContinueInBackgroundButton = ({ ); const ViewAllSearchSessionsButton = ({ - viewSearchSessionsLink = 'management', + viewSearchSessionsLink = 'management/kibana/search_sessions', buttonProps = {}, }: ActionButtonProps) => ( { let mockCoreSetup: MockedKeys>; let mockContext: jest.Mocked; + let mockLogger: Logger; beforeEach(() => { mockCoreSetup = coreMock.createSetup(); + mockLogger = coreMock.createPluginInitializerContext().logger.get(); mockContext = createSearchRequestHandlerContext(); - registerSessionRoutes(mockCoreSetup.http.createRouter()); + registerSessionRoutes(mockCoreSetup.http.createRouter(), mockLogger); }); it('save calls session.save with sessionId and attributes', async () => { diff --git a/x-pack/plugins/data_enhanced/server/routes/session.ts b/x-pack/plugins/data_enhanced/server/routes/session.ts index b056513f1d2f5..9e61dd39c83b8 100644 --- a/x-pack/plugins/data_enhanced/server/routes/session.ts +++ b/x-pack/plugins/data_enhanced/server/routes/session.ts @@ -5,10 +5,10 @@ */ import { schema } from '@kbn/config-schema'; -import { IRouter } from 'src/core/server'; +import { IRouter, Logger } from 'src/core/server'; import { reportServerError } from '../../../../../src/plugins/kibana_utils/server'; -export function registerSessionRoutes(router: IRouter): void { +export function registerSessionRoutes(router: IRouter, logger: Logger): void { router.post( { path: '/internal/session', @@ -49,6 +49,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } @@ -73,6 +74,7 @@ export function registerSessionRoutes(router: IRouter): void { }); } catch (e) { const err = e.output?.payload || e; + logger.error(err); return reportServerError(res, err); } } @@ -106,6 +108,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } @@ -128,6 +131,7 @@ export function registerSessionRoutes(router: IRouter): void { return res.ok(); } catch (e) { const err = e.output?.payload || e; + logger.error(err); return reportServerError(res, err); } } @@ -156,6 +160,7 @@ export function registerSessionRoutes(router: IRouter): void { body: response, }); } catch (err) { + logger.error(err); return reportServerError(res, err); } } diff --git a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts index f37aaf71fded5..1107ed8155080 100644 --- a/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/session/session_service.test.ts @@ -114,6 +114,7 @@ describe('SearchSessionService', () => { maxUpdateRetries: 3, defaultExpiration: moment.duration(7, 'd'), trackingInterval: moment.duration(10, 's'), + management: {} as any, }, }, }); diff --git a/x-pack/plugins/data_enhanced/tsconfig.json b/x-pack/plugins/data_enhanced/tsconfig.json index ec5c656ac50b5..c4b09276880d9 100644 --- a/x-pack/plugins/data_enhanced/tsconfig.json +++ b/x-pack/plugins/data_enhanced/tsconfig.json @@ -12,6 +12,7 @@ "public/**/*", "server/**/*", "config.ts", + "../../../typings/**/*", // have to declare *.json explicitly due to https://github.com/microsoft/TypeScript/issues/25636 "public/autocomplete/providers/kql_query_suggestion/__fixtures__/*.json" ], @@ -22,6 +23,7 @@ { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, + { "path": "../../../src/plugins/management/tsconfig.json" }, { "path": "../task_manager/tsconfig.json" }, { "path": "../features/tsconfig.json" }, diff --git a/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz b/x-pack/test/functional/es_archives/data/search_sessions/data.json.gz new file mode 100644 index 0000000000000000000000000000000000000000..28260ee99e4dc0c428d5e2a75a0a14db990e2723 GIT binary patch literal 1956 zcmZ9Jc|6m79KeSh9TP&B^*E=Nk)y(zq+{8LFt-&g=T?br)XoBaqK zZlb5cW0S}AEXNoT5lf!+Jg?`U=dbr4-`D5+{(Rrx-{^93mpp!)0Z9@3BK(X(!Y^I` z&E|N!3|c71j)?ZMhHD8@t*9J_!wEVeYp7D*Bil)Yt)84uT*GW^y?+%tW0T~j_N zzst5vQ8k>o9AWg>JlB#6&98gttUIN4dY6VVvJGq(`ZJnql9pJL>7vx5_0)vfo3wJ0 zm3VFZV)qQe-qf_Ifzv&slMBw(%amrO9g0HJ)7DeU{dKH=H$&}Ee|VATdgPqf2g3sW zs0HTY>NpDmF@Xo!(IIcgOOmo#HGYpKq#u8Fr?oBcCa~!0ZFGWH=l$d{oHKOA$)?of#1DMyBX+L6o41yzk9C_(?owkc!ja#8) zSFKe`6{9S(9R&&Uz##~kG(F&(EU3kn@X31slr3MbD(^Bs1Y+cNAhKv9R&g@w(YKK& z2bZg=1d+@6OR%NBhiRoxkE;?n?DFb*)91vb9oZ+MaJYG>26g1S!bjMn!!Z-@ai)2) z4YOA!(61N4T?cV+FSwW6`IVYm$_=k9dKZs2nsPr*SH7DDh`r=8G&9G(I9O@RZ8X`= zNU&SSE)wE1zC*)sC+}4Iial`Us$Rxt7lMV$83j(koLub`S*?4`GFSqf-x=t>NSmv?hGnI{*?t4V;vxS0q#YCy8rrK1v z&w*%Wa9G!_FBz}#YCC13($j>jfLA(R731|6%TTAB0c8K0z${7$X`OGTTb9o%Jj4^Y6p*AwLY%5 zwi*A*uW&zL&(q9|uw`Q$)VS3OEa7~$(GT+Z+{q0LALi+-uLpJgfdR%eQ?KA7Z+!U9 zJe11VMOK5UL&9|%2DQld7Y~G;IH@=>dDFu9csT#13E%3lj9%mTr@`SZ0h93&l9tNo z7O*aaYS)rrh5sC}UO7p9Y4LR}k4`dp+O`tdNqb~+Brt?dkJdp+hS%3#d}r=Br=ycfv^N6<3a8F1s>9t#`9 z)~~l{=V9g8s}hSB-@MLWXTKcl z95}&vo*K3o(_ZBuL?ZnMdtyn_cX2#GVo+h;F@Dy4m5}W`kp-IVAGm}Lx@FdTc!&R> zMq;^5RT6LJdWod}(drFz|1Vo_N#SeW$05IZrNJU$wZmT>oseT*jeaT>zHjz#5~cfA zZr)mFE`7G(l|#M%t~e(JXpeq750&^ts?sJvb6Vy?wE)RhxWCAT zm`L&UM30ia4dp!BWh!F8wQiR@ZiFU-xC+8G76T*%cnU0r1A&j|dPY40B(=f`d#8z@ z0fo84vI9}ji6=GL8`wo5t66`l42Bx34x)=}dcwn;gLZRVp%SI(k{E<`9|wjafhgEA zIlK4!iJ{_?1&?^rZVoM$`(SBPP)%z>$`Vn@`zW(;i+HD_O#Nf^K!L%09YU2>5Gcjf zz#wp%ZewC-^Y+6~5P?seM(KfE=>K=I>hqLGQQ*=zXrrTehu_- z0-(@;)Sm7CSCju$lmD+S)BQ(XcJ4p*=%+YmtQ_TzCmKZYCW<@3SO{=o23+-7iWmxW z*(^rv48YsYVr+E{Eg`#_c*KX4kZDfbY;uIee}7S)*MGY8oC=WFuPBK>*RS|wNEKq+ zUu8!Ga~N>tXNh7c%zk1q-pl|V;^JVRUpIw$#EU^Y%Y8uJboN&#vd7gFt@+=FP|z literal 0 HcmV?d00001 diff --git a/x-pack/test/functional/es_archives/data/search_sessions/mappings.json b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json new file mode 100644 index 0000000000000..24bbcbea23385 --- /dev/null +++ b/x-pack/test/functional/es_archives/data/search_sessions/mappings.json @@ -0,0 +1,2596 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "49eb3350984bd2a162914d3776e70cfb", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "background-session": "dfd06597e582fdbbbc09f1a3615e6ce0", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "core-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "4b9c0e7cfaf86d82a7ee9ed68065e50d", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "0cbbb16506734d341a96aaed65ec6413", + "epm-packages-assets": "44621b2f6052ef966da47b7c3a00f33b", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "27a94b2edcb0610c6aea54a7c56d7752", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "spaces-usage-stats": "3d1b76c39bfb2cc8296b024d73854724", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-counter": "0d409297dc5ebe1e3a1da691c6ee32e3", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "action": { + "properties": { + "actionTypeId": { + "type": "keyword" + }, + "config": { + "enabled": false, + "type": "object" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "secrets": { + "type": "binary" + } + } + }, + "action_task_params": { + "properties": { + "actionId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alert": { + "properties": { + "actions": { + "properties": { + "actionRef": { + "type": "keyword" + }, + "actionTypeId": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + }, + "type": "nested" + }, + "alertTypeId": { + "type": "keyword" + }, + "apiKey": { + "type": "binary" + }, + "apiKeyOwner": { + "type": "keyword" + }, + "consumer": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + }, + "createdBy": { + "type": "keyword" + }, + "enabled": { + "type": "boolean" + }, + "executionStatus": { + "properties": { + "error": { + "properties": { + "message": { + "type": "keyword" + }, + "reason": { + "type": "keyword" + } + } + }, + "lastExecutionDate": { + "type": "date" + }, + "status": { + "type": "keyword" + } + } + }, + "meta": { + "properties": { + "versionApiKeyLastmodified": { + "type": "keyword" + } + } + }, + "muteAll": { + "type": "boolean" + }, + "mutedInstanceIds": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "notifyWhen": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledTaskId": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "throttle": { + "type": "keyword" + }, + "updatedAt": { + "type": "date" + }, + "updatedBy": { + "type": "keyword" + } + } + }, + "api_key_pending_invalidation": { + "properties": { + "apiKeyId": { + "type": "keyword" + }, + "createdAt": { + "type": "date" + } + } + }, + "apm-indices": { + "properties": { + "apm_oss": { + "properties": { + "errorIndices": { + "type": "keyword" + }, + "metricsIndices": { + "type": "keyword" + }, + "onboardingIndices": { + "type": "keyword" + }, + "sourcemapIndices": { + "type": "keyword" + }, + "spanIndices": { + "type": "keyword" + }, + "transactionIndices": { + "type": "keyword" + } + } + } + } + }, + "apm-telemetry": { + "dynamic": "false", + "type": "object" + }, + "app_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "application_usage_daily": { + "dynamic": "false", + "properties": { + "timestamp": { + "type": "date" + } + } + }, + "application_usage_totals": { + "dynamic": "false", + "type": "object" + }, + "application_usage_transactional": { + "dynamic": "false", + "type": "object" + }, + "search-session": { + "properties": { + "appId": { + "type": "keyword" + }, + "created": { + "type": "date" + }, + "expires": { + "type": "date" + }, + "idMapping": { + "enabled": false, + "type": "object" + }, + "initialState": { + "enabled": false, + "type": "object" + }, + "name": { + "type": "keyword" + }, + "restoreState": { + "enabled": false, + "type": "object" + }, + "sessionId": { + "type": "keyword" + }, + "status": { + "type": "keyword" + }, + "urlGeneratorId": { + "type": "keyword" + } + } + }, + "canvas-element": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "content": { + "type": "text" + }, + "help": { + "type": "text" + }, + "image": { + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad": { + "dynamic": "false", + "properties": { + "@created": { + "type": "date" + }, + "@timestamp": { + "type": "date" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "canvas-workpad-template": { + "dynamic": "false", + "properties": { + "help": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "name": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "tags": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "template_key": { + "type": "keyword" + } + } + }, + "cases": { + "properties": { + "closed_at": { + "type": "date" + }, + "closed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "description": { + "type": "text" + }, + "external_service": { + "properties": { + "connector_id": { + "type": "keyword" + }, + "connector_name": { + "type": "keyword" + }, + "external_id": { + "type": "keyword" + }, + "external_title": { + "type": "text" + }, + "external_url": { + "type": "text" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "status": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-comments": { + "properties": { + "alertId": { + "type": "keyword" + }, + "comment": { + "type": "text" + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "index": { + "type": "keyword" + }, + "pushed_at": { + "type": "date" + }, + "pushed_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-configure": { + "properties": { + "closure_type": { + "type": "keyword" + }, + "connector": { + "properties": { + "fields": { + "properties": { + "key": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "id": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "date" + }, + "created_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + } + } + }, + "cases-user-actions": { + "properties": { + "action": { + "type": "keyword" + }, + "action_at": { + "type": "date" + }, + "action_by": { + "properties": { + "email": { + "type": "keyword" + }, + "full_name": { + "type": "keyword" + }, + "username": { + "type": "keyword" + } + } + }, + "action_field": { + "type": "keyword" + }, + "new_value": { + "type": "text" + }, + "old_value": { + "type": "text" + } + } + }, + "config": { + "dynamic": "false", + "properties": { + "buildNum": { + "type": "keyword" + } + } + }, + "core-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "dashboard": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "optionsJSON": { + "index": false, + "type": "text" + }, + "panelsJSON": { + "index": false, + "type": "text" + }, + "refreshInterval": { + "properties": { + "display": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "pause": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "section": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "value": { + "doc_values": false, + "index": false, + "type": "integer" + } + } + }, + "timeFrom": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "timeRestore": { + "doc_values": false, + "index": false, + "type": "boolean" + }, + "timeTo": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "endpoint:user-artifact": { + "properties": { + "body": { + "type": "binary" + }, + "compressionAlgorithm": { + "index": false, + "type": "keyword" + }, + "created": { + "index": false, + "type": "date" + }, + "decodedSha256": { + "index": false, + "type": "keyword" + }, + "decodedSize": { + "index": false, + "type": "long" + }, + "encodedSha256": { + "type": "keyword" + }, + "encodedSize": { + "index": false, + "type": "long" + }, + "encryptionAlgorithm": { + "index": false, + "type": "keyword" + }, + "identifier": { + "type": "keyword" + } + } + }, + "endpoint:user-artifact-manifest": { + "properties": { + "created": { + "index": false, + "type": "date" + }, + "ids": { + "index": false, + "type": "keyword" + }, + "schemaVersion": { + "type": "keyword" + }, + "semanticVersion": { + "index": false, + "type": "keyword" + } + } + }, + "enterprise_search_telemetry": { + "dynamic": "false", + "type": "object" + }, + "epm-packages": { + "properties": { + "es_index_patterns": { + "enabled": false, + "type": "object" + }, + "install_source": { + "type": "keyword" + }, + "install_started_at": { + "type": "date" + }, + "install_status": { + "type": "keyword" + }, + "install_version": { + "type": "keyword" + }, + "installed_es": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "installed_kibana": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "internal": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "package_assets": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "removable": { + "type": "boolean" + }, + "version": { + "type": "keyword" + } + } + }, + "epm-packages-assets": { + "properties": { + "asset_path": { + "type": "keyword" + }, + "data_base64": { + "type": "binary" + }, + "data_utf8": { + "index": false, + "type": "text" + }, + "install_source": { + "type": "keyword" + }, + "media_type": { + "type": "keyword" + }, + "package_name": { + "type": "keyword" + }, + "package_version": { + "type": "keyword" + } + } + }, + "exception-list": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "exception-list-agnostic": { + "properties": { + "_tags": { + "type": "keyword" + }, + "comments": { + "properties": { + "comment": { + "type": "keyword" + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "updated_at": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "created_at": { + "type": "keyword" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "keyword" + }, + "entries": { + "properties": { + "entries": { + "properties": { + "field": { + "type": "keyword" + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "field": { + "type": "keyword" + }, + "list": { + "properties": { + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "operator": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "value": { + "fields": { + "text": { + "type": "text" + } + }, + "type": "keyword" + } + } + }, + "immutable": { + "type": "boolean" + }, + "item_id": { + "type": "keyword" + }, + "list_id": { + "type": "keyword" + }, + "list_type": { + "type": "keyword" + }, + "meta": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "os_types": { + "type": "keyword" + }, + "tags": { + "type": "keyword" + }, + "tie_breaker_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_by": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "file-upload-telemetry": { + "properties": { + "filesUploadedTotalCount": { + "type": "long" + } + } + }, + "fleet-agent-actions": { + "properties": { + "ack_data": { + "type": "text" + }, + "agent_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "data": { + "type": "binary" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "sent_at": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agent-events": { + "properties": { + "action_id": { + "type": "keyword" + }, + "agent_id": { + "type": "keyword" + }, + "data": { + "type": "text" + }, + "message": { + "type": "text" + }, + "payload": { + "type": "text" + }, + "policy_id": { + "type": "keyword" + }, + "stream_id": { + "type": "keyword" + }, + "subtype": { + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "type": { + "type": "keyword" + } + } + }, + "fleet-agents": { + "properties": { + "access_api_key_id": { + "type": "keyword" + }, + "active": { + "type": "boolean" + }, + "current_error_events": { + "index": false, + "type": "text" + }, + "default_api_key": { + "type": "binary" + }, + "default_api_key_id": { + "type": "keyword" + }, + "enrolled_at": { + "type": "date" + }, + "last_checkin": { + "type": "date" + }, + "last_checkin_status": { + "type": "keyword" + }, + "last_updated": { + "type": "date" + }, + "local_metadata": { + "type": "flattened" + }, + "packages": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "policy_revision": { + "type": "integer" + }, + "shared_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "unenrolled_at": { + "type": "date" + }, + "unenrollment_started_at": { + "type": "date" + }, + "updated_at": { + "type": "date" + }, + "upgrade_started_at": { + "type": "date" + }, + "upgraded_at": { + "type": "date" + }, + "user_provided_metadata": { + "type": "flattened" + }, + "version": { + "type": "keyword" + } + } + }, + "fleet-enrollment-api-keys": { + "properties": { + "active": { + "type": "boolean" + }, + "api_key": { + "type": "binary" + }, + "api_key_id": { + "type": "keyword" + }, + "created_at": { + "type": "date" + }, + "expire_at": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "policy_id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "graph-workspace": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "legacyIndexPatternRef": { + "index": false, + "type": "text" + }, + "numLinks": { + "type": "integer" + }, + "numVertices": { + "type": "integer" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + }, + "wsState": { + "type": "text" + } + } + }, + "index-pattern": { + "dynamic": "false", + "properties": { + "title": { + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "infrastructure-ui-source": { + "dynamic": "false", + "type": "object" + }, + "ingest-agent-policies": { + "properties": { + "description": { + "type": "text" + }, + "is_default": { + "type": "boolean" + }, + "monitoring_enabled": { + "index": false, + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "package_policies": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "status": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest-outputs": { + "properties": { + "ca_sha256": { + "index": false, + "type": "keyword" + }, + "config": { + "type": "flattened" + }, + "config_yaml": { + "type": "text" + }, + "fleet_enroll_password": { + "type": "binary" + }, + "fleet_enroll_username": { + "type": "binary" + }, + "hosts": { + "type": "keyword" + }, + "is_default": { + "type": "boolean" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "ingest-package-policies": { + "properties": { + "created_at": { + "type": "date" + }, + "created_by": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "enabled": { + "type": "boolean" + }, + "inputs": { + "enabled": false, + "properties": { + "compiled_input": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "enabled": { + "type": "boolean" + }, + "streams": { + "properties": { + "compiled_stream": { + "type": "flattened" + }, + "config": { + "type": "flattened" + }, + "data_stream": { + "properties": { + "dataset": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "enabled": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "type": { + "type": "keyword" + }, + "vars": { + "type": "flattened" + } + }, + "type": "nested" + }, + "name": { + "type": "keyword" + }, + "namespace": { + "type": "keyword" + }, + "output_id": { + "type": "keyword" + }, + "package": { + "properties": { + "name": { + "type": "keyword" + }, + "title": { + "type": "keyword" + }, + "version": { + "type": "keyword" + } + } + }, + "policy_id": { + "type": "keyword" + }, + "revision": { + "type": "integer" + }, + "updated_at": { + "type": "date" + }, + "updated_by": { + "type": "keyword" + } + } + }, + "ingest_manager_settings": { + "properties": { + "agent_auto_upgrade": { + "type": "keyword" + }, + "has_seen_add_data_notice": { + "index": false, + "type": "boolean" + }, + "kibana_ca_sha256": { + "type": "keyword" + }, + "kibana_urls": { + "type": "keyword" + }, + "package_auto_upgrade": { + "type": "keyword" + } + } + }, + "inventory-view": { + "dynamic": "false", + "type": "object" + }, + "kql-telemetry": { + "properties": { + "optInCount": { + "type": "long" + }, + "optOutCount": { + "type": "long" + } + } + }, + "lens": { + "properties": { + "description": { + "type": "text" + }, + "expression": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "state": { + "type": "flattened" + }, + "title": { + "type": "text" + }, + "visualizationType": { + "type": "keyword" + } + } + }, + "lens-ui-telemetry": { + "properties": { + "count": { + "type": "integer" + }, + "date": { + "type": "date" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + } + }, + "map": { + "properties": { + "description": { + "type": "text" + }, + "layerListJSON": { + "type": "text" + }, + "mapStateJSON": { + "type": "text" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "maps-telemetry": { + "enabled": false, + "type": "object" + }, + "metrics-explorer-view": { + "dynamic": "false", + "type": "object" + }, + "migrationVersion": { + "dynamic": "true", + "properties": { + "config": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "index-pattern": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "search": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "space": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "ml-job": { + "properties": { + "datafeed_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "job_id": { + "fields": { + "keyword": { + "type": "keyword" + } + }, + "type": "text" + }, + "type": { + "type": "keyword" + } + } + }, + "ml-telemetry": { + "properties": { + "file_data_visualizer": { + "properties": { + "index_creation_count": { + "type": "long" + } + } + } + } + }, + "monitoring-telemetry": { + "properties": { + "reportedClusterUuids": { + "type": "keyword" + } + } + }, + "namespace": { + "type": "keyword" + }, + "namespaces": { + "type": "keyword" + }, + "originId": { + "type": "keyword" + }, + "query": { + "properties": { + "description": { + "type": "text" + }, + "filters": { + "enabled": false, + "type": "object" + }, + "query": { + "properties": { + "language": { + "type": "keyword" + }, + "query": { + "index": false, + "type": "keyword" + } + } + }, + "timefilter": { + "enabled": false, + "type": "object" + }, + "title": { + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "sample-data-telemetry": { + "properties": { + "installCount": { + "type": "long" + }, + "unInstallCount": { + "type": "long" + } + } + }, + "search": { + "properties": { + "columns": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "description": { + "type": "text" + }, + "hits": { + "doc_values": false, + "index": false, + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "sort": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "search-telemetry": { + "dynamic": "false", + "type": "object" + }, + "siem-detection-engine-rule-actions": { + "properties": { + "actions": { + "properties": { + "action_type_id": { + "type": "keyword" + }, + "group": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "params": { + "enabled": false, + "type": "object" + } + } + }, + "alertThrottle": { + "type": "keyword" + }, + "ruleAlertId": { + "type": "keyword" + }, + "ruleThrottle": { + "type": "keyword" + } + } + }, + "siem-detection-engine-rule-status": { + "properties": { + "alertId": { + "type": "keyword" + }, + "bulkCreateTimeDurations": { + "type": "float" + }, + "gap": { + "type": "text" + }, + "lastFailureAt": { + "type": "date" + }, + "lastFailureMessage": { + "type": "text" + }, + "lastLookBackDate": { + "type": "date" + }, + "lastSuccessAt": { + "type": "date" + }, + "lastSuccessMessage": { + "type": "text" + }, + "searchAfterTimeDurations": { + "type": "float" + }, + "status": { + "type": "keyword" + }, + "statusDate": { + "type": "date" + } + } + }, + "siem-ui-timeline": { + "properties": { + "columns": { + "properties": { + "aggregatable": { + "type": "boolean" + }, + "category": { + "type": "keyword" + }, + "columnHeaderType": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "example": { + "type": "text" + }, + "id": { + "type": "keyword" + }, + "indexes": { + "type": "keyword" + }, + "name": { + "type": "text" + }, + "placeholder": { + "type": "text" + }, + "searchable": { + "type": "boolean" + }, + "type": { + "type": "keyword" + } + } + }, + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "dataProviders": { + "properties": { + "and": { + "properties": { + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "enabled": { + "type": "boolean" + }, + "excluded": { + "type": "boolean" + }, + "id": { + "type": "keyword" + }, + "kqlQuery": { + "type": "text" + }, + "name": { + "type": "text" + }, + "queryMatch": { + "properties": { + "displayField": { + "type": "text" + }, + "displayValue": { + "type": "text" + }, + "field": { + "type": "text" + }, + "operator": { + "type": "text" + }, + "value": { + "type": "text" + } + } + }, + "type": { + "type": "text" + } + } + }, + "dateRange": { + "properties": { + "end": { + "type": "date" + }, + "start": { + "type": "date" + } + } + }, + "description": { + "type": "text" + }, + "eventType": { + "type": "keyword" + }, + "excludedRowRendererIds": { + "type": "text" + }, + "favorite": { + "properties": { + "favoriteDate": { + "type": "date" + }, + "fullName": { + "type": "text" + }, + "keySearch": { + "type": "text" + }, + "userName": { + "type": "text" + } + } + }, + "filters": { + "properties": { + "exists": { + "type": "text" + }, + "match_all": { + "type": "text" + }, + "meta": { + "properties": { + "alias": { + "type": "text" + }, + "controlledBy": { + "type": "text" + }, + "disabled": { + "type": "boolean" + }, + "field": { + "type": "text" + }, + "formattedValue": { + "type": "text" + }, + "index": { + "type": "keyword" + }, + "key": { + "type": "keyword" + }, + "negate": { + "type": "boolean" + }, + "params": { + "type": "text" + }, + "type": { + "type": "keyword" + }, + "value": { + "type": "text" + } + } + }, + "missing": { + "type": "text" + }, + "query": { + "type": "text" + }, + "range": { + "type": "text" + }, + "script": { + "type": "text" + } + } + }, + "indexNames": { + "type": "text" + }, + "kqlMode": { + "type": "keyword" + }, + "kqlQuery": { + "properties": { + "filterQuery": { + "properties": { + "kuery": { + "properties": { + "expression": { + "type": "text" + }, + "kind": { + "type": "keyword" + } + } + }, + "serializedQuery": { + "type": "text" + } + } + } + } + }, + "savedQueryId": { + "type": "keyword" + }, + "sort": { + "properties": { + "columnId": { + "type": "keyword" + }, + "sortDirection": { + "type": "keyword" + } + } + }, + "status": { + "type": "keyword" + }, + "templateTimelineId": { + "type": "text" + }, + "templateTimelineVersion": { + "type": "integer" + }, + "timelineType": { + "type": "keyword" + }, + "title": { + "type": "text" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-note": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "note": { + "type": "text" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "siem-ui-timeline-pinned-event": { + "properties": { + "created": { + "type": "date" + }, + "createdBy": { + "type": "text" + }, + "eventId": { + "type": "keyword" + }, + "timelineId": { + "type": "keyword" + }, + "updated": { + "type": "date" + }, + "updatedBy": { + "type": "text" + } + } + }, + "space": { + "properties": { + "_reserved": { + "type": "boolean" + }, + "color": { + "type": "keyword" + }, + "description": { + "type": "text" + }, + "disabledFeatures": { + "type": "keyword" + }, + "imageUrl": { + "index": false, + "type": "text" + }, + "initials": { + "type": "keyword" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "spaces-usage-stats": { + "dynamic": "false", + "type": "object" + }, + "tag": { + "properties": { + "color": { + "type": "text" + }, + "description": { + "type": "text" + }, + "name": { + "type": "text" + } + } + }, + "telemetry": { + "properties": { + "allowChangingOptInStatus": { + "type": "boolean" + }, + "enabled": { + "type": "boolean" + }, + "lastReported": { + "type": "date" + }, + "lastVersionChecked": { + "type": "keyword" + }, + "reportFailureCount": { + "type": "integer" + }, + "reportFailureVersion": { + "type": "keyword" + }, + "sendUsageFrom": { + "type": "keyword" + }, + "userHasSeenNotice": { + "type": "boolean" + } + } + }, + "timelion-sheet": { + "properties": { + "description": { + "type": "text" + }, + "hits": { + "type": "integer" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "type": "text" + } + } + }, + "timelion_chart_height": { + "type": "integer" + }, + "timelion_columns": { + "type": "integer" + }, + "timelion_interval": { + "type": "keyword" + }, + "timelion_other_interval": { + "type": "keyword" + }, + "timelion_rows": { + "type": "integer" + }, + "timelion_sheet": { + "type": "text" + }, + "title": { + "type": "text" + }, + "version": { + "type": "integer" + } + } + }, + "tsvb-validation-telemetry": { + "properties": { + "failedRequests": { + "type": "long" + } + } + }, + "type": { + "type": "keyword" + }, + "ui-counter": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "ui-metric": { + "properties": { + "count": { + "type": "integer" + } + } + }, + "updated_at": { + "type": "date" + }, + "upgrade-assistant-reindex-operation": { + "properties": { + "errorMessage": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "indexName": { + "type": "keyword" + }, + "lastCompletedStep": { + "type": "long" + }, + "locked": { + "type": "date" + }, + "newIndexName": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexOptions": { + "properties": { + "openAndClose": { + "type": "boolean" + }, + "queueSettings": { + "properties": { + "queuedAt": { + "type": "long" + }, + "startedAt": { + "type": "long" + } + } + } + } + }, + "reindexTaskId": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + }, + "reindexTaskPercComplete": { + "type": "float" + }, + "runningReindexCount": { + "type": "integer" + }, + "status": { + "type": "integer" + } + } + }, + "upgrade-assistant-telemetry": { + "properties": { + "features": { + "properties": { + "deprecation_logging": { + "properties": { + "enabled": { + "null_value": true, + "type": "boolean" + } + } + } + } + }, + "ui_open": { + "properties": { + "cluster": { + "null_value": 0, + "type": "long" + }, + "indices": { + "null_value": 0, + "type": "long" + }, + "overview": { + "null_value": 0, + "type": "long" + } + } + }, + "ui_reindex": { + "properties": { + "close": { + "null_value": 0, + "type": "long" + }, + "open": { + "null_value": 0, + "type": "long" + }, + "start": { + "null_value": 0, + "type": "long" + }, + "stop": { + "null_value": 0, + "type": "long" + } + } + } + } + }, + "uptime-dynamic-settings": { + "dynamic": "false", + "type": "object" + }, + "url": { + "properties": { + "accessCount": { + "type": "long" + }, + "accessDate": { + "type": "date" + }, + "createDate": { + "type": "date" + }, + "url": { + "fields": { + "keyword": { + "ignore_above": 2048, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "visualization": { + "properties": { + "description": { + "type": "text" + }, + "kibanaSavedObjectMeta": { + "properties": { + "searchSourceJSON": { + "index": false, + "type": "text" + } + } + }, + "savedSearchRefName": { + "doc_values": false, + "index": false, + "type": "keyword" + }, + "title": { + "type": "text" + }, + "uiStateJSON": { + "index": false, + "type": "text" + }, + "version": { + "type": "integer" + }, + "visState": { + "index": false, + "type": "text" + } + } + }, + "workplace_search_telemetry": { + "dynamic": "false", + "type": "object" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/index.ts b/x-pack/test/functional/page_objects/index.ts index 4c523ec5706e1..20b8acb9d4509 100644 --- a/x-pack/test/functional/page_objects/index.ts +++ b/x-pack/test/functional/page_objects/index.ts @@ -38,6 +38,7 @@ import { SpaceSelectorPageProvider } from './space_selector_page'; import { IngestPipelinesPageProvider } from './ingest_pipelines_page'; import { TagManagementPageProvider } from './tag_management_page'; import { NavigationalSearchProvider } from './navigational_search'; +import { SearchSessionsPageProvider } from './search_sessions_management_page'; // just like services, PageObjects are defined as a map of // names to Providers. Merge in Kibana's or pick specific ones @@ -64,6 +65,7 @@ export const pageObjects = { apiKeys: ApiKeysPageProvider, licenseManagement: LicenseManagementPageProvider, indexManagement: IndexManagementPageProvider, + searchSessionsManagement: SearchSessionsPageProvider, indexLifecycleManagement: IndexLifecycleManagementPageProvider, tagManagement: TagManagementPageProvider, snapshotRestore: SnapshotRestorePageProvider, diff --git a/x-pack/test/functional/page_objects/search_sessions_management_page.ts b/x-pack/test/functional/page_objects/search_sessions_management_page.ts new file mode 100644 index 0000000000000..99c3be82a214d --- /dev/null +++ b/x-pack/test/functional/page_objects/search_sessions_management_page.ts @@ -0,0 +1,60 @@ +/* + * 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 function SearchSessionsPageProvider({ getService, getPageObjects }: FtrProviderContext) { + const find = getService('find'); + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects(['common']); + + return { + async goTo() { + await PageObjects.common.navigateToApp('management/kibana/search_sessions'); + }, + + async refresh() { + await testSubjects.click('sessionManagementRefreshBtn'); + }, + + async getList() { + const table = await find.byCssSelector('table'); + const allRows = await table.findAllByTestSubject('searchSessionsRow'); + + return Promise.all( + allRows.map(async (row) => { + const $ = await row.parseDomContent(); + const viewCell = await row.findByTestSubject('sessionManagementNameCol'); + const actionsCell = await row.findByTestSubject('sessionManagementActionsCol'); + return { + name: $.findTestSubject('sessionManagementNameCol').text(), + status: $.findTestSubject('sessionManagementStatusLabel').attr('data-test-status'), + mainUrl: $.findTestSubject('sessionManagementNameCol').text(), + created: $.findTestSubject('sessionManagementCreatedCol').text(), + expires: $.findTestSubject('sessionManagementExpiresCol').text(), + app: $.findTestSubject('sessionManagementAppIcon').attr('data-test-app-id'), + view: async () => { + await viewCell.click(); + }, + reload: async () => { + await actionsCell.click(); + await find.clickByCssSelector( + '[data-test-subj="sessionManagementPopoverAction-reload"]' + ); + }, + cancel: async () => { + await actionsCell.click(); + await find.clickByCssSelector( + '[data-test-subj="sessionManagementPopoverAction-cancel"]' + ); + await PageObjects.common.clickConfirmOnModal(); + }, + }; + }) + ); + }, + }; +} diff --git a/x-pack/test/send_search_to_background_integration/config.ts b/x-pack/test/send_search_to_background_integration/config.ts index c14678febd811..bad818bb69664 100644 --- a/x-pack/test/send_search_to_background_integration/config.ts +++ b/x-pack/test/send_search_to_background_integration/config.ts @@ -23,6 +23,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { testFiles: [ resolve(__dirname, './tests/apps/dashboard/async_search'), resolve(__dirname, './tests/apps/discover'), + resolve(__dirname, './tests/apps/management/search_sessions'), ], kbnTestServer: { diff --git a/x-pack/test/send_search_to_background_integration/services/index.ts b/x-pack/test/send_search_to_background_integration/services/index.ts index 91b0ad502d053..35eed5a218b42 100644 --- a/x-pack/test/send_search_to_background_integration/services/index.ts +++ b/x-pack/test/send_search_to_background_integration/services/index.ts @@ -9,5 +9,5 @@ import { SendToBackgroundProvider } from './send_to_background'; export const services = { ...functionalServices, - sendToBackground: SendToBackgroundProvider, + searchSessions: SendToBackgroundProvider, }; diff --git a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts index 319496239de34..8c3261c2074ae 100644 --- a/x-pack/test/send_search_to_background_integration/services/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/services/send_to_background.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FtrProviderContext } from '../ftr_provider_context'; +import { SavedObjectsFindResponse } from 'src/core/server'; import { WebElementWrapper } from '../../../../test/functional/services/lib/web_element_wrapper'; +import { FtrProviderContext } from '../ftr_provider_context'; -const SEND_TO_BACKGROUND_TEST_SUBJ = 'searchSessionIndicator'; -const SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer'; +const SEARCH_SESSION_INDICATOR_TEST_SUBJ = 'searchSessionIndicator'; +const SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ = 'searchSessionIndicatorPopoverContainer'; type SessionStateType = | 'none' @@ -21,22 +22,24 @@ type SessionStateType = export function SendToBackgroundProvider({ getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); + const log = getService('log'); const retry = getService('retry'); const browser = getService('browser'); + const supertest = getService('supertest'); return new (class SendToBackgroundService { public async find(): Promise { - return testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ); + return testSubjects.find(SEARCH_SESSION_INDICATOR_TEST_SUBJ); } public async exists(): Promise { - return testSubjects.exists(SEND_TO_BACKGROUND_TEST_SUBJ); + return testSubjects.exists(SEARCH_SESSION_INDICATOR_TEST_SUBJ); } public async expectState(state: SessionStateType) { - return retry.waitFor(`sendToBackground indicator to get into state = ${state}`, async () => { + return retry.waitFor(`searchSessions indicator to get into state = ${state}`, async () => { const currentState = await ( - await testSubjects.find(SEND_TO_BACKGROUND_TEST_SUBJ) + await testSubjects.find(SEARCH_SESSION_INDICATOR_TEST_SUBJ) ).getAttribute('data-state'); return currentState === state; }); @@ -65,23 +68,57 @@ export function SendToBackgroundProvider({ getService }: FtrProviderContext) { await this.ensurePopoverClosed(); } + public async openPopover() { + await this.ensurePopoverOpened(); + } + private async ensurePopoverOpened() { - const isAlreadyOpen = await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + const isAlreadyOpen = await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ); if (isAlreadyOpen) return; - return retry.waitFor(`sendToBackground popover opened`, async () => { - await testSubjects.click(SEND_TO_BACKGROUND_TEST_SUBJ); - return await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ); + return retry.waitFor(`searchSessions popover opened`, async () => { + await testSubjects.click(SEARCH_SESSION_INDICATOR_TEST_SUBJ); + return await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ); }); } private async ensurePopoverClosed() { const isAlreadyClosed = !(await testSubjects.exists( - SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ + SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ )); if (isAlreadyClosed) return; - return retry.waitFor(`sendToBackground popover closed`, async () => { + return retry.waitFor(`searchSessions popover closed`, async () => { await browser.pressKeys(browser.keys.ESCAPE); - return !(await testSubjects.exists(SEND_TO_BACKGROUND_POPOVER_CONTENT_TEST_SUBJ)); + return !(await testSubjects.exists(SEARCH_SESSIONS_POPOVER_CONTENT_TEST_SUBJ)); + }); + } + + /* + * 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. + */ + public async deleteAllSearchSessions() { + log.debug('Deleting created background sessions'); + // ignores 409 errs and keeps retrying + await retry.tryForTime(10000, async () => { + const { body } = await supertest + .post('/internal/session/_find') + .set('kbn-xsrf', 'anything') + .set('kbn-system-request', 'true') + .send({ page: 1, perPage: 10000, sortField: 'created', sortOrder: 'asc' }) + .expect(200); + + const { saved_objects: savedObjects } = body as SavedObjectsFindResponse; + log.debug(`Found created background sessions: ${savedObjects.map(({ id }) => id)}`); + await Promise.all( + savedObjects.map(async (so) => { + log.debug(`Deleting background session: ${so.id}`); + await supertest + .delete(`/internal/session/${so.id}`) + .set(`kbn-xsrf`, `anything`) + .expect(200); + }) + ); }); } })(); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts index 2edaeb1918b25..03635efb6113d 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background.ts @@ -14,7 +14,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'dashboard', 'visChart']); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('send to background', () => { before(async function () { @@ -26,6 +26,10 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.common.navigateToApp('dashboard'); }); + after(async function () { + await searchSessions.deleteAllSearchSessions(); + }); + it('Restore using non-existing sessionId errors out. Refresh starts a new session and completes.', async () => { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); const url = await browser.getCurrentUrl(); @@ -33,7 +37,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const savedSessionURL = `${url}&searchSessionId=${fakeSessionId}`; await browser.get(savedSessionURL); await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.existOrFail('embeddableErrorLabel'); // expected that panel errors out because of non existing session const session1 = await dashboardPanelActions.getSearchSessionIdByTitle( @@ -41,9 +45,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ); expect(session1).to.be(fakeSessionId); - await sendToBackground.refresh(); + await searchSessions.refresh(); await PageObjects.header.waitUntilLoadingHasFinished(); - await sendToBackground.expectState('completed'); + await searchSessions.expectState('completed'); await testSubjects.missingOrFail('embeddableErrorLabel'); const session2 = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension' @@ -54,9 +58,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { it('Saves and restores a session', async () => { await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); await PageObjects.dashboard.waitForRenderComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( 'Sum of Bytes by Extension' ); @@ -69,7 +73,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); const data = await PageObjects.visChart.getBarChartData('Sum of bytes'); expect(data.length).to.be(5); @@ -77,7 +81,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { // switching dashboard to edit mode (or any other non-fetch required) state change // should leave session state untouched await PageObjects.dashboard.switchToEditMode(); - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); }); }); } diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts index 9eb42b74668c8..ce6c8978c7d67 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/send_to_background_relative_time.ts @@ -25,7 +25,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const find = getService('find'); const dashboardExpect = getService('dashboardExpect'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('send to background with relative time', () => { before(async () => { @@ -60,9 +60,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); await checkSampleDashboardLoaded(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( '[Flights] Airline Carrier' ); @@ -80,7 +80,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await checkSampleDashboardLoaded(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts index 7d00761b2fa9f..f590e44138642 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/dashboard/async_search/sessions_in_space.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { ]); const dashboardPanelActions = getService('dashboardPanelActions'); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('dashboard in space', () => { describe('Send to background in space', () => { @@ -73,9 +73,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); const savedSessionId = await dashboardPanelActions.getSearchSessionIdByTitle( 'A Pie in another space' ); @@ -88,7 +88,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.dashboard.waitForRenderComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts index 5c94a50e0a84d..6384afb179593 100644 --- a/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts +++ b/x-pack/test/send_search_to_background_integration/tests/apps/discover/sessions_in_space.ts @@ -20,7 +20,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { 'timePicker', ]); const browser = getService('browser'); - const sendToBackground = getService('sendToBackground'); + const searchSessions = getService('searchSessions'); describe('discover in space', () => { describe('Send to background in space', () => { @@ -74,9 +74,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitForDocTableLoadingComplete(); - await sendToBackground.expectState('completed'); - await sendToBackground.save(); - await sendToBackground.expectState('backgroundCompleted'); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); await inspector.open(); const savedSessionId = await ( @@ -92,7 +92,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.discover.waitForDocTableLoadingComplete(); // Check that session is restored - await sendToBackground.expectState('restored'); + await searchSessions.expectState('restored'); await testSubjects.missingOrFail('embeddableErrorLabel'); }); }); diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts new file mode 100644 index 0000000000000..6a11a15f31567 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/index.ts @@ -0,0 +1,24 @@ +/* + * 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('search sessions management', function () { + this.tags('ciGroup3'); + + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('dashboard/async_search'); + await kibanaServer.uiSettings.replace({ defaultIndex: 'logstash-*' }); + await kibanaServer.uiSettings.replace({ 'search:timeout': 10000 }); + }); + + loadTestFile(require.resolve('./sessions_management')); + }); +} diff --git a/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts new file mode 100644 index 0000000000000..f06e8eba0bf68 --- /dev/null +++ b/x-pack/test/send_search_to_background_integration/tests/apps/management/search_sessions/sessions_management.ts @@ -0,0 +1,148 @@ +/* + * 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 ({ getService, getPageObjects }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + const PageObjects = getPageObjects([ + 'common', + 'header', + 'dashboard', + 'visChart', + 'searchSessionsManagement', + ]); + const searchSessions = getService('searchSessions'); + const esArchiver = getService('esArchiver'); + const retry = getService('retry'); + + describe('Search search sessions Management UI', () => { + describe('New search sessions', () => { + before(async () => { + await PageObjects.common.navigateToApp('dashboard'); + }); + + after(async () => { + await searchSessions.deleteAllSearchSessions(); + }); + + it('Saves a session and verifies it in the Management app', async () => { + await PageObjects.dashboard.loadSavedDashboard('Not Delayed'); + await PageObjects.dashboard.waitForRenderComplete(); + await searchSessions.expectState('completed'); + await searchSessions.save(); + await searchSessions.expectState('backgroundCompleted'); + + await searchSessions.openPopover(); + await searchSessions.viewSearchSessions(); + + await retry.waitFor(`wait for first item to complete`, async function () { + const s = await PageObjects.searchSessionsManagement.getList(); + return s[0] && s[0].status === 'complete'; + }); + + // find there is only one item in the table which is the newly saved session + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + expect(searchSessionList.length).to.be(1); + expect(searchSessionList[0].expires).not.to.eql('--'); + expect(searchSessionList[0].name).to.eql('Not Delayed'); + + // navigate to dashboard + await searchSessionList[0].view(); + + // embeddable has loaded + await testSubjects.existOrFail('embeddablePanelHeading-SumofBytesbyExtension'); + await PageObjects.dashboard.waitForRenderComplete(); + + // search session was restored + await searchSessions.expectState('restored'); + }); + + it('Reloads as new session from management', async () => { + await PageObjects.searchSessionsManagement.goTo(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(1); + await searchSessionList[0].reload(); + + // embeddable has loaded + await PageObjects.dashboard.waitForRenderComplete(); + + // new search session was completed + await searchSessions.expectState('completed'); + }); + + it('Cancels a session from management', async () => { + await PageObjects.searchSessionsManagement.goTo(); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(1); + await searchSessionList[0].cancel(); + + // TODO: update this once canceling doesn't delete the object! + await retry.waitFor(`wait for list to be empty`, async function () { + const s = await PageObjects.searchSessionsManagement.getList(); + + return s.length === 0; + }); + }); + }); + + describe('Archived search sessions', () => { + before(async () => { + await PageObjects.searchSessionsManagement.goTo(); + }); + + after(async () => { + await searchSessions.deleteAllSearchSessions(); + }); + + it('shows no items found', async () => { + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + expect(searchSessionList.length).to.be(0); + }); + + it('autorefreshes and shows items on the server', async () => { + await esArchiver.load('data/search_sessions'); + + const searchSessionList = await PageObjects.searchSessionsManagement.getList(); + + expect(searchSessionList.length).to.be(10); + + expect(searchSessionList.map((ss) => ss.created)).to.eql([ + '25 Dec, 2020, 00:00:00', + '24 Dec, 2020, 00:00:00', + '23 Dec, 2020, 00:00:00', + '22 Dec, 2020, 00:00:00', + '21 Dec, 2020, 00:00:00', + '20 Dec, 2020, 00:00:00', + '19 Dec, 2020, 00:00:00', + '18 Dec, 2020, 00:00:00', + '17 Dec, 2020, 00:00:00', + '16 Dec, 2020, 00:00:00', + ]); + + expect(searchSessionList.map((ss) => ss.expires)).to.eql([ + '--', + '--', + '--', + '23 Dec, 2020, 00:00:00', + '22 Dec, 2020, 00:00:00', + '--', + '--', + '--', + '18 Dec, 2020, 00:00:00', + '17 Dec, 2020, 00:00:00', + ]); + + await esArchiver.unload('data/search_sessions'); + }); + }); + }); +}