From 79fa4f405bfb89b741e74c6d9e617be2ef080f4d Mon Sep 17 00:00:00 2001 From: Joe Reuter Date: Mon, 31 May 2021 11:14:31 +0200 Subject: [PATCH 01/46] [Lens][Dashboard] Share session between lens and dashboard (#100214) --- .../public/application/dashboard_app.tsx | 7 -- .../public/application/dashboard_router.tsx | 11 ++- .../hooks/use_dashboard_container.ts | 5 +- .../application/listing/dashboard_listing.tsx | 5 ++ .../es_query/es_query/build_es_query.test.ts | 86 ++++++++++++++++--- .../es_query/es_query/build_es_query.ts | 14 ++- .../search/session/session_service.test.ts | 8 -- .../public/search/session/session_service.ts | 27 ------ .../vis_type_vega/public/data_model/types.ts | 2 +- .../lens/public/app_plugin/mounter.test.tsx | 27 ++++-- .../lens/public/app_plugin/mounter.tsx | 30 +++++-- .../datapanel.test.tsx | 2 +- .../field_item.test.tsx | 2 +- .../public/hosts/pages/hosts.test.tsx | 2 +- .../public/network/pages/network.test.tsx | 2 +- .../components/timeline/helpers.test.tsx | 4 +- .../test/functional/apps/maps/mvt_scaling.js | 2 +- .../functional/apps/maps/mvt_super_fine.js | 2 +- 18 files changed, 154 insertions(+), 84 deletions(-) diff --git a/src/plugins/dashboard/public/application/dashboard_app.tsx b/src/plugins/dashboard/public/application/dashboard_app.tsx index fa86fb81bd407..93310bb821361 100644 --- a/src/plugins/dashboard/public/application/dashboard_app.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app.tsx @@ -295,13 +295,6 @@ export function DashboardApp({ }; }, [dashboardStateManager, dashboardContainer, onAppLeave, embeddable]); - // clear search session when leaving dashboard route - useEffect(() => { - return () => { - data.search.session.clear(); - }; - }, [data.search.session]); - return ( <> {savedDashboard && dashboardStateManager && dashboardContainer && viewMode && ( diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index d5eddf6bb4864..be279ed98492e 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -198,8 +198,14 @@ export async function mountApp({ return ; }; - // make sure the index pattern list is up to date - await dataStart.indexPatterns.clearCache(); + const hasEmbeddableIncoming = Boolean( + dashboardServices.embeddable + .getStateTransfer() + .getIncomingEmbeddablePackage(DashboardConstants.DASHBOARDS_ID, false) + ); + if (!hasEmbeddableIncoming) { + dataStart.indexPatterns.clearCache(); + } // dispatch synthetic hash change event to update hash history objects // this is necessary because hash updates triggered by using popState won't trigger this event naturally. @@ -242,7 +248,6 @@ export async function mountApp({ } render(app, element); return () => { - dataStart.search.session.clear(); unlistenParentHistory(); unmountComponentAtNode(element); appUnMounted(); diff --git a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts index 0be29f67a9492..d715fb70ec91a 100644 --- a/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts +++ b/src/plugins/dashboard/public/application/hooks/use_dashboard_container.ts @@ -85,6 +85,7 @@ export const useDashboardContainer = ({ let canceled = false; let pendingContainer: DashboardContainer | ErrorEmbeddable | null | undefined; (async function createContainer() { + const existingSession = searchSession.getSessionId(); pendingContainer = await dashboardFactory.create( getDashboardContainerInput({ isEmbeddedExternally: Boolean(isEmbeddedExternally), @@ -92,7 +93,9 @@ export const useDashboardContainer = ({ dashboardStateManager, incomingEmbeddable, query, - searchSessionId: searchSessionIdFromURL ?? searchSession.start(), + searchSessionId: + searchSessionIdFromURL ?? + (existingSession && incomingEmbeddable ? existingSession : searchSession.start()), }) ); diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index db0404595af6c..e2c11d614d797 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -83,6 +83,11 @@ export const DashboardListing = ({ }; }, [title, savedObjectsClient, redirectTo, data.query, kbnUrlStateStorage]); + // clear dangling session because they are not required here + useEffect(() => { + data.search.session.clear(); + }, [data.search.session]); + const hideWriteControls = dashboardCapabilities.hideWriteControls; const listingLimit = savedObjects.settings.getListingLimit(); const defaultFilter = title ? `"${title}"` : ''; diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts index c6d923f4505f0..fa9a2c85aaef5 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.test.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.test.ts @@ -39,12 +39,16 @@ describe('build query', () => { { query: 'extension:jpg', language: 'kuery' }, { query: 'bar:baz', language: 'lucene' }, ] as Query[]; - const filters = [ - { - match_all: {}, - meta: { type: 'match_all' }, - } as MatchAllFilter, - ]; + const filters = { + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }; const config = { allowLeadingWildcards: true, queryStringOptions: {}, @@ -56,7 +60,11 @@ describe('build query', () => { must: [decorateQuery(luceneStringToDsl('bar:baz'), config.queryStringOptions)], filter: [ toElasticsearchQuery(fromKueryExpression('extension:jpg'), indexPattern), - { match_all: {} }, + { + match: { + a: 'b', + }, + }, ], should: [], must_not: [], @@ -71,9 +79,15 @@ describe('build query', () => { it('should accept queries and filters as either single objects or arrays', () => { const queries = { query: 'extension:jpg', language: 'lucene' } as Query; const filters = { - match_all: {}, - meta: { type: 'match_all' }, - } as MatchAllFilter; + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }; const config = { allowLeadingWildcards: true, queryStringOptions: {}, @@ -83,7 +97,13 @@ describe('build query', () => { const expectedResult = { bool: { must: [decorateQuery(luceneStringToDsl('extension:jpg'), config.queryStringOptions)], - filter: [{ match_all: {} }], + filter: [ + { + match: { + a: 'b', + }, + }, + ], should: [], must_not: [], }, @@ -94,6 +114,49 @@ describe('build query', () => { expect(result).toEqual(expectedResult); }); + it('should remove match_all clauses', () => { + const filters = [ + { + match_all: {}, + meta: { type: 'match_all' }, + } as MatchAllFilter, + { + match: { + a: 'b', + }, + meta: { + alias: '', + disabled: false, + negate: false, + }, + }, + ]; + const config = { + allowLeadingWildcards: true, + queryStringOptions: {}, + ignoreFilterIfFieldNotInIndex: false, + }; + + const expectedResult = { + bool: { + must: [], + filter: [ + { + match: { + a: 'b', + }, + }, + ], + should: [], + must_not: [], + }, + }; + + const result = buildEsQuery(indexPattern, [], filters, config); + + expect(result).toEqual(expectedResult); + }); + it('should use the default time zone set in the Advanced Settings in queries and filters', () => { const queries = [ { query: '@timestamp:"2019-03-23T13:18:00"', language: 'kuery' }, @@ -122,7 +185,6 @@ describe('build query', () => { indexPattern, config ), - { match_all: {} }, ], should: [], must_not: [], diff --git a/src/plugins/data/common/es_query/es_query/build_es_query.ts b/src/plugins/data/common/es_query/es_query/build_es_query.ts index 18b360de9aaa6..45724796c3518 100644 --- a/src/plugins/data/common/es_query/es_query/build_es_query.ts +++ b/src/plugins/data/common/es_query/es_query/build_es_query.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { groupBy, has } from 'lodash'; +import { groupBy, has, isEqual } from 'lodash'; import { buildQueryFromKuery } from './from_kuery'; import { buildQueryFromFilters } from './from_filters'; import { buildQueryFromLucene } from './from_lucene'; @@ -21,6 +21,12 @@ export interface EsQueryConfig { dateFormatTZ?: string; } +function removeMatchAll(filters: T[]) { + return filters.filter( + (filter) => !filter || typeof filter !== 'object' || !isEqual(filter, { match_all: {} }) + ); +} + /** * @param indexPattern * @param queries - a query object or array of query objects. Each query has a language property and a query property. @@ -63,9 +69,9 @@ export function buildEsQuery( return { bool: { - must: [...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must], - filter: [...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter], - should: [...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should], + must: removeMatchAll([...kueryQuery.must, ...luceneQuery.must, ...filterQuery.must]), + filter: removeMatchAll([...kueryQuery.filter, ...luceneQuery.filter, ...filterQuery.filter]), + should: removeMatchAll([...kueryQuery.should, ...luceneQuery.should, ...filterQuery.should]), must_not: [...kueryQuery.must_not, ...luceneQuery.must_not, ...filterQuery.must_not], }, }; diff --git a/src/plugins/data/public/search/session/session_service.test.ts b/src/plugins/data/public/search/session/session_service.test.ts index 13a1a1bd388ba..39680c4948366 100644 --- a/src/plugins/data/public/search/session/session_service.test.ts +++ b/src/plugins/data/public/search/session/session_service.test.ts @@ -98,14 +98,6 @@ describe('Session service', () => { expect(nowProvider.reset).toHaveBeenCalled(); }); - it("Can't clear other apps' session", async () => { - sessionService.start(); - expect(sessionService.getSessionId()).not.toBeUndefined(); - currentAppId$.next('change'); - sessionService.clear(); - expect(sessionService.getSessionId()).not.toBeUndefined(); - }); - it("Can start a new session in case there is other apps' stale session", async () => { const s1 = sessionService.start(); expect(sessionService.getSessionId()).not.toBeUndefined(); diff --git a/src/plugins/data/public/search/session/session_service.ts b/src/plugins/data/public/search/session/session_service.ts index 71f51b4bc8d83..629d76b07d7ca 100644 --- a/src/plugins/data/public/search/session/session_service.ts +++ b/src/plugins/data/public/search/session/session_service.ts @@ -128,21 +128,6 @@ export class SessionService { this.subscription.add( coreStart.application.currentAppId$.subscribe((newAppName) => { this.currentApp = newAppName; - if (!this.getSessionId()) return; - - // Apps required to clean up their sessions before unmounting - // Make sure that apps don't leave sessions open by throwing an error in DEV mode - const message = `Application '${ - this.state.get().appName - }' had an open session while navigating`; - if (initializerContext.env.mode.dev) { - coreStart.fatalErrors.add(message); - } else { - // this should never happen in prod because should be caught in dev mode - // in case this happen we don't want to throw fatal error, as most likely possible bugs are not that critical - // eslint-disable-next-line no-console - console.warn(message); - } }) ); }); @@ -230,18 +215,6 @@ export class SessionService { * Cleans up current state */ public clear() { - // make sure apps can't clear other apps' sessions - const currentSessionApp = this.state.get().appName; - if (currentSessionApp && currentSessionApp !== this.currentApp) { - // eslint-disable-next-line no-console - console.warn( - `Skip clearing session "${this.getSessionId()}" because it belongs to a different app. current: "${ - this.currentApp - }", owner: "${currentSessionApp}"` - ); - return; - } - this.state.transitions.clear(); this.searchSessionInfoProvider = undefined; this.searchSessionIndicatorUiConfig = undefined; diff --git a/src/plugins/vis_type_vega/public/data_model/types.ts b/src/plugins/vis_type_vega/public/data_model/types.ts index 8590b51d3b5ff..255bd9774f9df 100644 --- a/src/plugins/vis_type_vega/public/data_model/types.ts +++ b/src/plugins/vis_type_vega/public/data_model/types.ts @@ -147,7 +147,7 @@ export interface Bool { bool?: Bool; must?: DslQuery[]; filter?: Filter[]; - should?: never[]; + should?: Filter[]; must_not?: Filter[]; } diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx index f2640c5c32acf..e84f6fd43418b 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.test.tsx @@ -12,12 +12,13 @@ import { LensEmbeddableInput } from '../editor_frame_service/embeddable/embeddab const defaultSavedObjectId = '1234'; describe('Mounter', () => { + const byValueFlag = { allowByValueEmbeddables: true }; describe('loadDocument', () => { it('does not load a document if there is no initial input', async () => { const services = makeDefaultServices(); const redirectCallback = jest.fn(); const lensStore = mockLensStore({ data: services.data }); - await loadDocument(redirectCallback, undefined, services, lensStore); + await loadDocument(redirectCallback, undefined, services, lensStore, undefined, byValueFlag); expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); }); @@ -39,7 +40,9 @@ describe('Mounter', () => { redirectCallback, { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); @@ -76,7 +79,9 @@ describe('Mounter', () => { redirectCallback, { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); @@ -85,7 +90,9 @@ describe('Mounter', () => { redirectCallback, { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); @@ -96,7 +103,9 @@ describe('Mounter', () => { redirectCallback, { savedObjectId: '5678' } as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); @@ -116,7 +125,9 @@ describe('Mounter', () => { redirectCallback, { savedObjectId: defaultSavedObjectId } as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); expect(services.attributeService.unwrapAttributes).toHaveBeenCalledWith({ @@ -136,7 +147,9 @@ describe('Mounter', () => { redirectCallback, ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput, services, - lensStore + lensStore, + undefined, + byValueFlag ); }); diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 708573e843fcf..3e56fbb2003cb 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -17,6 +17,7 @@ import { i18n } from '@kbn/i18n'; import { DashboardFeatureFlagConfig } from 'src/plugins/dashboard/public'; import { Provider } from 'react-redux'; import { uniq, isEqual } from 'lodash'; +import { EmbeddableEditorState } from 'src/plugins/embeddable/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; import { LensReportManager, setReportManager, trackUiEvent } from '../lens_ui_telemetry'; @@ -71,6 +72,8 @@ export async function mountApp( const historyLocationState = params.history.location.state as HistoryLocationState; const embeddableEditorIncomingState = stateTransfer?.getIncomingEditorState(APP_ID); + const dashboardFeatureFlag = await getByValueFeatureFlag(); + const lensServices: LensAppServices = { data, storage, @@ -92,7 +95,7 @@ export async function mountApp( }, // Temporarily required until the 'by value' paradigm is default. - dashboardFeatureFlag: await getByValueFeatureFlag(), + dashboardFeatureFlag, }; addHelpMenuToAppChrome(coreStart.chrome, coreStart.docLinks); @@ -172,7 +175,6 @@ export async function mountApp( if (!initialContext) { data.query.filterManager.setAppFilters([]); } - const preloadedState = getPreloadedState({ query: data.query.queryString.getQuery(), // Do not use app-specific filters from previous app, @@ -180,7 +182,7 @@ export async function mountApp( filters: !initialContext ? data.query.filterManager.getGlobalFilters() : data.query.filterManager.getFilters(), - searchSessionId: data.search.session.start(), + searchSessionId: data.search.session.getSessionId(), resolvedDateRange: getResolvedDateRange(data.query.timefilter.timefilter), isLinkedToOriginatingApp: Boolean(embeddableEditorIncomingState?.originatingApp), }); @@ -197,7 +199,14 @@ export async function mountApp( ); trackUiEvent('loaded'); const initialInput = getInitialInput(props.id, props.editByValue); - loadDocument(redirectCallback, initialInput, lensServices, lensStore); + loadDocument( + redirectCallback, + initialInput, + lensServices, + lensStore, + embeddableEditorIncomingState, + dashboardFeatureFlag + ); return ( { - data.search.session.clear(); unmountComponentAtNode(params.element); unlistenParentHistory(); lensStore.dispatch(navigateAway()); @@ -276,7 +284,9 @@ export function loadDocument( redirectCallback: (savedObjectId?: string) => void, initialInput: LensEmbeddableInput | undefined, lensServices: LensAppServices, - lensStore: LensRootStore + lensStore: LensRootStore, + embeddableEditorIncomingState: EmbeddableEditorState | undefined, + dashboardFeatureFlag: DashboardFeatureFlagConfig ) { const { attributeService, chrome, notifications, data } = lensServices; const { persistedDoc } = lensStore.getState().app; @@ -317,12 +327,20 @@ export function loadDocument( data.query.filterManager.setAppFilters( injectFilterReferences(doc.state.filters, doc.references) ); + const currentSessionId = data.search.session.getSessionId(); lensStore.dispatch( setState({ query: doc.state.query, isAppLoading: false, indexPatternsForTopNav: indexPatterns, lastKnownDoc: doc, + searchSessionId: + dashboardFeatureFlag.allowByValueEmbeddables && + Boolean(embeddableEditorIncomingState?.originatingApp) && + !(initialInput as LensByReferenceInput)?.savedObjectId && + currentSessionId + ? currentSessionId + : data.search.session.start(), ...(!isEqual(persistedDoc, doc) ? { persistedDoc: doc } : null), }) ); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index eeec7871a262c..03eb234d90766 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -237,7 +237,7 @@ const initialState: IndexPatternPrivateState = { isFirstExistenceFetch: false, }; -const dslQuery = { bool: { must: [{ match_all: {} }], filter: [], should: [], must_not: [] } }; +const dslQuery = { bool: { must: [], filter: [], should: [], must_not: [] } }; describe('IndexPattern Data Panel', () => { let defaultProps: Parameters[0] & { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx index cf9f7c0c559e4..2aa031959f5d7 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.test.tsx @@ -164,7 +164,7 @@ describe('IndexPattern Field Item', () => { body: JSON.stringify({ dslQuery: { bool: { - must: [{ match_all: {} }], + must: [], filter: [], should: [], must_not: [], diff --git a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx index 4871cfcb069d2..f1eab38c56db0 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/hosts.test.tsx @@ -155,7 +155,7 @@ describe('Hosts - rendering', () => { myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(HostsTabs).props().filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx index 862a4f1a56c12..764b8fcd0444b 100644 --- a/x-pack/plugins/security_solution/public/network/pages/network.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/network.test.tsx @@ -159,7 +159,7 @@ describe('Network page - rendering', () => { myStore.dispatch(inputsActions.setSearchBarFilter({ id: 'global', filters: newFilters })); wrapper.update(); expect(wrapper.find(NetworkRoutes).props().filterQuery).toEqual( - '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' + '{"bool":{"must":[],"filter":[{"bool":{"filter":[{"bool":{"should":[{"match_phrase":{"host.name":"ItRocks"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}' ); }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx index 7512e4b68dd4a..7e6dbe80dc7b0 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/helpers.test.tsx @@ -255,7 +255,7 @@ describe('Combined Queries', () => { isEventViewer, }) ).toEqual({ - filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + filterQuery: '{"bool":{"must":[],"filter":[],"should":[],"must_not":[]}}', }); }); @@ -299,7 +299,7 @@ describe('Combined Queries', () => { }) ).toEqual({ filterQuery: - '{"bool":{"must":[],"filter":[{"match_all":{}},{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', + '{"bool":{"must":[],"filter":[{"exists":{"field":"host.name"}}],"should":[],"must_not":[]}}', }); }); diff --git a/x-pack/test/functional/apps/maps/mvt_scaling.js b/x-pack/test/functional/apps/maps/mvt_scaling.js index d0e2ddd0ae9f0..66a511f6e9fec 100644 --- a/x-pack/test/functional/apps/maps/mvt_scaling.js +++ b/x-pack/test/functional/apps/maps/mvt_scaling.js @@ -31,7 +31,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect( mapboxStyle.sources[VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!((match_all:())),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape` + `/api/maps/mvt/getTile/{z}/{x}/{y}.pbf?geometryFieldName=geometry&index=geo_shapes*&requestBody=(_source:!(geometry),docvalue_fields:!(prop1),query:(bool:(filter:!(),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(),size:10001,stored_fields:!(geometry,prop1))&geoFieldType=geo_shape` ) ).to.equal(true); diff --git a/x-pack/test/functional/apps/maps/mvt_super_fine.js b/x-pack/test/functional/apps/maps/mvt_super_fine.js index 8608b62dee8f7..dcd2923cb9335 100644 --- a/x-pack/test/functional/apps/maps/mvt_super_fine.js +++ b/x-pack/test/functional/apps/maps/mvt_super_fine.js @@ -34,7 +34,7 @@ export default function ({ getPageObjects, getService }) { //Source should be correct expect( mapboxStyle.sources[MB_VECTOR_SOURCE_ID].tiles[0].startsWith( - `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((match_all:()),(range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point` + `/api/maps/mvt/getGridTile/{z}/{x}/{y}.pbf?geometryFieldName=geo.coordinates&index=logstash-*&requestBody=(_source:(excludes:!()),aggs:(gridSplit:(aggs:(gridCentroid:(geo_centroid:(field:geo.coordinates)),max_of_bytes:(max:(field:bytes))),geotile_grid:(bounds:!n,field:geo.coordinates,precision:!n,shard_size:65535,size:65535))),fields:!((field:'@timestamp',format:date_time),(field:'relatedContent.article:modified_time',format:date_time),(field:'relatedContent.article:published_time',format:date_time),(field:utc_time,format:date_time)),query:(bool:(filter:!((range:('@timestamp':(format:strict_date_optional_time,gte:'2015-09-20T00:00:00.000Z',lte:'2015-09-20T01:00:00.000Z')))),must:!(),must_not:!(),should:!())),runtime_mappings:(),script_fields:(hour_of_day:(script:(lang:painless,source:'doc[!'@timestamp!'].value.getHour()'))),size:0,stored_fields:!('*'))&requestType=grid&geoFieldType=geo_point` ) ).to.equal(true); From 7b127ac0eeb6eb637a496c30a848467bee82c156 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Mon, 31 May 2021 13:24:15 +0300 Subject: [PATCH 02/46] [Vega] fix redundant scrollbars in default vega config (#97210) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- src/plugins/vis_type_vega/public/components/vega_vis.scss | 2 +- src/plugins/vis_type_vega/public/vega_view/vega_base_view.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/plugins/vis_type_vega/public/components/vega_vis.scss b/src/plugins/vis_type_vega/public/components/vega_vis.scss index f0062869e0046..5b96eb9a560c7 100644 --- a/src/plugins/vis_type_vega/public/components/vega_vis.scss +++ b/src/plugins/vis_type_vega/public/components/vega_vis.scss @@ -18,7 +18,7 @@ z-index: 0; flex: 1 1 100%; - display: block; + //display determined by js max-width: 100%; max-height: 100%; width: 100%; diff --git a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js index b7f2b064cf9c2..54084c7476b6b 100644 --- a/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js +++ b/src/plugins/vis_type_vega/public/vega_view/vega_base_view.js @@ -83,9 +83,10 @@ export class VegaBaseView { return; } + const containerDisplay = this._parser.useResize ? 'flex' : 'block'; this._$container = $('
') // Force a height here because css is not loaded in mocha test - .css('height', '100%') + .css({ height: '100%', display: containerDisplay }) .appendTo(this._$parentEl); this._$controls = $( `
` From 0cf48f3ecfd999053e4583088600fbb134bb0db7 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 31 May 2021 15:06:52 +0200 Subject: [PATCH 03/46] [Discover] Fix infinite scrolling using Classic table (#97634) * Fix infinite scrolling * Add functional tests --- .../angular/doc_table/infinite_scroll.ts | 3 +- test/functional/apps/discover/_doc_table.ts | 41 +++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts index f2377b61b5151..2029354376f26 100644 --- a/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts +++ b/src/plugins/discover/public/application/angular/doc_table/infinite_scroll.ts @@ -34,11 +34,12 @@ export function createInfiniteScrollDirective() { const isMobileView = document.getElementsByClassName('dscSidebar__mobile').length > 0; const usedScrollDiv = isMobileView ? scrollDivMobile : scrollDiv; const scrollTop = usedScrollDiv.scrollTop(); + const scrollOffset = usedScrollDiv.prop('offsetTop') || 0; const winHeight = Number(usedScrollDiv.height()); const winBottom = Number(winHeight) + Number(scrollTop); const elTop = $element.get(0).offsetTop || 0; - const remaining = elTop - winBottom; + const remaining = elTop - scrollOffset - winBottom; if (remaining <= winHeight) { $scope[$scope.$$phase ? '$eval' : '$apply'](function () { diff --git a/test/functional/apps/discover/_doc_table.ts b/test/functional/apps/discover/_doc_table.ts index df148a35aaf24..6f298a364abfa 100644 --- a/test/functional/apps/discover/_doc_table.ts +++ b/test/functional/apps/discover/_doc_table.ts @@ -10,6 +10,7 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { + const browser = getService('browser'); const log = getService('log'); const retry = getService('retry'); const esArchiver = getService('esArchiver'); @@ -59,6 +60,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.timePicker.setDefaultAbsoluteRange(); }); + describe('classic table in window 900x700', async function () { + before(async () => { + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await browser.setWindowSize(900, 700); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () { + const initialRows = await testSubjects.findAll('docTableRow'); + await testSubjects.scrollIntoView('discoverBackToTop'); + // now count the rows + await retry.waitFor('next batch of documents to be displayed', async () => { + const actual = await testSubjects.findAll('docTableRow'); + log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`); + return actual.length > initialRows.length; + }); + }); + }); + + describe('classic table in window 600x700', async function () { + before(async () => { + await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); + await browser.setWindowSize(600, 700); + await PageObjects.common.navigateToApp('discover'); + await PageObjects.discover.waitUntilSearchingHasFinished(); + }); + + it(`should load up to ${rowsHardLimit} rows when scrolling at the end of the table with `, async function () { + const initialRows = await testSubjects.findAll('docTableRow'); + await testSubjects.scrollIntoView('discoverBackToTop'); + // now count the rows + await retry.waitFor('next batch of documents to be displayed', async () => { + const actual = await testSubjects.findAll('docTableRow'); + log.debug(`initial doc nr: ${initialRows.length}, actual doc nr: ${actual.length}`); + return actual.length > initialRows.length; + }); + }); + }); + describe('legacy', async function () { before(async () => { await kibanaServer.uiSettings.update({ 'doc_table:legacy': true }); From 055ade5947ba63bd4c4fff1b9edc2cb5221de08a Mon Sep 17 00:00:00 2001 From: Yaroslav Kuznietsov Date: Mon, 31 May 2021 17:12:49 +0300 Subject: [PATCH 04/46] [TSVB] Fix color rules are not applied for null series (#100404) * Fixed color rules behaviour on empty metrics data. Refactored function `getLastValue`, which was providing unexpected result on empty arrays. It was returning string, instead of null/undefined value. Created two useful functions, which are providing possibility to handle empty result and get default value, which is expected. * Tests added. Tests for getLastValue refactored. Tests for getLastValueOrDefault and getLastValueOrZero written. * Removed console.log * Added default value for empty operator. Added default value for empty operator, which will compare statistics to empty array. Added conditional render of colorRuleValue field, if operator doesn't require some specified value to be chosen ( as default, in this case ). * Added empty data handling. Added empty value var and way of displaying in widgets. Added way of handling empty results and prevented comparing null, empty array and numeric rules. * Prettier fixes. * Added the same logic of displaying data to gauge. Added displaying of empty data to gauge module. Fixed label color styles (before, it was ignoring, because of setting colorValue out of default scope while reactcss(...) call). * Added empty data handling in Top N chart. * Removed getLastValueOrZero. Removed getLastValueOrZero and replaced by getLastValueOrEmpty. * Added isEmptyValue function. Added isEmptyValue function, which is able to check equality. It provides a possibility to encapsulate the logic of empty value and flexible changing of its' behavior. * Fixed and refactor. Fixed hidden value input, if no operator selected. Removed useless DEFAULT_VALUE and getLastValueOrDefault. * Color rules Tests. Changed from js to ts last_value_utils. Updated tests for color_rules component. * Replaces isEqual rule with eq. * Migrations added. * Fixed types, EMPTY_VALUE, empty method. Removed type definition for methods in last_value_utils.ts. Changed EMPTY_VALUE from array to null. Removed default value. Added logic for handling empty value handling and comparison methods. * Fixed comparing null and numeric rules. * Changed migrations. * Added test for migrations. * Migration fix. * Updated code, based on nits and fixed reasons of pipeline errors. * Moved actions, connected to operators to the separate file. Reduced duplication of code. * Type names changed. * Test for operators_utils added. * Fixed based on nits. * Added vis_type_timeseries to tsconfig references. * Changed version and added migrations. * Small fix in migrations. * Fixes based on review. * Revert "Fixes based on review." This reverts commit 35af7b2b6a0c7a44a5d57c991837f9f53a517870. * Fixes based on review. Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../common/get_last_value.test.js | 40 -------- .../common/last_value_utils.test.ts | 60 ++++++++++++ ...{get_last_value.js => last_value_utils.ts} | 15 +-- .../common/operators_utils.test.ts | 59 +++++++++++ .../common/operators_utils.ts | 47 +++++++++ .../components/color_rules.test.tsx | 69 ++++++++++--- .../application/components/color_rules.tsx | 69 +++++++++---- .../components/lib/convert_series_to_vars.js | 2 +- .../components/lib/tick_formatter.js | 10 +- .../components/vis_types/gauge/vis.js | 11 ++- .../components/vis_types/metric/vis.js | 11 ++- .../components/vis_types/top_n/vis.js | 10 +- .../application/visualizations/views/gauge.js | 13 +-- .../visualizations/views/gauge_vis.js | 9 +- .../visualizations/views/metric.js | 6 +- .../application/visualizations/views/top_n.js | 11 ++- .../lib/vis_data/table/process_bucket.js | 2 +- .../visualize_embeddable_factory.ts | 9 ++ .../visualization_common_migrations.ts | 49 ++++++++++ ...ualization_saved_object_migrations.test.ts | 97 +++++++++++++++++++ .../visualization_saved_object_migrations.ts | 25 +++++ 21 files changed, 506 insertions(+), 118 deletions(-) delete mode 100644 src/plugins/vis_type_timeseries/common/get_last_value.test.js create mode 100644 src/plugins/vis_type_timeseries/common/last_value_utils.test.ts rename src/plugins/vis_type_timeseries/common/{get_last_value.js => last_value_utils.ts} (50%) create mode 100644 src/plugins/vis_type_timeseries/common/operators_utils.test.ts create mode 100644 src/plugins/vis_type_timeseries/common/operators_utils.ts diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.test.js b/src/plugins/vis_type_timeseries/common/get_last_value.test.js deleted file mode 100644 index 794bbe17a1e7a..0000000000000 --- a/src/plugins/vis_type_timeseries/common/get_last_value.test.js +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getLastValue } from './get_last_value'; - -describe('getLastValue(data)', () => { - test('should returns data if data is not array', () => { - expect(getLastValue('foo')).toBe('foo'); - }); - - test('should returns 0 as a value when not an array', () => { - expect(getLastValue(0)).toBe(0); - }); - - test('should returns the last value', () => { - expect(getLastValue([[1, 2]])).toBe(2); - }); - - test('should return 0 as a valid value', () => { - expect(getLastValue([[0, 0]])).toBe(0); - }); - - test('should returns the default value ', () => { - expect(getLastValue()).toBe('-'); - }); - - test('should returns 0 if second to last is not defined (default)', () => { - expect( - getLastValue([ - [1, null], - [2, null], - ]) - ).toBe('-'); - }); -}); diff --git a/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts b/src/plugins/vis_type_timeseries/common/last_value_utils.test.ts new file mode 100644 index 0000000000000..34e1265b9a6a2 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/last_value_utils.test.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getLastValue, isEmptyValue, EMPTY_VALUE } from './last_value_utils'; +import { clone } from 'lodash'; + +describe('getLastValue(data)', () => { + test('should return data, if data is not an array', () => { + const data = 'foo'; + expect(getLastValue(data)).toBe(data); + }); + + test('should return 0 as a value, when data is not an array', () => { + expect(getLastValue(0)).toBe(0); + }); + + test('should return the last value', () => { + const lastVal = 2; + expect(getLastValue([[1, lastVal]])).toBe(lastVal); + }); + + test('should return 0 as a valid value', () => { + expect(getLastValue([[0, 0]])).toBe(0); + }); + + test("should return empty value (null), if second array is empty or it's last element is null/undefined (default)", () => { + expect( + getLastValue([ + [1, null], + [2, null], + ]) + ).toBe(EMPTY_VALUE); + + expect( + getLastValue([ + [1, null], + [2, undefined], + ]) + ).toBe(EMPTY_VALUE); + }); +}); + +describe('isEmptyValue(value)', () => { + test('should return true if is equal to the empty value', () => { + // if empty value will change, no need to rewrite test for passing it. + const emptyValue = + typeof EMPTY_VALUE === 'object' && EMPTY_VALUE != null ? clone(EMPTY_VALUE) : EMPTY_VALUE; + expect(isEmptyValue(emptyValue)).toBe(true); + }); + + test('should return the last value', () => { + const notEmptyValue = [...Array(10).keys()]; + expect(isEmptyValue(notEmptyValue)).toBe(false); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/get_last_value.js b/src/plugins/vis_type_timeseries/common/last_value_utils.ts similarity index 50% rename from src/plugins/vis_type_timeseries/common/get_last_value.js rename to src/plugins/vis_type_timeseries/common/last_value_utils.ts index 80adf7098f24d..a51a04962a891 100644 --- a/src/plugins/vis_type_timeseries/common/get_last_value.js +++ b/src/plugins/vis_type_timeseries/common/last_value_utils.ts @@ -6,16 +6,19 @@ * Side Public License, v 1. */ -import { isArray, last } from 'lodash'; +import { isArray, last, isEqual } from 'lodash'; -export const DEFAULT_VALUE = '-'; +export const EMPTY_VALUE = null; +export const DISPLAY_EMPTY_VALUE = '-'; -const extractValue = (data) => (data && data[1]) ?? null; +const extractValue = (data: unknown[] | void) => (data && data[1]) ?? EMPTY_VALUE; -export const getLastValue = (data) => { +export const getLastValue = (data: unknown) => { if (!isArray(data)) { - return data ?? DEFAULT_VALUE; + return data; } - return extractValue(last(data)) ?? DEFAULT_VALUE; + return extractValue(last(data)); }; + +export const isEmptyValue = (value: unknown) => isEqual(value, EMPTY_VALUE); diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.test.ts b/src/plugins/vis_type_timeseries/common/operators_utils.test.ts new file mode 100644 index 0000000000000..ad66f058a4918 --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/operators_utils.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { getOperator, shouldOperate, Rule, Operator } from './operators_utils'; + +describe('getOperator(operator)', () => { + test('should return operator function', () => { + const operatorName = Operator.Gte; + const operator = getOperator(operatorName); + expect(typeof operator).toBe('function'); + }); +}); + +describe('shouldOperate(rule, value)', () => { + test('should operate, if value is not null and rule value is not null', () => { + const rule: Rule = { + value: 1, + operator: Operator.Gte, + }; + const value = 2; + + expect(shouldOperate(rule, value)).toBeTruthy(); + }); + + test('should operate, if value is null and operator allows null value', () => { + const rule: Rule = { + operator: Operator.Empty, + value: null, + }; + const value = null; + + expect(shouldOperate(rule, value)).toBeTruthy(); + }); + + test("should not operate, if value is null and operator doesn't allow null values", () => { + const rule: Rule = { + operator: Operator.Gte, + value: 2, + }; + const value = null; + + expect(shouldOperate(rule, value)).toBeFalsy(); + }); + + test("should not operate, if rule value is null and operator doesn't allow null values", () => { + const rule: Rule = { + operator: Operator.Gte, + value: null, + }; + const value = 3; + + expect(shouldOperate(rule, value)).toBeFalsy(); + }); +}); diff --git a/src/plugins/vis_type_timeseries/common/operators_utils.ts b/src/plugins/vis_type_timeseries/common/operators_utils.ts new file mode 100644 index 0000000000000..603e63159b22d --- /dev/null +++ b/src/plugins/vis_type_timeseries/common/operators_utils.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { gt, gte, lt, lte, isNull } from 'lodash'; + +export enum Operator { + Gte = 'gte', + Lte = 'lte', + Gt = 'gt', + Lt = 'lt', + Empty = 'empty', +} + +export interface Rule { + operator: Operator; + value: unknown; +} + +type OperatorsAllowNullType = { + [name in Operator]?: boolean; +}; + +const OPERATORS = { + [Operator.Gte]: gte, + [Operator.Lte]: lte, + [Operator.Gt]: gt, + [Operator.Lt]: lt, + [Operator.Empty]: isNull, +}; + +const OPERATORS_ALLOW_NULL: OperatorsAllowNullType = { + [Operator.Empty]: true, +}; + +export const getOperator = (operator: Operator) => { + return OPERATORS[operator]; +}; + +// This check is necessary for preventing from comparing null values with numeric rules. +export const shouldOperate = (rule: Rule, value: unknown) => + (isNull(rule.value) && OPERATORS_ALLOW_NULL[rule.operator]) || + (!isNull(rule.value) && !isNull(value)); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx index 9ea8898636cec..3b1356d571749 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.test.tsx @@ -12,22 +12,51 @@ import { findTestSubject } from '@elastic/eui/lib/test'; import { mountWithIntl } from '@kbn/test/jest'; import { collectionActions } from './lib/collection_actions'; -import { ColorRules, ColorRulesProps } from './color_rules'; +import { + ColorRules, + ColorRulesProps, + colorRulesOperatorsList, + ColorRulesOperator, +} from './color_rules'; +import { Operator } from '../../../common/operators_utils'; describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js', () => { - const defaultProps = ({ + const emptyRule: ColorRulesOperator = colorRulesOperatorsList.filter( + (operator) => operator.method === Operator.Empty + )[0]; + const notEmptyRule: ColorRulesOperator = colorRulesOperatorsList.filter( + (operator) => operator.method !== Operator.Empty + )[0]; + + const getColorRulesProps = (gaugeColorRules: unknown = []) => ({ name: 'gauge_color_rules', - model: { - gauge_color_rules: [ - { - gauge: null, - value: 0, - id: 'unique value', - }, - ], - }, + model: { gauge_color_rules: gaugeColorRules }, onChange: jest.fn(), - } as unknown) as ColorRulesProps; + }); + + const defaultProps = (getColorRulesProps([ + { + gauge: null, + value: 0, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; + + const emptyColorRuleProps = (getColorRulesProps([ + { + operator: emptyRule?.method, + value: emptyRule?.value, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; + + const notEmptyColorRuleProps = (getColorRulesProps([ + { + operator: notEmptyRule?.method, + value: notEmptyRule?.value, + id: 'unique value', + }, + ]) as unknown) as ColorRulesProps; describe('ColorRules', () => { it('should render empty
node', () => { @@ -47,6 +76,7 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js' expect(isNode).toBeTruthy(); }); + it('should handle change of operator and value correctly', () => { collectionActions.handleChange = jest.fn(); const wrapper = mountWithIntl(); @@ -57,8 +87,23 @@ describe('src/legacy/core_plugins/metrics/public/components/color_rules.test.js' expect((collectionActions.handleChange as jest.Mock).mock.calls[0][1].operator).toEqual('gt'); const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + numberInput.simulate('change', { target: { value: '123' } }); expect((collectionActions.handleChange as jest.Mock).mock.calls[1][1].value).toEqual(123); }); + + it('should handle render of value field if empty value oparetor is selected by default', () => { + collectionActions.handleChange = jest.fn(); + const wrapper = mountWithIntl(); + const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + expect(numberInput.exists()).toBeFalsy(); + }); + + it('should handle render of value field if not empty operator is selected by default', () => { + collectionActions.handleChange = jest.fn(); + const wrapper = mountWithIntl(); + const numberInput = findTestSubject(wrapper, 'colorRuleValue'); + expect(numberInput.exists()).toBeTruthy(); + }); }); }); diff --git a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx index 7aea5f934ee90..0cc64528ae3f3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/color_rules.tsx @@ -23,6 +23,7 @@ import { AddDeleteButtons } from './add_delete_buttons'; import { collectionActions } from './lib/collection_actions'; import { ColorPicker, ColorPickerProps } from './color_picker'; import { TimeseriesVisParams } from '../../types'; +import { Operator } from '../../../common/operators_utils'; export interface ColorRulesProps { name: keyof TimeseriesVisParams; @@ -40,10 +41,17 @@ interface ColorRule { id: string; background_color?: string; color?: string; - operator?: string; + operator?: Operator; text?: string; } +export interface ColorRulesOperator { + label: string; + method: Operator; + value?: unknown; + hideValueSelector?: boolean; +} + const defaultSecondaryName = i18n.translate( 'visTypeTimeseries.colorRules.defaultSecondaryNameLabel', { @@ -54,33 +62,45 @@ const defaultPrimaryName = i18n.translate('visTypeTimeseries.colorRules.defaultP defaultMessage: 'background', }); -const operatorOptions = [ +export const colorRulesOperatorsList: ColorRulesOperator[] = [ { label: i18n.translate('visTypeTimeseries.colorRules.greaterThanLabel', { defaultMessage: '> greater than', }), - value: 'gt', + method: Operator.Gt, }, { label: i18n.translate('visTypeTimeseries.colorRules.greaterThanOrEqualLabel', { defaultMessage: '>= greater than or equal', }), - value: 'gte', + method: Operator.Gte, }, { label: i18n.translate('visTypeTimeseries.colorRules.lessThanLabel', { defaultMessage: '< less than', }), - value: 'lt', + method: Operator.Lt, }, { label: i18n.translate('visTypeTimeseries.colorRules.lessThanOrEqualLabel', { defaultMessage: '<= less than or equal', }), - value: 'lte', + method: Operator.Lte, + }, + { + label: i18n.translate('visTypeTimeseries.colorRules.emptyLabel', { + defaultMessage: 'empty', + }), + method: Operator.Empty, + hideValueSelector: true, }, ]; +const operatorOptions = colorRulesOperatorsList.map((operator) => ({ + label: operator.label, + value: operator.method, +})); + export class ColorRules extends Component { constructor(props: ColorRulesProps) { super(props); @@ -100,9 +120,14 @@ export class ColorRules extends Component { handleOperatorChange = (item: ColorRule) => { return (options: Array>) => { + const selectedOperator = colorRulesOperatorsList.find( + (operator) => options[0]?.value === operator.method + ); + const value = selectedOperator?.value ?? null; collectionActions.handleChange(this.props, { ...item, - operator: options[0].value, + operator: options[0]?.value, + value, }); }; }; @@ -119,7 +144,11 @@ export class ColorRules extends Component { const selectedOperatorOption = operatorOptions.find( (option) => model.operator === option.value ); + const selectedOperator = colorRulesOperatorsList.find( + (operator) => model.operator === operator.method + ); + const hideValueSelectorField = selectedOperator?.hideValueSelector ?? false; const labelStyle = { marginBottom: 0 }; let secondary; @@ -203,19 +232,19 @@ export class ColorRules extends Component { fullWidth /> - - - - - + {!hideValueSelectorField && ( + + + + )} { - let value; - - if (val === DEFAULT_VALUE) { - return val; + if (isEmptyValue(val)) { + return DISPLAY_EMPTY_VALUE; } + let value; + if (!isNumber(val)) { value = val; } else { diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js index a464771b01af3..6140726975cbd 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/gauge/vis.js @@ -10,9 +10,10 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; -import _, { get, isUndefined, assign, includes } from 'lodash'; +import { get, isUndefined, assign, includes } from 'lodash'; import { Gauge } from '../../../visualizations/views/gauge'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function getColors(props) { const { model, visData } = props; @@ -21,9 +22,9 @@ function getColors(props) { let gauge; if (model.gauge_color_rules) { model.gauge_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null) { - const value = (series[0] && getLastValue(series[0].data)) || 0; - if (_[rule.operator](value, rule.value)) { + if (rule.operator) { + const value = getLastValue(series[0]?.data); + if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) { gauge = rule.gauge; text = rule.text; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js index 3029bba04b450..b35ee977d3e44 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/metric/vis.js @@ -10,10 +10,11 @@ import PropTypes from 'prop-types'; import React from 'react'; import { visWithSplits } from '../../vis_with_splits'; import { createTickFormatter } from '../../lib/tick_formatter'; -import _, { get, isUndefined, assign, includes, pick } from 'lodash'; +import { get, isUndefined, assign, includes, pick } from 'lodash'; import { Metric } from '../../../visualizations/views/metric'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function getColors(props) { const { model, visData } = props; @@ -22,9 +23,9 @@ function getColors(props) { let background; if (model.background_color_rules) { model.background_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null) { - const value = (series[0] && getLastValue(series[0].data)) || 0; - if (_[rule.operator](value, rule.value)) { + if (rule.operator) { + const value = getLastValue(series[0]?.data); + if (shouldOperate(rule, value) && getOperator(rule.operator)(value, rule.value)) { background = rule.background_color; color = rule.color; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js index 41e6236cbc39b..0b3a24615c0e3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_types/top_n/vis.js @@ -9,13 +9,13 @@ import { getCoreStart } from '../../../../services'; import { createTickFormatter } from '../../lib/tick_formatter'; import { TopN } from '../../../visualizations/views/top_n'; -import { getLastValue } from '../../../../../common/get_last_value'; +import { getLastValue } from '../../../../../common/last_value_utils'; import { isBackgroundInverted } from '../../../lib/set_is_reversed'; import { replaceVars } from '../../lib/replace_vars'; import PropTypes from 'prop-types'; import React from 'react'; -import { sortBy, first, get, gt, gte, lt, lte } from 'lodash'; -const OPERATORS = { gt, gte, lt, lte }; +import { sortBy, first, get } from 'lodash'; +import { getOperator, shouldOperate } from '../../../../../common/operators_utils'; function sortByDirection(data, direction, fn) { if (direction === 'desc') { @@ -53,8 +53,8 @@ function TopNVisualization(props) { let color = item.color || seriesConfig.color; if (model.bar_color_rules) { model.bar_color_rules.forEach((rule) => { - if (rule.operator && rule.value != null && rule.bar_color) { - if (OPERATORS[rule.operator](value, rule.value)) { + if (shouldOperate(rule, value) && rule.operator && rule.bar_color) { + if (getOperator(rule.operator)(value, rule.value)) { color = rule.bar_color; } } diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js index 31ea3412972e8..000701c3a0764 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge.js @@ -11,7 +11,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import classNames from 'classnames'; import { isBackgroundInverted, isBackgroundDark } from '../../lib/set_is_reversed'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { getValueBy } from '../lib/get_value_by'; import { GaugeVis } from './gauge_vis'; import reactcss from 'reactcss'; @@ -61,7 +61,7 @@ export class Gauge extends Component { render() { const { metric, type } = this.props; const { scale, translateX, translateY } = this.state; - const value = metric && getLastValue(metric.data); + const value = getLastValue(metric?.data); const max = (metric && getValueBy('max', metric.data)) || 1; const formatter = (metric && (metric.tickFormatter || metric.formatter)) || @@ -76,16 +76,13 @@ export class Gauge extends Component { left: this.state.left || 0, transform: `matrix(${scale}, 0, 0, ${scale}, ${translateX}, ${translateY})`, }, - }, - valueColor: { - value: { + valueColor: { color: this.props.valueColor, }, }, }, this.props ); - const gaugeProps = { value, reversed: isBackgroundDark(this.props.backgroundColor), @@ -114,7 +111,7 @@ export class Gauge extends Component {
{title}
-
+
{formatter(value)}
{additionalLabel} @@ -127,7 +124,7 @@ export class Gauge extends Component { ref={(el) => (this.inner = el)} style={styles.inner} > -
+
{formatter(value)}
diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js index c8789f98969f8..30b7844a90fda 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/gauge_vis.js @@ -12,6 +12,7 @@ import _ from 'lodash'; import reactcss from 'reactcss'; import { calculateCoordinates } from '../lib/calculate_coordinates'; import { COLORS } from '../constants/chart'; +import { isEmptyValue } from '../../../../common/last_value_utils'; export class GaugeVis extends Component { constructor(props) { @@ -55,10 +56,14 @@ export class GaugeVis extends Component { render() { const { type, value, max, color } = this.props; + + // if value is empty array, no metrics to display. + const formattedValue = isEmptyValue(value) ? 1 : value; + const { scale, translateX, translateY } = this.state; const size = 2 * Math.PI * 50; const sliceSize = type === 'half' ? 0.6 : 1; - const percent = value < max ? value / max : 1; + const percent = formattedValue < max ? formattedValue / max : 1; const styles = reactcss( { default: { @@ -161,6 +166,6 @@ GaugeVis.propTypes = { max: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), metric: PropTypes.object, reversed: PropTypes.bool, - value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]), + value: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.array]), type: PropTypes.oneOf(['half', 'circle']), }; diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js index 17cadb94457b6..bc4230d0a15ef 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/metric.js @@ -11,7 +11,7 @@ import React, { Component } from 'react'; import _ from 'lodash'; import reactcss from 'reactcss'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { calculateCoordinates } from '../lib/calculate_coordinates'; export class Metric extends Component { @@ -58,7 +58,8 @@ export class Metric extends Component { const { metric, secondary } = this.props; const { scale, translateX, translateY } = this.state; const primaryFormatter = (metric && (metric.tickFormatter || metric.formatter)) || ((n) => n); - const primaryValue = primaryFormatter(getLastValue(metric && metric.data)); + const primaryValue = primaryFormatter(getLastValue(metric?.data)); + const styles = reactcss( { default: { @@ -120,7 +121,6 @@ export class Metric extends Component { if (this.props.reversed) { className += ' tvbVisMetric--reversed'; } - return (
(this.resize = el)} className="tvbVisMetric__resize"> diff --git a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js index 2559ed543e543..0c43ab157fbbb 100644 --- a/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js +++ b/src/plugins/vis_type_timeseries/public/application/visualizations/views/top_n.js @@ -8,7 +8,7 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue, isEmptyValue } from '../../../../common/last_value_utils'; import { labelDateFormatter } from '../../components/lib/label_date_formatter'; import { emptyLabel } from '../../../../common/empty_label'; import reactcss from 'reactcss'; @@ -97,15 +97,16 @@ export class TopN extends Component { const renderMode = TopN.getRenderMode(min, max); const key = `${item.id || item.label}`; const lastValue = getLastValue(item.data); + // if result is empty, all bar need to be colored. + const lastValueFormatted = isEmptyValue(lastValue) ? 1 : lastValue; const formatter = item.tickFormatter || this.props.tickFormatter; - const isPositiveValue = lastValue >= 0; + const isPositiveValue = lastValueFormatted >= 0; const intervalLength = TopN.calcDomain(renderMode, min, max); // if both are 0, the division returns NaN causing unexpected behavior. // For this it defaults to 0 - const width = 100 * (Math.abs(lastValue) / intervalLength) || 0; + const width = 100 * (Math.abs(lastValueFormatted) / intervalLength) || 0; const label = item.labelFormatted ? labelDateFormatter(item.labelFormatted) : item.label; - const styles = reactcss( { default: { @@ -150,7 +151,7 @@ export class TopN extends Component { const intervalSettings = this.props.series.reduce( (acc, series, index) => { - const value = getLastValue(series.data); + const value = getLastValue(series.data) ?? 1; return { min: !index || value < acc.min ? value : acc.min, diff --git a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js index 9e3a2ac71ed02..88b06d7f7ffaa 100644 --- a/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js +++ b/src/plugins/vis_type_timeseries/server/lib/vis_data/table/process_bucket.js @@ -8,7 +8,7 @@ import { buildProcessorFunction } from '../build_processor_function'; import { processors } from '../response_processors/table'; -import { getLastValue } from '../../../../common/get_last_value'; +import { getLastValue } from '../../../../common/last_value_utils'; import { first, get } from 'lodash'; import { overwrite } from '../helpers'; import { getActiveSeries } from '../helpers/get_active_series'; diff --git a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts index 6f214745e1291..212c033a65c26 100644 --- a/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts +++ b/src/plugins/visualizations/server/embeddable/visualize_embeddable_factory.ts @@ -13,6 +13,7 @@ import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, + commonAddEmptyValueColorRule, } from '../migrations/visualization_common_migrations'; const byValueAddSupportOfDualIndexSelectionModeInTSVB = (state: SerializableState) => { @@ -36,6 +37,13 @@ const byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (state: Serial }; }; +const byValueAddEmptyValueColorRule = (state: SerializableState) => { + return { + ...state, + savedVis: commonAddEmptyValueColorRule(state.savedVis), + }; +}; + export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { return { id: 'visualization', @@ -47,6 +55,7 @@ export const visualizeEmbeddableFactory = (): EmbeddableRegistryDefinition => { byValueHideTSVBLastValueIndicator, byValueRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel )(state), + '7.14.0': (state) => flow(byValueAddEmptyValueColorRule)(state), }, }; }; diff --git a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts index 3f09f19d9ac63..13b8d8c4a0f98 100644 --- a/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_common_migrations.ts @@ -6,6 +6,9 @@ * Side Public License, v 1. */ +import { get, last } from 'lodash'; +import uuid from 'uuid'; + export const commonAddSupportOfDualIndexSelectionModeInTSVB = (visState: any) => { if (visState && visState.type === 'metrics') { const { params } = visState; @@ -42,3 +45,49 @@ export const commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel = (visStat return visState; }; + +export const commonAddEmptyValueColorRule = (visState: any) => { + if (visState && visState.type === 'metrics') { + const params: any = get(visState, 'params') || {}; + + const getRuleWithComparingToZero = (rules: any[] = []) => { + const compareWithEqualMethods = ['gte', 'lte']; + return last( + rules.filter((rule) => compareWithEqualMethods.includes(rule.operator) && rule.value === 0) + ); + }; + + const convertRuleToEmpty = (rule: any = {}) => ({ + ...rule, + id: uuid.v4(), + operator: 'empty', + value: null, + }); + + const addEmptyRuleToListIfNecessary = (rules: any[]) => { + const rule = getRuleWithComparingToZero(rules); + + if (rule) { + return [...rules, convertRuleToEmpty(rule)]; + } + + return rules; + }; + + const colorRules = { + bar_color_rules: addEmptyRuleToListIfNecessary(params.bar_color_rules), + background_color_rules: addEmptyRuleToListIfNecessary(params.background_color_rules), + gauge_color_rules: addEmptyRuleToListIfNecessary(params.gauge_color_rules), + }; + + return { + ...visState, + params: { + ...params, + ...colorRules, + }, + }; + } + + return visState; +}; diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts index dbe5482c442b7..36e1635ad4730 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.test.ts @@ -2017,4 +2017,101 @@ describe('migration visualization', () => { expect(params.use_kibana_indexes).toBeFalsy(); }); }); + + describe('7.14.0 tsvb - add empty value rule to savedObjects with less and greater then zero rules', () => { + const migrate = (doc: any) => + visualizationSavedObjectTypeMigrations['7.14.0']( + doc as Parameters[0], + savedObjectMigrationContext + ); + + const rule1 = { value: 0, operator: 'lte', color: 'rgb(145, 112, 184)' }; + const rule2 = { value: 0, operator: 'gte', color: 'rgb(96, 146, 192)' }; + const rule3 = { value: 0, operator: 'gt', color: 'rgb(84, 179, 153)' }; + const rule4 = { value: 0, operator: 'lt', color: 'rgb(84, 179, 153)' }; + + const createTestDocWithType = (params: any) => ({ + attributes: { + title: 'My Vis', + description: 'This is my super cool vis.', + visState: `{ + "type":"metrics", + "params": ${JSON.stringify(params)} + }`, + }, + }); + + const checkEmptyRuleIsAddedToArray = ( + rulesArrayProperty: string, + prevParams: any, + migratedParams: any, + rule: any + ) => { + expect(migratedParams).toHaveProperty(rulesArrayProperty); + expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy(); + expect(migratedParams[rulesArrayProperty].length).toBe( + prevParams[rulesArrayProperty].length + 1 + ); + + const lastElementIndex = migratedParams[rulesArrayProperty].length - 1; + expect(migratedParams[rulesArrayProperty][lastElementIndex]).toHaveProperty('operator'); + expect(migratedParams[rulesArrayProperty][lastElementIndex].operator).toEqual('empty'); + expect(migratedParams[rulesArrayProperty][lastElementIndex].color).toEqual(rule.color); + }; + + const checkRuleIsNotAddedToArray = ( + rulesArrayProperty: string, + prevParams: any, + migratedParams: any, + rule: any + ) => { + expect(migratedParams).toHaveProperty(rulesArrayProperty); + expect(Array.isArray(migratedParams[rulesArrayProperty])).toBeTruthy(); + expect(migratedParams[rulesArrayProperty].length).toBe(prevParams[rulesArrayProperty].length); + // expects, that array contains one element... + expect(migratedParams[rulesArrayProperty][0].operator).toBe(rule.operator); + }; + + it('should add empty rule if operator = lte and value = 0', () => { + const params = { + bar_color_rules: [rule1], + background_color_rules: [rule1], + gauge_color_rules: [rule1], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule1); + checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule1); + checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule1); + }); + + it('should add empty rule if operator = gte and value = 0', () => { + const params = { + bar_color_rules: [rule2], + background_color_rules: [rule2], + gauge_color_rules: [rule2], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkEmptyRuleIsAddedToArray('bar_color_rules', params, migratedParams, rule2); + checkEmptyRuleIsAddedToArray('background_color_rules', params, migratedParams, rule2); + checkEmptyRuleIsAddedToArray('gauge_color_rules', params, migratedParams, rule2); + }); + + it('should not add empty rule if operator = gt or lt and value = any', () => { + const params = { + bar_color_rules: [rule3], + background_color_rules: [rule3], + gauge_color_rules: [rule4], + }; + const migratedTestDoc = migrate(createTestDocWithType(params)); + const { params: migratedParams } = JSON.parse(migratedTestDoc.attributes.visState); + + checkRuleIsNotAddedToArray('bar_color_rules', params, migratedParams, rule3); + checkRuleIsNotAddedToArray('background_color_rules', params, migratedParams, rule3); + checkRuleIsNotAddedToArray('gauge_color_rules', params, migratedParams, rule4); + }); + }); }); diff --git a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts index b9885588b6f76..c5050b4a6940b 100644 --- a/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts +++ b/src/plugins/visualizations/server/migrations/visualization_saved_object_migrations.ts @@ -15,6 +15,7 @@ import { commonAddSupportOfDualIndexSelectionModeInTSVB, commonHideTSVBLastValueIndicator, commonRemoveDefaultIndexPatternAndTimeFieldFromTSVBModel, + commonAddEmptyValueColorRule, } from './visualization_common_migrations'; const migrateIndexPattern: SavedObjectMigrationFn = (doc) => { @@ -966,6 +967,29 @@ const removeDefaultIndexPatternAndTimeFieldFromTSVBModel: SavedObjectMigrationFn }; }; +const addEmptyValueColorRule: SavedObjectMigrationFn = (doc) => { + const visStateJSON = get(doc, 'attributes.visState'); + let visState; + + if (visStateJSON) { + try { + visState = JSON.parse(visStateJSON); + } catch (e) { + // Let it go, the data is invalid and we'll leave it as is + } + const newVisState = commonAddEmptyValueColorRule(visState); + + return { + ...doc, + attributes: { + ...doc.attributes, + visState: JSON.stringify(newVisState), + }, + }; + } + return doc; +}; + export const visualizationSavedObjectTypeMigrations = { /** * We need to have this migration twice, once with a version prior to 7.0.0 once with a version @@ -1012,4 +1036,5 @@ export const visualizationSavedObjectTypeMigrations = { hideTSVBLastValueIndicator, removeDefaultIndexPatternAndTimeFieldFromTSVBModel ), + '7.14.0': flow(addEmptyValueColorRule), }; From 622bf0bf5a4e8c0203dd1c34a7f6bdbe553a35f3 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 31 May 2021 16:23:58 +0100 Subject: [PATCH 05/46] skip flaky suite (#100570) --- x-pack/test/functional/apps/spaces/enter_space.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 4e4d4974ced09..b2619ab0385ea 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,7 +14,8 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - describe('Enter Space', function () { + // FLAKY: https://github.com/elastic/kibana/issues/100570 + describe.skip('Enter Space', function () { this.tags('includeFirefox'); before(async () => { await esArchiver.load('spaces/enter_space'); From 5191dc57346db932fb91305dd40d7249f83654e1 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Mon, 31 May 2021 16:37:47 +0100 Subject: [PATCH 06/46] skip flaky suite (#98240) --- test/api_integration/apis/ui_counters/ui_counters.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/api_integration/apis/ui_counters/ui_counters.ts b/test/api_integration/apis/ui_counters/ui_counters.ts index aa201eb6a96ff..ab3ca2e8dd3a7 100644 --- a/test/api_integration/apis/ui_counters/ui_counters.ts +++ b/test/api_integration/apis/ui_counters/ui_counters.ts @@ -47,7 +47,8 @@ export default function ({ getService }: FtrProviderContext) { return savedObject; }; - describe('UI Counters API', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98240 + describe.skip('UI Counters API', () => { const dayDate = moment().format('DDMMYYYY'); before(async () => await esArchiver.emptyKibanaIndex()); From 6c879cc7c55c35a24c2bb69deb3e49a6707952ab Mon Sep 17 00:00:00 2001 From: Marco Liberati Date: Mon, 31 May 2021 18:16:11 +0200 Subject: [PATCH 07/46] [Lens] [Docs] Add more QA for Lens (#97142) Co-authored-by: Marta Bondyra Co-authored-by: Kaarina Tungseth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../images/lens_missing_values_strategy.png | Bin 0 -> 46415 bytes docs/user/dashboard/lens.asciidoc | 49 ++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 docs/user/dashboard/images/lens_missing_values_strategy.png diff --git a/docs/user/dashboard/images/lens_missing_values_strategy.png b/docs/user/dashboard/images/lens_missing_values_strategy.png new file mode 100644 index 0000000000000000000000000000000000000000..d77c230b533f582483bc029dc9f297dc9d729a13 GIT binary patch literal 46415 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. Sorting dimensions in visualizations is unsupported in *Lens*. +You can sort the dimensions for a single column in data tables: click the column header, then select the sorting criteria you want to use. +If you use the dimension as `Columns`, then all the columns that belong to the same dimension are sorted in the table. + [float] [[is-it-possible-to-use-saved-serches-in-lens]] ===== How do I visualize saved searches? @@ -254,3 +259,47 @@ Visualizing saved searches in unsupported in *Lens*. ===== How do I change the number of suggestions? Configuring the *Suggestions* that *Lens* automatically populates is unsupported. + +[float] +[[is-it-possible-to-use-different-indexpatterns-in-lens]] +===== Can I visualize multiple index patterns in a single visualization? + +You can create *Bar*, *Line* and *Area* charts from multiple index patterns. + +Each *Layer* in a visualization is associated with an index pattern and mutiple *Layers* can be combined together within the same visualization. Each *Layer* also has a chart switcher button in order to select the best type of visualization for the specific dataset. +You can also change the index pattern for a single *Layer*. + +[float] +[[why-my-field-x-is-missing-from-the-fields-list]] +===== Why is my field X missing from the fields list? + +*Lens* does not support the visualization of full-text fields, therefore it is not showing them in the data summary. + +[float] +[[how-to-handle-gaps-in-time-series-visualizations]] +===== How do I handle gaps in time series visualizations? + +*Lens* provides a set of features to handle missing values for *Area* and *Line* charts, which is useful for sparse data in time series data. + +To select a different way to represent missing values, open the *Visual options* menu, then select how to handle missing values. The default is to hide the missing values. ++ +[role="screenshot"] +image::images/lens_missing_values_strategy.png[Lens Missing values strategies menu] + +[float] +[[is-it-possible-to-change-the-scale-of-Y-axis]] +===== Is it possible to statically define the scale of the y-axis in a visualization? + +The ability to start the y-axis from another value than 0, or use a logarithmic scale, is unsupported in *Lens*. + +[float] +[[is-it-possible-to-have-pagination-for-datatable]] +===== Is it possible to have pagination in a data table? + +Pagination in a data table is unsupported in *Lens*. However, the <> supports pagination. + +[float] +[[is-it-possible-to-have-more-than-one-Y-axis-scale]] +===== Is it possible to have more than one y-axis scale in visualizations? + +*Lens* lets you pick, for each Y dimension, up to two distinct axis: *left* and *right*. Each axis can have a different scale. From af4b8e66263d3758ed8a3acb9c761e0e12fd5e60 Mon Sep 17 00:00:00 2001 From: Matthias Wilhelm Date: Mon, 31 May 2021 18:46:13 +0200 Subject: [PATCH 08/46] [Discover] Improve document selection menu - Change position of "Copy documents to clipboard (JSON)" and "Clear selection" --- .../discover_grid_document_selection.tsx | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index 4aaefc99479c1..a99819fa9e057 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -96,19 +96,6 @@ export function DiscoverGridDocumentToolbarBtn({ /> ), - - { - setIsSelectionPopoverOpen(false); - setSelectedDocs([]); - setIsFilterActive(false); - }} - > - - , )} , + { + setIsSelectionPopoverOpen(false); + setSelectedDocs([]); + setIsFilterActive(false); + }} + > + + , ]; }, [ isFilterActive, From d6828a221b755a5d57f62173157b3811c504aeda Mon Sep 17 00:00:00 2001 From: "Christiane (Tina) Heiligers" Date: Mon, 31 May 2021 16:04:46 -0700 Subject: [PATCH 09/46] [so-migrationsv2] Use named arguments in migrationsv2 actions (#100964) * Use named arguments in migrationsv2 actions * Addresses some optional review feedback --- .../migrationsv2/actions/index.test.ts | 96 ++- .../migrationsv2/actions/index.ts | 316 +++++--- .../integration_tests/actions.test.ts | 714 ++++++++++-------- .../migrations_state_machine_cleanup.ts | 2 +- .../server/saved_objects/migrationsv2/next.ts | 124 +-- 5 files changed, 779 insertions(+), 473 deletions(-) diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts index df74a4e1282e4..05da335d70884 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.test.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.test.ts @@ -37,7 +37,7 @@ describe('actions', () => { describe('fetchIndices', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.fetchIndices(client, ['my_index']); + const task = Actions.fetchIndices({ client, indices: ['my_index'] }); try { await task(); } catch (e) { @@ -49,7 +49,7 @@ describe('actions', () => { describe('setWriteBlock', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.setWriteBlock(client, 'my_index'); + const task = Actions.setWriteBlock({ client, index: 'my_index' }); try { await task(); } catch (e) { @@ -58,7 +58,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -66,7 +69,11 @@ describe('actions', () => { describe('cloneIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.cloneIndex(client, 'my_source_index', 'my_target_index'); + const task = Actions.cloneIndex({ + client, + source: 'my_source_index', + target: 'my_target_index', + }); try { await task(); } catch (e) { @@ -75,7 +82,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -95,7 +105,7 @@ describe('actions', () => { describe('openPit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.openPit(client, 'my_index'); + const task = Actions.openPit({ client, index: 'my_index' }); try { await task(); } catch (e) { @@ -107,7 +117,12 @@ describe('actions', () => { describe('readWithPit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.readWithPit(client, 'pitId', { match_all: {} }, 10_000); + const task = Actions.readWithPit({ + client, + pitId: 'pitId', + query: { match_all: {} }, + batchSize: 10_000, + }); try { await task(); } catch (e) { @@ -119,7 +134,7 @@ describe('actions', () => { describe('closePit', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.closePit(client, 'pitId'); + const task = Actions.closePit({ client, pitId: 'pitId' }); try { await task(); } catch (e) { @@ -131,14 +146,14 @@ describe('actions', () => { describe('reindex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.reindex( + const task = Actions.reindex({ client, - 'my_source_index', - 'my_target_index', - Option.none, - false, - {} - ); + sourceIndex: 'my_source_index', + targetIndex: 'my_target_index', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: {}, + }); try { await task(); } catch (e) { @@ -150,7 +165,7 @@ describe('actions', () => { describe('waitForReindexTask', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.waitForReindexTask(client, 'my task id', '60s'); + const task = Actions.waitForReindexTask({ client, taskId: 'my task id', timeout: '60s' }); try { await task(); } catch (e) { @@ -160,7 +175,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -168,7 +186,11 @@ describe('actions', () => { describe('waitForPickupUpdatedMappingsTask', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.waitForPickupUpdatedMappingsTask(client, 'my task id', '60s'); + const task = Actions.waitForPickupUpdatedMappingsTask({ + client, + taskId: 'my task id', + timeout: '60s', + }); try { await task(); } catch (e) { @@ -178,7 +200,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -186,7 +211,7 @@ describe('actions', () => { describe('updateAliases', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.updateAliases(client, []); + const task = Actions.updateAliases({ client, aliasActions: [] }); try { await task(); } catch (e) { @@ -196,7 +221,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -204,7 +232,11 @@ describe('actions', () => { describe('createIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.createIndex(client, 'new_index', { properties: {} }); + const task = Actions.createIndex({ + client, + indexName: 'new_index', + mappings: { properties: {} }, + }); try { await task(); } catch (e) { @@ -214,7 +246,10 @@ describe('actions', () => { expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(retryableError); }); it('re-throws non retry-able errors', async () => { - const task = Actions.setWriteBlock(clientWithNonRetryableError, 'my_index'); + const task = Actions.setWriteBlock({ + client: clientWithNonRetryableError, + index: 'my_index', + }); await task(); expect(catchRetryableEsClientErrors).toHaveBeenCalledWith(nonRetryableError); }); @@ -222,7 +257,11 @@ describe('actions', () => { describe('updateAndPickupMappings', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.updateAndPickupMappings(client, 'new_index', { properties: {} }); + const task = Actions.updateAndPickupMappings({ + client, + index: 'new_index', + mappings: { properties: {} }, + }); try { await task(); } catch (e) { @@ -276,7 +315,12 @@ describe('actions', () => { describe('bulkOverwriteTransformedDocuments', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.bulkOverwriteTransformedDocuments(client, 'new_index', [], 'wait_for'); + const task = Actions.bulkOverwriteTransformedDocuments({ + client, + index: 'new_index', + transformedDocs: [], + refresh: 'wait_for', + }); try { await task(); } catch (e) { @@ -289,7 +333,7 @@ describe('actions', () => { describe('refreshIndex', () => { it('calls catchRetryableEsClientErrors when the promise rejects', async () => { - const task = Actions.refreshIndex(client, 'target_index'); + const task = Actions.refreshIndex({ client, targetIndex: 'target_index' }); try { await task(); } catch (e) { diff --git a/src/core/server/saved_objects/migrationsv2/actions/index.ts b/src/core/server/saved_objects/migrationsv2/actions/index.ts index c2e0476960c3b..905d64947298e 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/index.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/index.ts @@ -68,20 +68,26 @@ export type FetchIndexResponse = Record< { aliases: Record; mappings: IndexMapping; settings: unknown } >; +/** @internal */ +export interface FetchIndicesParams { + client: ElasticsearchClient; + indices: string[]; +} + /** * Fetches information about the given indices including aliases, mappings and * settings. */ -export const fetchIndices = ( - client: ElasticsearchClient, - indicesToFetch: string[] -): TaskEither.TaskEither => +export const fetchIndices = ({ + client, + indices, +}: FetchIndicesParams): TaskEither.TaskEither => // @ts-expect-error @elastic/elasticsearch IndexState.alias and IndexState.mappings should be required () => { return client.indices .get( { - index: indicesToFetch, + index: indices, ignore_unavailable: true, // Don't return an error for missing indices. Note this *will* include closed indices, the docs are misleading https://github.com/elastic/elasticsearch/issues/63607 }, { ignore: [404], maxRetries: 0 } @@ -96,6 +102,12 @@ export interface IndexNotFound { type: 'index_not_found_exception'; index: string; } + +/** @internal */ +export interface SetWriteBlockParams { + client: ElasticsearchClient; + index: string; +} /** * Sets a write block in place for the given index. If the response includes * `acknowledged: true` all in-progress writes have drained and no further @@ -105,10 +117,10 @@ export interface IndexNotFound { * include `shards_acknowledged: true` but once the block is in place, * subsequent calls return `shards_acknowledged: false` */ -export const setWriteBlock = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither< +export const setWriteBlock = ({ + client, + index, +}: SetWriteBlockParams): TaskEither.TaskEither< IndexNotFound | RetryableEsClientError, 'set_write_block_succeeded' > => () => { @@ -145,13 +157,21 @@ export const setWriteBlock = ( ); }; +/** @internal */ +export interface RemoveWriteBlockParams { + client: ElasticsearchClient; + index: string; +} /** * Removes a write block from an index */ -export const removeWriteBlock = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither => () => { +export const removeWriteBlock = ({ + client, + index, +}: RemoveWriteBlockParams): TaskEither.TaskEither< + RetryableEsClientError, + 'remove_write_block_succeeded' +> => () => { return client.indices .putSettings<{ acknowledged: boolean; @@ -182,6 +202,12 @@ export const removeWriteBlock = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface WaitForIndexStatusYellowParams { + client: ElasticsearchClient; + index: string; + timeout?: string; +} /** * A yellow index status means the index's primary shard is allocated and the * index is ready for searching/indexing documents, but ES wasn't able to @@ -193,11 +219,11 @@ export const removeWriteBlock = ( * yellow at any point in the future. So ultimately data-redundancy is up to * users to maintain. */ -export const waitForIndexStatusYellow = ( - client: ElasticsearchClient, - index: string, - timeout = DEFAULT_TIMEOUT -): TaskEither.TaskEither => () => { +export const waitForIndexStatusYellow = ({ + client, + index, + timeout = DEFAULT_TIMEOUT, +}: WaitForIndexStatusYellowParams): TaskEither.TaskEither => () => { return client.cluster .health({ index, wait_for_status: 'yellow', timeout }) .then(() => { @@ -208,6 +234,14 @@ export const waitForIndexStatusYellow = ( export type CloneIndexResponse = AcknowledgeResponse; +/** @internal */ +export interface CloneIndexParams { + client: ElasticsearchClient; + source: string; + target: string; + /** only used for testing */ + timeout?: string; +} /** * Makes a clone of the source index into the target. * @@ -218,13 +252,15 @@ export type CloneIndexResponse = AcknowledgeResponse; * - the first call will wait up to 120s for the cluster state and all shards * to be updated. */ -export const cloneIndex = ( - client: ElasticsearchClient, - source: string, - target: string, - /** only used for testing */ - timeout = DEFAULT_TIMEOUT -): TaskEither.TaskEither => { +export const cloneIndex = ({ + client, + source, + target, + timeout = DEFAULT_TIMEOUT, +}: CloneIndexParams): TaskEither.TaskEither< + RetryableEsClientError | IndexNotFound, + CloneIndexResponse +> => { const cloneTask: TaskEither.TaskEither< RetryableEsClientError | IndexNotFound, AcknowledgeResponse @@ -302,7 +338,7 @@ export const cloneIndex = ( } else { // Otherwise, wait until the target index has a 'green' status. return pipe( - waitForIndexStatusYellow(client, target, timeout), + waitForIndexStatusYellow({ client, index: target, timeout }), TaskEither.map((value) => { /** When the index status is 'green' we know that all shards were started */ return { acknowledged: true, shardsAcknowledged: true }; @@ -352,16 +388,22 @@ const catchWaitForTaskCompletionTimeout = ( } }; +/** @internal */ +export interface WaitForTaskParams { + client: ElasticsearchClient; + taskId: string; + timeout: string; +} /** * Blocks for up to 60s or until a task completes. * * TODO: delete completed tasks */ -const waitForTask = ( - client: ElasticsearchClient, - taskId: string, - timeout: string -): TaskEither.TaskEither< +const waitForTask = ({ + client, + taskId, + timeout, +}: WaitForTaskParams): TaskEither.TaskEither< RetryableEsClientError | WaitForTaskCompletionTimeout, WaitForTaskResponse > => () => { @@ -433,16 +475,21 @@ export interface OpenPitResponse { pitId: string; } +/** @internal */ +export interface OpenPitParams { + client: ElasticsearchClient; + index: string; +} // how long ES should keep PIT alive const pitKeepAlive = '10m'; /* * Creates a lightweight view of data when the request has been initiated. * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html * */ -export const openPit = ( - client: ElasticsearchClient, - index: string -): TaskEither.TaskEither => () => { +export const openPit = ({ + client, + index, +}: OpenPitParams): TaskEither.TaskEither => () => { return client .openPointInTime({ index, @@ -459,17 +506,28 @@ export interface ReadWithPit { readonly totalHits: number | undefined; } +/** @internal */ + +export interface ReadWithPitParams { + client: ElasticsearchClient; + pitId: string; + query: estypes.QueryContainer; + batchSize: number; + searchAfter?: number[]; + seqNoPrimaryTerm?: boolean; +} + /* * Requests documents from the index using PIT mechanism. * */ -export const readWithPit = ( - client: ElasticsearchClient, - pitId: string, - query: estypes.QueryContainer, - batchSize: number, - searchAfter?: number[], - seqNoPrimaryTerm?: boolean -): TaskEither.TaskEither => () => { +export const readWithPit = ({ + client, + pitId, + query, + batchSize, + searchAfter, + seqNoPrimaryTerm, +}: ReadWithPitParams): TaskEither.TaskEither => () => { return client .search({ seq_no_primary_term: seqNoPrimaryTerm, @@ -516,14 +574,19 @@ export const readWithPit = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface ClosePitParams { + client: ElasticsearchClient; + pitId: string; +} /* * Closes PIT. * See https://www.elastic.co/guide/en/elasticsearch/reference/current/point-in-time-api.html * */ -export const closePit = ( - client: ElasticsearchClient, - pitId: string -): TaskEither.TaskEither => () => { +export const closePit = ({ + client, + pitId, +}: ClosePitParams): TaskEither.TaskEither => () => { return client .closePointInTime({ body: { id: pitId }, @@ -537,27 +600,42 @@ export const closePit = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface TransformDocsParams { + transformRawDocs: TransformRawDocs; + outdatedDocuments: SavedObjectsRawDoc[]; +} /* * Transform outdated docs * */ -export const transformDocs = ( - transformRawDocs: TransformRawDocs, - outdatedDocuments: SavedObjectsRawDoc[] -): TaskEither.TaskEither => - transformRawDocs(outdatedDocuments); +export const transformDocs = ({ + transformRawDocs, + outdatedDocuments, +}: TransformDocsParams): TaskEither.TaskEither< + DocumentsTransformFailed, + DocumentsTransformSuccess +> => transformRawDocs(outdatedDocuments); /** @internal */ export interface ReindexResponse { taskId: string; } +/** @internal */ +export interface RefreshIndexParams { + client: ElasticsearchClient; + targetIndex: string; +} /** * Wait for Elasticsearch to reindex all the changes. */ -export const refreshIndex = ( - client: ElasticsearchClient, - targetIndex: string -): TaskEither.TaskEither => () => { +export const refreshIndex = ({ + client, + targetIndex, +}: RefreshIndexParams): TaskEither.TaskEither< + RetryableEsClientError, + { refreshed: boolean } +> => () => { return client.indices .refresh({ index: targetIndex, @@ -567,6 +645,19 @@ export const refreshIndex = ( }) .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface ReindexParams { + client: ElasticsearchClient; + sourceIndex: string; + targetIndex: string; + reindexScript: Option.Option; + requireAlias: boolean; + /* When reindexing we use a source query to exclude saved objects types which + * are no longer used. These saved objects will still be kept in the outdated + * index for backup purposes, but won't be available in the upgraded index. + */ + unusedTypesQuery: estypes.QueryContainer; +} /** * Reindex documents from the `sourceIndex` into the `targetIndex`. Returns a * task ID which can be tracked for progress. @@ -575,18 +666,14 @@ export const refreshIndex = ( * this in parallel. By using `op_type: 'create', conflicts: 'proceed'` there * will be only one write per reindexed document. */ -export const reindex = ( - client: ElasticsearchClient, - sourceIndex: string, - targetIndex: string, - reindexScript: Option.Option, - requireAlias: boolean, - /* When reindexing we use a source query to exclude saved objects types which - * are no longer used. These saved objects will still be kept in the outdated - * index for backup purposes, but won't be available in the upgraded index. - */ - unusedTypesQuery: estypes.QueryContainer -): TaskEither.TaskEither => () => { +export const reindex = ({ + client, + sourceIndex, + targetIndex, + reindexScript, + requireAlias, + unusedTypesQuery, +}: ReindexParams): TaskEither.TaskEither => () => { return client .reindex({ // Require targetIndex to be an alias. Prevents a new index from being @@ -688,11 +775,18 @@ export const waitForReindexTask = flow( ) ); -export const verifyReindex = ( - client: ElasticsearchClient, - sourceIndex: string, - targetIndex: string -): TaskEither.TaskEither< +/** @internal */ +export interface VerifyReindexParams { + client: ElasticsearchClient; + sourceIndex: string; + targetIndex: string; +} + +export const verifyReindex = ({ + client, + sourceIndex, + targetIndex, +}: VerifyReindexParams): TaskEither.TaskEither< RetryableEsClientError | { type: 'verify_reindex_failed' }, 'verify_reindex_succeeded' > => () => { @@ -762,13 +856,18 @@ export type AliasAction = | { remove: { index: string; alias: string; must_exist: boolean } } | { add: { index: string; alias: string } }; +/** @internal */ +export interface UpdateAliasesParams { + client: ElasticsearchClient; + aliasActions: AliasAction[]; +} /** * Calls the Update index alias API `_alias` with the provided alias actions. */ -export const updateAliases = ( - client: ElasticsearchClient, - aliasActions: AliasAction[] -): TaskEither.TaskEither< +export const updateAliases = ({ + client, + aliasActions, +}: UpdateAliasesParams): TaskEither.TaskEither< IndexNotFound | AliasNotFound | RemoveIndexNotAConcreteIndex | RetryableEsClientError, 'update_aliases_succeeded' > => () => { @@ -836,6 +935,14 @@ function aliasArrayToRecord(aliases: string[]): Record { } return result; } + +/** @internal */ +export interface CreateIndexParams { + client: ElasticsearchClient; + indexName: string; + mappings: IndexMapping; + aliases?: string[]; +} /** * Creates an index with the given mappings * @@ -846,12 +953,12 @@ function aliasArrayToRecord(aliases: string[]): Record { * - the first call will wait up to 120s for the cluster state and all shards * to be updated. */ -export const createIndex = ( - client: ElasticsearchClient, - indexName: string, - mappings: IndexMapping, - aliases: string[] = [] -): TaskEither.TaskEither => { +export const createIndex = ({ + client, + indexName, + mappings, + aliases = [], +}: CreateIndexParams): TaskEither.TaskEither => { const createIndexTask: TaskEither.TaskEither< RetryableEsClientError, AcknowledgeResponse @@ -930,7 +1037,7 @@ export const createIndex = ( } else { // Otherwise, wait until the target index has a 'yellow' status. return pipe( - waitForIndexStatusYellow(client, indexName, DEFAULT_TIMEOUT), + waitForIndexStatusYellow({ client, index: indexName, timeout: DEFAULT_TIMEOUT }), TaskEither.map(() => { /** When the index status is 'yellow' we know that all shards were started */ return 'create_index_succeeded'; @@ -946,15 +1053,24 @@ export interface UpdateAndPickupMappingsResponse { taskId: string; } +/** @internal */ +export interface UpdateAndPickupMappingsParams { + client: ElasticsearchClient; + index: string; + mappings: IndexMapping; +} /** * Updates an index's mappings and runs an pickupUpdatedMappings task so that the mapping * changes are "picked up". Returns a taskId to track progress. */ -export const updateAndPickupMappings = ( - client: ElasticsearchClient, - index: string, - mappings: IndexMapping -): TaskEither.TaskEither => { +export const updateAndPickupMappings = ({ + client, + index, + mappings, +}: UpdateAndPickupMappingsParams): TaskEither.TaskEither< + RetryableEsClientError, + UpdateAndPickupMappingsResponse +> => { const putMappingTask: TaskEither.TaskEither< RetryableEsClientError, 'update_mappings_succeeded' @@ -1053,16 +1169,26 @@ export const searchForOutdatedDocuments = ( .catch(catchRetryableEsClientErrors); }; +/** @internal */ +export interface BulkOverwriteTransformedDocumentsParams { + client: ElasticsearchClient; + index: string; + transformedDocs: SavedObjectsRawDoc[]; + refresh?: estypes.Refresh; +} /** * Write the up-to-date transformed documents to the index, overwriting any * documents that are still on their outdated version. */ -export const bulkOverwriteTransformedDocuments = ( - client: ElasticsearchClient, - index: string, - transformedDocs: SavedObjectsRawDoc[], - refresh: estypes.Refresh -): TaskEither.TaskEither => () => { +export const bulkOverwriteTransformedDocuments = ({ + client, + index, + transformedDocs, + refresh = false, +}: BulkOverwriteTransformedDocumentsParams): TaskEither.TaskEither< + RetryableEsClientError, + 'bulk_index_succeeded' +> => () => { return client .bulk({ // Because we only add aliases in the MARK_VERSION_INDEX_READY step we diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts index d0158a4c68f24..67a2685caf3d6 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/actions.test.ts @@ -67,9 +67,13 @@ describe('migration actions', () => { client = start.elasticsearch.client.asInternalUser; // Create test fixture data: - await createIndex(client, 'existing_index_with_docs', { - dynamic: true, - properties: {}, + await createIndex({ + client, + indexName: 'existing_index_with_docs', + mappings: { + dynamic: true, + properties: {}, + }, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -78,25 +82,30 @@ describe('migration actions', () => { { _source: { title: 'saved object 4', type: 'another_unused_type' } }, { _source: { title: 'f-agent-event 5', type: 'f_agent_event' } }, ] as unknown) as SavedObjectsRawDoc[]; - await bulkOverwriteTransformedDocuments( + await bulkOverwriteTransformedDocuments({ + client, + index: 'existing_index_with_docs', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); + + await createIndex({ client, indexName: 'existing_index_2', mappings: { properties: {} } })(); + await createIndex({ client, - 'existing_index_with_docs', - sourceDocs, - 'wait_for' - )(); - - await createIndex(client, 'existing_index_2', { properties: {} })(); - await createIndex(client, 'existing_index_with_write_block', { properties: {} })(); - await bulkOverwriteTransformedDocuments( + indexName: 'existing_index_with_write_block', + mappings: { properties: {} }, + })(); + await bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_write_block', - sourceDocs, - 'wait_for' - )(); - await setWriteBlock(client, 'existing_index_with_write_block')(); - await updateAliases(client, [ - { add: { index: 'existing_index_2', alias: 'existing_index_2_alias' } }, - ])(); + index: 'existing_index_with_write_block', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); + await setWriteBlock({ client, index: 'existing_index_with_write_block' })(); + await updateAliases({ + client, + aliasActions: [{ add: { index: 'existing_index_2', alias: 'existing_index_2_alias' } }], + })(); }); afterAll(async () => { @@ -107,7 +116,7 @@ describe('migration actions', () => { describe('fetchIndices', () => { it('resolves right empty record if no indices were found', async () => { expect.assertions(1); - const task = fetchIndices(client, ['no_such_index']); + const task = fetchIndices({ client, indices: ['no_such_index'] }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -117,10 +126,10 @@ describe('migration actions', () => { }); it('resolves right record with found indices', async () => { expect.assertions(1); - const res = (await fetchIndices(client, [ - 'no_such_index', - 'existing_index_with_docs', - ])()) as Either.Right; + const res = (await fetchIndices({ + client, + indices: ['no_such_index', 'existing_index_with_docs'], + })()) as Either.Right; expect(res.right).toEqual( expect.objectContaining({ @@ -136,11 +145,15 @@ describe('migration actions', () => { describe('setWriteBlock', () => { beforeAll(async () => { - await createIndex(client, 'new_index_without_write_block', { properties: {} })(); + await createIndex({ + client, + indexName: 'new_index_without_write_block', + mappings: { properties: {} }, + })(); }); it('resolves right when setting the write block succeeds', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'new_index_without_write_block'); + const task = setWriteBlock({ client, index: 'new_index_without_write_block' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -150,7 +163,7 @@ describe('migration actions', () => { }); it('resolves right when setting a write block on an index that already has one', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'existing_index_with_write_block'); + const task = setWriteBlock({ client, index: 'existing_index_with_write_block' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -160,7 +173,7 @@ describe('migration actions', () => { }); it('once resolved, prevents further writes to the index', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'new_index_without_write_block'); + const task = setWriteBlock({ client, index: 'new_index_without_write_block' }); await task(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -169,17 +182,17 @@ describe('migration actions', () => { { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments( + bulkOverwriteTransformedDocuments({ client, - 'new_index_without_write_block', - sourceDocs, - 'wait_for' - )() + index: 'new_index_without_write_block', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })() ).rejects.toMatchObject(expect.anything()); }); it('resolves left index_not_found_exception when the index does not exist', async () => { expect.assertions(1); - const task = setWriteBlock(client, 'no_such_index'); + const task = setWriteBlock({ client, index: 'no_such_index' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -194,13 +207,21 @@ describe('migration actions', () => { describe('removeWriteBlock', () => { beforeAll(async () => { - await createIndex(client, 'existing_index_without_write_block_2', { properties: {} })(); - await createIndex(client, 'existing_index_with_write_block_2', { properties: {} })(); - await setWriteBlock(client, 'existing_index_with_write_block_2')(); + await createIndex({ + client, + indexName: 'existing_index_without_write_block_2', + mappings: { properties: {} }, + })(); + await createIndex({ + client, + indexName: 'existing_index_with_write_block_2', + mappings: { properties: {} }, + })(); + await setWriteBlock({ client, index: 'existing_index_with_write_block_2' })(); }); it('resolves right if successful when an index already has a write block', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'existing_index_with_write_block_2'); + const task = removeWriteBlock({ client, index: 'existing_index_with_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -210,7 +231,7 @@ describe('migration actions', () => { }); it('resolves right if successful when an index does not have a write block', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'existing_index_without_write_block_2'); + const task = removeWriteBlock({ client, index: 'existing_index_without_write_block_2' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -220,7 +241,7 @@ describe('migration actions', () => { }); it('rejects if there is a non-retryable error', async () => { expect.assertions(1); - const task = removeWriteBlock(client, 'no_such_index'); + const task = removeWriteBlock({ client, index: 'no_such_index' }); await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` ); @@ -251,7 +272,10 @@ describe('migration actions', () => { ); // Start tracking the index status - const indexStatusPromise = waitForIndexStatusYellow(client, 'red_then_yellow_index')(); + const indexStatusPromise = waitForIndexStatusYellow({ + client, + index: 'red_then_yellow_index', + })(); const redStatusResponse = await client.cluster.health({ index: 'red_then_yellow_index' }); expect(redStatusResponse.body.status).toBe('red'); @@ -281,7 +305,11 @@ describe('migration actions', () => { } }); it('resolves right if cloning into a new target index', async () => { - const task = cloneIndex(client, 'existing_index_with_write_block', 'clone_target_1'); + const task = cloneIndex({ + client, + source: 'existing_index_with_write_block', + target: 'clone_target_1', + }); expect.assertions(1); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -314,11 +342,11 @@ describe('migration actions', () => { .catch((e) => {}); // Call clone even though the index already exists - const cloneIndexPromise = cloneIndex( + const cloneIndexPromise = cloneIndex({ client, - 'existing_index_with_write_block', - 'clone_red_then_yellow_index' - )(); + source: 'existing_index_with_write_block', + target: 'clone_red_then_yellow_index', + })(); let indexYellow = false; setTimeout(() => { @@ -348,7 +376,7 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if the source index does not exist', async () => { expect.assertions(1); - const task = cloneIndex(client, 'no_such_index', 'clone_target_3'); + const task = cloneIndex({ client, source: 'no_such_index', target: 'clone_target_3' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -378,12 +406,12 @@ describe('migration actions', () => { .catch((e) => {}); // Call clone even though the index already exists - const cloneIndexPromise = cloneIndex( + const cloneIndexPromise = cloneIndex({ client, - 'existing_index_with_write_block', - 'clone_red_index', - '0s' - )(); + source: 'existing_index_with_write_block', + target: 'clone_red_index', + timeout: '0s', + })(); await cloneIndexPromise.then((res) => { expect(res).toMatchInlineSnapshot(` @@ -404,15 +432,15 @@ describe('migration actions', () => { // together with waitForReindexTask describe('reindex & waitForReindexTask', () => { it('resolves right when reindex succeeds without reindex script', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -436,21 +464,21 @@ describe('migration actions', () => { `); }); it('resolves right and excludes all documents not matching the unusedTypesQuery', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_excluded_docs', - Option.none, - false, - { + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_excluded_docs', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { bool: { must_not: ['f_agent_event', 'another_unused_type'].map((type) => ({ term: { type }, })), }, - } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -473,15 +501,15 @@ describe('migration actions', () => { }); it('resolves right when reindex succeeds with reindex script', async () => { expect.assertions(2); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_2', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_2', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -506,15 +534,15 @@ describe('migration actions', () => { it('resolves right, ignores version conflicts and does not update existing docs when reindex multiple times', async () => { expect.assertions(3); // Reindex with a script - let res = (await reindex( + let res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_3', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - let task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_3', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + let task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -523,15 +551,15 @@ describe('migration actions', () => { `); // reindex without a script - res = (await reindex( + res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_3', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_3', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -559,7 +587,7 @@ describe('migration actions', () => { expect.assertions(2); // Simulate a reindex that only adds some of the documents from the // source index into the target index - await createIndex(client, 'reindex_target_4', { properties: {} })(); + await createIndex({ client, indexName: 'reindex_target_4', mappings: { properties: {} } })(); const sourceDocs = ((await searchForOutdatedDocuments(client, { batchSize: 1000, targetIndex: 'existing_index_with_docs', @@ -570,18 +598,23 @@ describe('migration actions', () => { _id, _source, })); - await bulkOverwriteTransformedDocuments(client, 'reindex_target_4', sourceDocs, 'wait_for')(); + await bulkOverwriteTransformedDocuments({ + client, + index: 'reindex_target_4', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); // Now do a real reindex - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_4', - Option.some(`ctx._source.title = ctx._source.title + '_updated'`), - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_4', + reindexScript: Option.some(`ctx._source.title = ctx._source.title + '_updated'`), + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -614,24 +647,28 @@ describe('migration actions', () => { // and should ignore this error. // Create an index with incompatible mappings - await createIndex(client, 'reindex_target_5', { - dynamic: 'strict', - properties: { - /** no title field */ + await createIndex({ + client, + indexName: 'reindex_target_5', + mappings: { + dynamic: 'strict', + properties: { + /** no title field */ + }, }, })(); const { right: { taskId: reindexTaskId }, - } = (await reindex( + } = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_5', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, reindexTaskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_5', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -651,22 +688,26 @@ describe('migration actions', () => { // and should ignore this error. // Create an index with incompatible mappings - await createIndex(client, 'reindex_target_6', { - dynamic: false, - properties: { title: { type: 'integer' } }, // integer is incompatible with string title + await createIndex({ + client, + indexName: 'reindex_target_6', + mappings: { + dynamic: false, + properties: { title: { type: 'integer' } }, // integer is incompatible with string title + }, })(); const { right: { taskId: reindexTaskId }, - } = (await reindex( + } = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_6', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - const task = waitForReindexTask(client, reindexTaskId, '10s'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_6', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + const task = waitForReindexTask({ client, taskId: reindexTaskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -679,10 +720,17 @@ describe('migration actions', () => { }); it('resolves left index_not_found_exception if source index does not exist', async () => { expect.assertions(1); - const res = (await reindex(client, 'no_such_index', 'reindex_target', Option.none, false, { - match_all: {}, + const res = (await reindex({ + client, + sourceIndex: 'no_such_index', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { + match_all: {}, + }, })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -695,16 +743,16 @@ describe('migration actions', () => { }); it('resolves left target_index_had_write_block if all failures are due to a write block', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'existing_index_with_write_block', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_with_write_block', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -717,16 +765,16 @@ describe('migration actions', () => { }); it('resolves left if requireAlias=true and the target is not an alias', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'existing_index_with_write_block', - Option.none, - true, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_with_write_block', + reindexScript: Option.none, + requireAlias: true, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '10s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -739,16 +787,16 @@ describe('migration actions', () => { `); }); it('resolves left wait_for_task_completion_timeout when the task does not finish within the timeout', async () => { - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; - const task = waitForReindexTask(client, res.right.taskId, '0s'); + const task = waitForReindexTask({ client, taskId: res.right.taskId, timeout: '0s' }); await expect(task()).resolves.toMatchObject({ _tag: 'Left', @@ -766,17 +814,21 @@ describe('migration actions', () => { describe('verifyReindex', () => { it('resolves right if source and target indices have the same amount of documents', async () => { expect.assertions(1); - const res = (await reindex( + const res = (await reindex({ client, - 'existing_index_with_docs', - 'reindex_target_7', - Option.none, - false, - { match_all: {} } - )()) as Either.Right; - await waitForReindexTask(client, res.right.taskId, '10s')(); - - const task = verifyReindex(client, 'existing_index_with_docs', 'reindex_target_7'); + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_7', + reindexScript: Option.none, + requireAlias: false, + unusedTypesQuery: { match_all: {} }, + })()) as Either.Right; + await waitForReindexTask({ client, taskId: res.right.taskId, timeout: '10s' })(); + + const task = verifyReindex({ + client, + sourceIndex: 'existing_index_with_docs', + targetIndex: 'reindex_target_7', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -786,7 +838,11 @@ describe('migration actions', () => { }); it('resolves left if source and target indices have different amount of documents', async () => { expect.assertions(1); - const task = verifyReindex(client, 'existing_index_with_docs', 'existing_index_2'); + const task = verifyReindex({ + client, + sourceIndex: 'existing_index_with_docs', + targetIndex: 'existing_index_2', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -798,19 +854,27 @@ describe('migration actions', () => { }); it('rejects if source or target index does not exist', async () => { expect.assertions(2); - let task = verifyReindex(client, 'no_such_index', 'existing_index_2'); + let task = verifyReindex({ + client, + sourceIndex: 'no_such_index', + targetIndex: 'existing_index_2', + }); await expect(task()).rejects.toMatchInlineSnapshot( `[ResponseError: index_not_found_exception]` ); - task = verifyReindex(client, 'existing_index_2', 'no_such_index'); + task = verifyReindex({ + client, + sourceIndex: 'existing_index_2', + targetIndex: 'no_such_index', + }); await expect(task()).rejects.toThrow('index_not_found_exception'); }); }); describe('openPit', () => { it('opens PointInTime for an index', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; expect(pitResponse.right.pitId).toEqual(expect.any(String)); @@ -824,52 +888,52 @@ describe('migration actions', () => { await expect(searchResponse.body.hits.hits.length).toBeGreaterThan(0); }); it('rejects if index does not exist', async () => { - const openPitTask = openPit(client, 'no_such_index'); + const openPitTask = openPit({ client, index: 'no_such_index' }); await expect(openPitTask()).rejects.toThrow('index_not_found_exception'); }); }); describe('readWithPit', () => { it('requests documents from an index using given PIT', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { match_all: {} }, - 1000, - undefined - ); + pitId: pitResponse.right.pitId, + query: { match_all: {} }, + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(5); }); it('requests the batchSize of documents from an index', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { match_all: {} }, - 3, - undefined - ); + pitId: pitResponse.right.pitId, + query: { match_all: {} }, + batchSize: 3, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; await expect(docsResponse.right.outdatedDocuments.length).toBe(3); }); it('it excludes documents not matching the provided "query"', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { bool: { must_not: [ { @@ -885,9 +949,9 @@ describe('migration actions', () => { ], }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -902,18 +966,18 @@ describe('migration actions', () => { }); it('only returns documents that match the provided "query"', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -928,19 +992,19 @@ describe('migration actions', () => { }); it('returns docs with _seq_no and _primary_term when specified', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined, - true - ); + batchSize: 1000, + searchAfter: undefined, + seqNoPrimaryTerm: true, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -955,18 +1019,18 @@ describe('migration actions', () => { }); it('does not return docs with _seq_no and _primary_term if not specified', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - pitResponse.right.pitId, - { + pitId: pitResponse.right.pitId, + query: { match: { title: { query: 'doc' } }, }, - 1000, - undefined - ); + batchSize: 1000, + searchAfter: undefined, + }); const docsResponse = (await readWithPitTask()) as Either.Right; @@ -981,24 +1045,24 @@ describe('migration actions', () => { }); it('rejects if PIT does not exist', async () => { - const readWithPitTask = readWithPit( + const readWithPitTask = readWithPit({ client, - 'no_such_pit', - { match_all: {} }, - 1000, - undefined - ); + pitId: 'no_such_pit', + query: { match_all: {} }, + batchSize: 1000, + searchAfter: undefined, + }); await expect(readWithPitTask()).rejects.toThrow('illegal_argument_exception'); }); }); describe('closePit', () => { it('closes PointInTime', async () => { - const openPitTask = openPit(client, 'existing_index_with_docs'); + const openPitTask = openPit({ client, index: 'existing_index_with_docs' }); const pitResponse = (await openPitTask()) as Either.Right; const pitId = pitResponse.right.pitId; - await closePit(client, pitId)(); + await closePit({ client, pitId })(); const searchTask = client.search({ body: { @@ -1010,7 +1074,7 @@ describe('migration actions', () => { }); it('rejects if PIT does not exist', async () => { - const closePitTask = closePit(client, 'no_such_pit'); + const closePitTask = closePit({ client, pitId: 'no_such_pit' }); await expect(closePitTask()).rejects.toThrow('illegal_argument_exception'); }); }); @@ -1034,7 +1098,10 @@ describe('migration actions', () => { return Either.right({ processedDocs }); }; } - const transformTask = transformDocs(innerTransformRawDocs, originalDocs); + const transformTask = transformDocs({ + transformRawDocs: innerTransformRawDocs, + outdatedDocuments: originalDocs, + }); const resultsWithProcessDocs = ((await transformTask()) as Either.Right) .right.processedDocs; @@ -1051,7 +1118,11 @@ describe('migration actions', () => { 'existing_index_with_write_block' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); // We can't do a snapshot match because the response includes an index // id which ES assigns dynamically @@ -1065,7 +1136,11 @@ describe('migration actions', () => { 'no_such_index' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); await expect(task()).rejects.toMatchInlineSnapshot(` [Error: pickupUpdatedMappings task failed with the following error: @@ -1078,7 +1153,11 @@ describe('migration actions', () => { 'existing_index_with_docs' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '0s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '0s', + }); await expect(task()).resolves.toMatchObject({ _tag: 'Left', @@ -1097,7 +1176,11 @@ describe('migration actions', () => { 'existing_index_with_docs' )()) as Either.Right; - const task = waitForPickupUpdatedMappingsTask(client, res.right.taskId, '10s'); + const task = waitForPickupUpdatedMappingsTask({ + client, + taskId: res.right.taskId, + timeout: '10s', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -1111,9 +1194,13 @@ describe('migration actions', () => { describe('updateAndPickupMappings', () => { it('resolves right when mappings were updated and picked up', async () => { // Create an index without any mappings and insert documents into it - await createIndex(client, 'existing_index_without_mappings', { - dynamic: false, - properties: {}, + await createIndex({ + client, + indexName: 'existing_index_without_mappings', + mappings: { + dynamic: false, + properties: {}, + }, })(); const sourceDocs = ([ { _source: { title: 'doc 1' } }, @@ -1121,12 +1208,12 @@ describe('migration actions', () => { { _source: { title: 'doc 3' } }, { _source: { title: 'doc 4' } }, ] as unknown) as SavedObjectsRawDoc[]; - await bulkOverwriteTransformedDocuments( + await bulkOverwriteTransformedDocuments({ client, - 'existing_index_without_mappings', - sourceDocs, - 'wait_for' - )(); + index: 'existing_index_without_mappings', + transformedDocs: sourceDocs, + refresh: 'wait_for', + })(); // Assert that we can't search over the unmapped fields of the document const originalSearchResults = ((await searchForOutdatedDocuments(client, { @@ -1139,14 +1226,18 @@ describe('migration actions', () => { expect(originalSearchResults.length).toBe(0); // Update and pickup mappings so that the title field is searchable - const res = await updateAndPickupMappings(client, 'existing_index_without_mappings', { - properties: { - title: { type: 'text' }, + const res = await updateAndPickupMappings({ + client, + index: 'existing_index_without_mappings', + mappings: { + properties: { + title: { type: 'text' }, + }, }, })(); expect(Either.isRight(res)).toBe(true); const taskId = (res as Either.Right).right.taskId; - await waitForPickupUpdatedMappingsTask(client, taskId, '60s')(); + await waitForPickupUpdatedMappingsTask({ client, taskId, timeout: '60s' })(); // Repeat the search expecting to be able to find the existing documents const pickedUpSearchResults = ((await searchForOutdatedDocuments(client, { @@ -1163,15 +1254,18 @@ describe('migration actions', () => { describe('updateAliases', () => { describe('remove', () => { it('resolves left index_not_found_exception when the index does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'no_such_index', - must_exist: false, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'no_such_index', + must_exist: false, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1184,15 +1278,18 @@ describe('migration actions', () => { }); describe('with must_exist=false', () => { it('resolves left alias_not_found_exception when alias does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'existing_index_with_docs', - must_exist: false, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'existing_index_with_docs', + must_exist: false, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1205,15 +1302,18 @@ describe('migration actions', () => { }); describe('with must_exist=true', () => { it('resolves left alias_not_found_exception when alias does not exist on specified index', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'existing_index_2_alias', - index: 'existing_index_with_docs', - must_exist: true, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'existing_index_2_alias', + index: 'existing_index_with_docs', + must_exist: true, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1224,15 +1324,18 @@ describe('migration actions', () => { `); }); it('resolves left alias_not_found_exception when alias does not exist', async () => { - const task = updateAliases(client, [ - { - remove: { - alias: 'no_such_alias', - index: 'existing_index_with_docs', - must_exist: true, + const task = updateAliases({ + client, + aliasActions: [ + { + remove: { + alias: 'no_such_alias', + index: 'existing_index_with_docs', + must_exist: true, + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1246,13 +1349,16 @@ describe('migration actions', () => { }); describe('remove_index', () => { it('left index_not_found_exception if index does not exist', async () => { - const task = updateAliases(client, [ - { - remove_index: { - index: 'no_such_index', + const task = updateAliases({ + client, + aliasActions: [ + { + remove_index: { + index: 'no_such_index', + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1264,13 +1370,16 @@ describe('migration actions', () => { `); }); it('left remove_index_not_a_concrete_index when remove_index targets an alias', async () => { - const task = updateAliases(client, [ - { - remove_index: { - index: 'existing_index_2_alias', + const task = updateAliases({ + client, + aliasActions: [ + { + remove_index: { + index: 'existing_index_2_alias', + }, }, - }, - ]); + ], + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Left", @@ -1312,7 +1421,11 @@ describe('migration actions', () => { }); // Call createIndex even though the index already exists - const createIndexPromise = createIndex(client, 'red_then_yellow_index', undefined as any)(); + const createIndexPromise = createIndex({ + client, + indexName: 'red_then_yellow_index', + mappings: undefined as any, + })(); let indexYellow = false; setTimeout(() => { @@ -1341,7 +1454,7 @@ describe('migration actions', () => { // Creating an index with the same name as an existing alias to induce // failure await expect( - createIndex(client, 'existing_index_2_alias', undefined as any)() + createIndex({ client, indexName: 'existing_index_2_alias', mappings: undefined as any })() ).rejects.toMatchInlineSnapshot(`[ResponseError: invalid_index_name_exception]`); }); }); @@ -1353,12 +1466,12 @@ describe('migration actions', () => { { _source: { title: 'doc 6' } }, { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; - const task = bulkOverwriteTransformedDocuments( + const task = bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_docs', - newDocs, - 'wait_for' - ); + index: 'existing_index_with_docs', + transformedDocs: newDocs, + refresh: 'wait_for', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { @@ -1374,12 +1487,15 @@ describe('migration actions', () => { outdatedDocumentsQuery: undefined, })()) as Either.Right).right.outdatedDocuments; - const task = bulkOverwriteTransformedDocuments( + const task = bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_docs', - [...existingDocs, ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc], - 'wait_for' - ); + index: 'existing_index_with_docs', + transformedDocs: [ + ...existingDocs, + ({ _source: { title: 'doc 8' } } as unknown) as SavedObjectsRawDoc, + ], + refresh: 'wait_for', + }); await expect(task()).resolves.toMatchInlineSnapshot(` Object { "_tag": "Right", @@ -1394,12 +1510,12 @@ describe('migration actions', () => { { _source: { title: 'doc 7' } }, ] as unknown) as SavedObjectsRawDoc[]; await expect( - bulkOverwriteTransformedDocuments( + bulkOverwriteTransformedDocuments({ client, - 'existing_index_with_write_block', - newDocs, - 'wait_for' - )() + index: 'existing_index_with_write_block', + transformedDocs: newDocs, + refresh: 'wait_for', + })() ).rejects.toMatchObject(expect.anything()); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts index 1881f9a712c29..e9cb33c0aa54a 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_machine_cleanup.ts @@ -19,7 +19,7 @@ export async function cleanup( if (!state) return; if ('sourceIndexPitId' in state) { try { - await Actions.closePit(client, state.sourceIndexPitId)(); + await Actions.closePit({ client, pitId: state.sourceIndexPitId })(); } catch (e) { executionLog.push({ type: 'cleanup', diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 07ebf80271d48..3c3e3c46a8d68 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -58,38 +58,46 @@ export type ResponseType = UnwrapPromise< export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: TransformRawDocs) => { return { INIT: (state: InitState) => - Actions.fetchIndices(client, [state.currentAlias, state.versionAlias]), + Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }), WAIT_FOR_YELLOW_SOURCE: (state: WaitForYellowSourceState) => - Actions.waitForIndexStatusYellow(client, state.sourceIndex.value), + Actions.waitForIndexStatusYellow({ client, index: state.sourceIndex.value }), SET_SOURCE_WRITE_BLOCK: (state: SetSourceWriteBlockState) => - Actions.setWriteBlock(client, state.sourceIndex.value), + Actions.setWriteBlock({ client, index: state.sourceIndex.value }), CREATE_NEW_TARGET: (state: CreateNewTargetState) => - Actions.createIndex(client, state.targetIndex, state.targetIndexMappings), + Actions.createIndex({ + client, + indexName: state.targetIndex, + mappings: state.targetIndexMappings, + }), CREATE_REINDEX_TEMP: (state: CreateReindexTempState) => - Actions.createIndex(client, state.tempIndex, state.tempIndexMappings), + Actions.createIndex({ + client, + indexName: state.tempIndex, + mappings: state.tempIndexMappings, + }), REINDEX_SOURCE_TO_TEMP_OPEN_PIT: (state: ReindexSourceToTempOpenPit) => - Actions.openPit(client, state.sourceIndex.value), + Actions.openPit({ client, index: state.sourceIndex.value }), REINDEX_SOURCE_TO_TEMP_READ: (state: ReindexSourceToTempRead) => - Actions.readWithPit( + Actions.readWithPit({ client, - state.sourceIndexPitId, + pitId: state.sourceIndexPitId, /* When reading we use a source query to exclude saved objects types which * are no longer used. These saved objects will still be kept in the outdated * index for backup purposes, but won't be available in the upgraded index. */ - state.unusedTypesQuery, - state.batchSize, - state.lastHitSortValue - ), + query: state.unusedTypesQuery, + batchSize: state.batchSize, + searchAfter: state.lastHitSortValue, + }), REINDEX_SOURCE_TO_TEMP_CLOSE_PIT: (state: ReindexSourceToTempClosePit) => - Actions.closePit(client, state.sourceIndexPitId), + Actions.closePit({ client, pitId: state.sourceIndexPitId }), REINDEX_SOURCE_TO_TEMP_INDEX: (state: ReindexSourceToTempIndex) => - Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), REINDEX_SOURCE_TO_TEMP_INDEX_BULK: (state: ReindexSourceToTempIndexBulk) => - Actions.bulkOverwriteTransformedDocuments( + Actions.bulkOverwriteTransformedDocuments({ client, - state.tempIndex, - state.transformedDocs, + index: state.tempIndex, + transformedDocs: state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -97,39 +105,48 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * before we reach out to the OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT step. * Right now, it's performed during REFRESH_TARGET step. */ - false - ), + refresh: false, + }), SET_TEMP_WRITE_BLOCK: (state: SetTempWriteBlock) => - Actions.setWriteBlock(client, state.tempIndex), + Actions.setWriteBlock({ client, index: state.tempIndex }), CLONE_TEMP_TO_TARGET: (state: CloneTempToSource) => - Actions.cloneIndex(client, state.tempIndex, state.targetIndex), - REFRESH_TARGET: (state: RefreshTarget) => Actions.refreshIndex(client, state.targetIndex), + Actions.cloneIndex({ client, source: state.tempIndex, target: state.targetIndex }), + REFRESH_TARGET: (state: RefreshTarget) => + Actions.refreshIndex({ client, targetIndex: state.targetIndex }), UPDATE_TARGET_MAPPINGS: (state: UpdateTargetMappingsState) => - Actions.updateAndPickupMappings(client, state.targetIndex, state.targetIndexMappings), + Actions.updateAndPickupMappings({ + client, + index: state.targetIndex, + mappings: state.targetIndexMappings, + }), UPDATE_TARGET_MAPPINGS_WAIT_FOR_TASK: (state: UpdateTargetMappingsWaitForTaskState) => - Actions.waitForPickupUpdatedMappingsTask(client, state.updateTargetMappingsTaskId, '60s'), + Actions.waitForPickupUpdatedMappingsTask({ + client, + taskId: state.updateTargetMappingsTaskId, + timeout: '60s', + }), OUTDATED_DOCUMENTS_SEARCH_OPEN_PIT: (state: OutdatedDocumentsSearchOpenPit) => - Actions.openPit(client, state.targetIndex), + Actions.openPit({ client, index: state.targetIndex }), OUTDATED_DOCUMENTS_SEARCH_READ: (state: OutdatedDocumentsSearchRead) => - Actions.readWithPit( + Actions.readWithPit({ client, - state.pitId, + pitId: state.pitId, // search for outdated documents only - state.outdatedDocumentsQuery, - state.batchSize, - state.lastHitSortValue - ), + query: state.outdatedDocumentsQuery, + batchSize: state.batchSize, + searchAfter: state.lastHitSortValue, + }), OUTDATED_DOCUMENTS_SEARCH_CLOSE_PIT: (state: OutdatedDocumentsSearchClosePit) => - Actions.closePit(client, state.pitId), + Actions.closePit({ client, pitId: state.pitId }), OUTDATED_DOCUMENTS_REFRESH: (state: OutdatedDocumentsRefresh) => - Actions.refreshIndex(client, state.targetIndex), + Actions.refreshIndex({ client, targetIndex: state.targetIndex }), OUTDATED_DOCUMENTS_TRANSFORM: (state: OutdatedDocumentsTransform) => - Actions.transformDocs(transformRawDocs, state.outdatedDocuments), + Actions.transformDocs({ transformRawDocs, outdatedDocuments: state.outdatedDocuments }), TRANSFORMED_DOCUMENTS_BULK_INDEX: (state: TransformedDocumentsBulkIndex) => - Actions.bulkOverwriteTransformedDocuments( + Actions.bulkOverwriteTransformedDocuments({ client, - state.targetIndex, - state.transformedDocs, + index: state.targetIndex, + transformedDocs: state.transformedDocs, /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -137,29 +154,32 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra * before we reach out to the MARK_VERSION_INDEX_READY step. * Right now, it's performed during OUTDATED_DOCUMENTS_REFRESH step. */ - false - ), + }), MARK_VERSION_INDEX_READY: (state: MarkVersionIndexReady) => - Actions.updateAliases(client, state.versionIndexReadyActions.value), + Actions.updateAliases({ client, aliasActions: state.versionIndexReadyActions.value }), MARK_VERSION_INDEX_READY_CONFLICT: (state: MarkVersionIndexReadyConflict) => - Actions.fetchIndices(client, [state.currentAlias, state.versionAlias]), + Actions.fetchIndices({ client, indices: [state.currentAlias, state.versionAlias] }), LEGACY_SET_WRITE_BLOCK: (state: LegacySetWriteBlockState) => - Actions.setWriteBlock(client, state.legacyIndex), + Actions.setWriteBlock({ client, index: state.legacyIndex }), LEGACY_CREATE_REINDEX_TARGET: (state: LegacyCreateReindexTargetState) => - Actions.createIndex(client, state.sourceIndex.value, state.legacyReindexTargetMappings), + Actions.createIndex({ + client, + indexName: state.sourceIndex.value, + mappings: state.legacyReindexTargetMappings, + }), LEGACY_REINDEX: (state: LegacyReindexState) => - Actions.reindex( + Actions.reindex({ client, - state.legacyIndex, - state.sourceIndex.value, - state.preMigrationScript, - false, - state.unusedTypesQuery - ), + sourceIndex: state.legacyIndex, + targetIndex: state.sourceIndex.value, + reindexScript: state.preMigrationScript, + requireAlias: false, + unusedTypesQuery: state.unusedTypesQuery, + }), LEGACY_REINDEX_WAIT_FOR_TASK: (state: LegacyReindexWaitForTaskState) => - Actions.waitForReindexTask(client, state.legacyReindexTaskId, '60s'), + Actions.waitForReindexTask({ client, taskId: state.legacyReindexTaskId, timeout: '60s' }), LEGACY_DELETE: (state: LegacyDeleteState) => - Actions.updateAliases(client, state.legacyPreMigrationDoneActions), + Actions.updateAliases({ client, aliasActions: state.legacyPreMigrationDoneActions }), }; }; From a622cd5450ce8c812a2f59ba6dfa45d00ac91558 Mon Sep 17 00:00:00 2001 From: Peter Pisljar Date: Tue, 1 Jun 2021 06:05:39 +0200 Subject: [PATCH 10/46] adding expressions dev_docs tutorial (#101004) --- dev_docs/tutorials/expressions.mdx | 129 +++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 dev_docs/tutorials/expressions.mdx diff --git a/dev_docs/tutorials/expressions.mdx b/dev_docs/tutorials/expressions.mdx new file mode 100644 index 0000000000000..f0fc1dc595cfa --- /dev/null +++ b/dev_docs/tutorials/expressions.mdx @@ -0,0 +1,129 @@ +--- +id: kibDevTutorialExpressions +slug: /kibana-dev-docs/tutorials/expressions +title: Kibana Expressions Service +summary: Kibana Expressions Service +date: 2021-06-01 +tags: ['kibana', 'onboarding', 'dev', 'architecture'] +--- + +## Expressions service + +Expression service exposes a registry of reusable functions primary used for fetching and transposing data and a registry of renderer functions that can render data into a DOM element. +Adding functions is easy and so is reusing them. An expression is a chain of functions with provided arguments, which given a single input translates to a single output. +Each expression is representable by a human friendly string which a user can type. + +### creating expressions + +Here is a very simple expression string: + + essql 'select column1, column2 from myindex' | mapColumn name=column3 fn='{ column1 + 3 }' | table + + +It consists of 3 functions: + + - essql which runs given sql query against elasticsearch and returns the results + - `mapColumn`, which computes a new column from existing ones; + - `table`, which prepares the data for rendering in a tabular format. + +The same expression could also be constructed in the code: + +```ts +import { buildExpression, buildExpressionFunction } from 'src/plugins/expressions'; + +const expression = buildExpression([ + buildExpressionFunction('essql', [ q: 'select column1, column2 from myindex' ]), + buildExpressionFunction('mapColumn', [ name: 'column3', expression: 'column1 + 3' ]), + buildExpressionFunction('table'), +] +``` + +Note: Consumers need to be aware which plugin registers specific functions with expressions function registry and import correct type definitions from there. + + + The `expressions` service is available on both server and client, with similar APIs. + + +### Running expressions + +Expression service exposes `execute` method which allows you to execute an expression: + +```ts +const executionContract = expressions.execute(expression, input); +const result = await executionContract.getData(); +``` + + + Check the full spec of execute function [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.execution.md) + + +In addition, on the browser side, there are two additional ways to run expressions and render the results. + +#### React expression renderer component + +This is the easiest way to get expressions rendered inside your application. + +```ts + +``` + + + Check the full spec of ReactExpressionRenderer component props [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.reactexpressionrendererprops.md) + + +#### Expression loader + +If you are not using React, you can use the loader expression service provides to achieve the same: + +```ts +const handler = loader(domElement, expression, params); +``` + + + Check the full spec of expression loader params [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.iexpressionloaderparams.md) + + +### Creating new expression functions + +Creating a new expression function is easy, just call `registerFunction` method on expressions service setup contract with your function definition: + +```ts +const functionDefinition = { + name: 'clog', + args: {}, + help: 'Outputs the context to the console', + fn: (input: unknown) => { + // eslint-disable-next-line no-console + console.log(input); + return input; + }, +}; + +expressions.registerFunction(functionDefinition); +``` + + + Check the full interface of ExpressionFuntionDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionfunctiondefinition.md) + + +### Creating new expression renderers + +Adding new renderers is just as easy as adding functions: + +```ts +const rendererDefinition = { + name: 'debug', + help: 'Outputs the context to the dom element', + render: (domElement, input, handlers) => { + // eslint-disable-next-line no-console + domElement.innerText = JSON.strinfigy(input); + handlers.done(); + }, +}; + +expressions.registerRenderer(rendererDefinition); +``` + + + Check the full interface of ExpressionRendererDefinition [here](https://github.com/elastic/kibana/blob/master/docs/development/plugins/expressions/public/kibana-plugin-plugins-expressions-public.expressionrenderdefinition.md) + From 842bb69aea264d926ab1d8d282cfc5dd93bc30c3 Mon Sep 17 00:00:00 2001 From: Liza Katz Date: Tue, 1 Jun 2021 09:57:46 +0300 Subject: [PATCH 11/46] [bfetch] compress stream chunks (#97994) * Move inspector adapter integration into search source * docs and ts * Move other bucket to search source * test ts + delete unused tabilfy function * hierarchical param in aggconfig. ts improvements more inspector tests * fix jest * separate inspect more tests * jest * inspector * Error handling and more tests * put the fun in functional tests * delete client side legacy msearch code * ts * override to sync search in search source * delete more legacy code * ts * delete moarrrr * deflate bfetch chunks * update tests use only zlib * ts * extract getInflatedResponse * tests * Use fflate in attempt to reduce package size * use node streams, fflate and hex encoding. * DISABLE_SEARCH_COMPRESSION UI Settings Use base64 and async compression * i18n * Code review Use custom header for compression Promisify once * use custom headers * Update jest * fix tests * code review, baby! * integration * tests * limit * limit * limit Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .i18nrc.json | 1 + package.json | 1 + packages/kbn-optimizer/limits.yml | 2 +- packages/kbn-ui-shared-deps/entry.js | 2 + packages/kbn-ui-shared-deps/index.js | 1 + src/plugins/bfetch/common/batch.ts | 5 + src/plugins/bfetch/common/constants.ts | 9 ++ src/plugins/bfetch/common/index.ts | 1 + .../create_streaming_batched_function.test.ts | 50 +++++++- .../create_streaming_batched_function.ts | 17 ++- src/plugins/bfetch/public/batching/index.ts | 12 ++ src/plugins/bfetch/public/plugin.ts | 34 ++++-- .../public/streaming/fetch_streaming.test.ts | 112 +++++++++++++++++- .../public/streaming/fetch_streaming.ts | 49 ++++++-- src/plugins/bfetch/public/streaming/index.ts | 1 + .../public/streaming/inflate_response.ts | 15 +++ src/plugins/bfetch/server/plugin.ts | 32 ++++- .../streaming/create_compressed_stream.ts | 59 +++++++++ .../bfetch/server/streaming/create_stream.ts | 23 ++++ src/plugins/bfetch/server/streaming/index.ts | 2 + src/plugins/bfetch/server/ui_settings.ts | 29 +++++ .../server/collectors/management/schema.ts | 4 + .../server/collectors/management/types.ts | 1 + src/plugins/telemetry/schema/oss_plugins.json | 6 + test/api_integration/apis/search/bsearch.ts | 83 ++++++++++--- yarn.lock | 5 + 26 files changed, 504 insertions(+), 52 deletions(-) create mode 100644 src/plugins/bfetch/common/constants.ts create mode 100644 src/plugins/bfetch/public/batching/index.ts create mode 100644 src/plugins/bfetch/public/streaming/inflate_response.ts create mode 100644 src/plugins/bfetch/server/streaming/create_compressed_stream.ts create mode 100644 src/plugins/bfetch/server/streaming/create_stream.ts create mode 100644 src/plugins/bfetch/server/ui_settings.ts diff --git a/.i18nrc.json b/.i18nrc.json index dc01a10b6a686..57dffa4147e52 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -3,6 +3,7 @@ "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", + "bfetch": "src/plugins/bfetch", "dashboard": "src/plugins/dashboard", "data": "src/plugins/data", "embeddableApi": "src/plugins/embeddable", diff --git a/package.json b/package.json index f41c85c4c7b80..e5b9ca1ef98cc 100644 --- a/package.json +++ b/package.json @@ -229,6 +229,7 @@ "expiry-js": "0.1.7", "extract-zip": "^2.0.1", "fast-deep-equal": "^3.1.1", + "fflate": "^0.6.9", "file-saver": "^1.3.8", "file-type": "^10.9.0", "focus-trap-react": "^3.1.1", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index c28fd83591960..6ccf6269751b1 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -3,7 +3,7 @@ pageLoadAssetSize: alerting: 106936 apm: 64385 apmOss: 18996 - bfetch: 41874 + bfetch: 51874 canvas: 1066647 charts: 195358 cloud: 21076 diff --git a/packages/kbn-ui-shared-deps/entry.js b/packages/kbn-ui-shared-deps/entry.js index 4029ce28faf5b..d3755ed7c5f29 100644 --- a/packages/kbn-ui-shared-deps/entry.js +++ b/packages/kbn-ui-shared-deps/entry.js @@ -44,6 +44,8 @@ export const Theme = require('./theme.ts'); export const Lodash = require('lodash'); export const LodashFp = require('lodash/fp'); +export const Fflate = require('fflate/esm/browser'); + // runtime deps which don't need to be copied across all bundles export const TsLib = require('tslib'); export const KbnAnalytics = require('@kbn/analytics'); diff --git a/packages/kbn-ui-shared-deps/index.js b/packages/kbn-ui-shared-deps/index.js index 62ddb09d25add..877bf3df6c039 100644 --- a/packages/kbn-ui-shared-deps/index.js +++ b/packages/kbn-ui-shared-deps/index.js @@ -52,6 +52,7 @@ exports.externals = { '@elastic/eui/dist/eui_theme_dark.json': '__kbnSharedDeps__.Theme.euiDarkVars', lodash: '__kbnSharedDeps__.Lodash', 'lodash/fp': '__kbnSharedDeps__.LodashFp', + fflate: '__kbnSharedDeps__.Fflate', /** * runtime deps which don't need to be copied across all bundles diff --git a/src/plugins/bfetch/common/batch.ts b/src/plugins/bfetch/common/batch.ts index a84d94b541ae5..59b012751c66d 100644 --- a/src/plugins/bfetch/common/batch.ts +++ b/src/plugins/bfetch/common/batch.ts @@ -19,3 +19,8 @@ export interface BatchResponseItem new Promise((resolve) => setImmediate(resolve)); const getPromiseState = (promise: Promise): Promise<'resolved' | 'rejected' | 'pending'> => Promise.race<'resolved' | 'rejected' | 'pending'>([ @@ -52,6 +54,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, + compressionDisabled$: rxof(true), }); expect(typeof fn).toBe('function'); }); @@ -61,6 +64,7 @@ describe('createStreamingBatchedFunction()', () => { const fn = createStreamingBatchedFunction({ url: '/test', fetchStreaming, + compressionDisabled$: rxof(true), }); const res = fn({}); expect(typeof res.then).toBe('function'); @@ -74,6 +78,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -93,6 +98,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); @@ -107,6 +113,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ foo: 'bar' }); @@ -125,6 +132,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ foo: 'bar' }); @@ -146,14 +154,18 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ foo: 'bar' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ baz: 'quix' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(0); fn({ full: 'yep' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(1); }); @@ -164,6 +176,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -186,11 +199,13 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ a: '1' }); fn({ b: '2' }); fn({ c: '3' }); + await flushPromises(); expect(fetchStreaming.mock.calls[0][0]).toMatchObject({ url: '/test', @@ -209,13 +224,16 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); fn({ a: '1' }); fn({ b: '2' }); fn({ c: '3' }); + await flushPromises(); expect(fetchStreaming).toHaveBeenCalledTimes(1); fn({ d: '4' }); + await flushPromises(); await new Promise((r) => setTimeout(r, 6)); expect(fetchStreaming).toHaveBeenCalledTimes(2); }); @@ -229,6 +247,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -246,8 +265,11 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); + await flushPromises(); + const promise1 = fn({ a: '1' }); const promise2 = fn({ b: '2' }); const promise3 = fn({ c: '3' }); @@ -287,6 +309,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -314,6 +337,20 @@ describe('createStreamingBatchedFunction()', () => { expect(await promise3).toEqual({ foo: 'bar 2' }); }); + test('compression is false by default', async () => { + const { fetchStreaming } = setup(); + const fn = createStreamingBatchedFunction({ + url: '/test', + flushOnMaxItems: 1, + fetchStreaming, + }); + + fn({ a: '1' }); + + const dontCompress = await fetchStreaming.mock.calls[0][0].compressionDisabled$.toPromise(); + expect(dontCompress).toBe(false); + }); + test('resolves falsy results', async () => { const { fetchStreaming, stream } = setup(); const fn = createStreamingBatchedFunction({ @@ -321,6 +358,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = fn({ a: '1' }); @@ -362,6 +400,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise = fn({ a: '1' }); @@ -390,6 +429,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -442,6 +482,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -471,6 +512,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const abortController = new AbortController(); @@ -509,6 +551,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -539,6 +582,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -576,6 +620,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -608,6 +653,7 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); const promise1 = of(fn({ a: '1' })); @@ -644,7 +690,9 @@ describe('createStreamingBatchedFunction()', () => { fetchStreaming, maxItemAge: 5, flushOnMaxItems: 3, + compressionDisabled$: rxof(true), }); + await flushPromises(); const promise1 = of(fn({ a: '1' })); const promise2 = of(fn({ a: '2' })); diff --git a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts index 2d81331f10a88..d5f955f517d13 100644 --- a/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts +++ b/src/plugins/bfetch/public/batching/create_streaming_batched_function.ts @@ -6,16 +6,16 @@ * Side Public License, v 1. */ +import { Observable, of } from 'rxjs'; import { AbortError, abortSignalToPromise, defer } from '../../../kibana_utils/public'; import { ItemBufferParams, TimedItemBufferParams, createBatchedFunction, - BatchResponseItem, ErrorLike, + normalizeError, } from '../../common'; -import { fetchStreaming, split } from '../streaming'; -import { normalizeError } from '../../common'; +import { fetchStreaming } from '../streaming'; import { BatchedFunc, BatchItem } from './types'; export interface BatchedFunctionProtocolError extends ErrorLike { @@ -47,6 +47,11 @@ export interface StreamingBatchedFunctionParams { * before sending the batch request. */ maxItemAge?: TimedItemBufferParams['maxItemAge']; + + /** + * Disabled zlib compression of response chunks. + */ + compressionDisabled$?: Observable; } /** @@ -64,6 +69,7 @@ export const createStreamingBatchedFunction = ( fetchStreaming: fetchStreamingInjected = fetchStreaming, flushOnMaxItems = 25, maxItemAge = 10, + compressionDisabled$ = of(false), } = params; const [fn] = createBatchedFunction({ onCall: (payload: Payload, signal?: AbortSignal) => { @@ -119,6 +125,7 @@ export const createStreamingBatchedFunction = ( body: JSON.stringify({ batch }), method: 'POST', signal: abortController.signal, + compressionDisabled$, }); const handleStreamError = (error: any) => { @@ -127,10 +134,10 @@ export const createStreamingBatchedFunction = ( for (const { future } of items) future.reject(normalizedError); }; - stream.pipe(split('\n')).subscribe({ + stream.subscribe({ next: (json: string) => { try { - const response = JSON.parse(json) as BatchResponseItem; + const response = JSON.parse(json); if (response.error) { items[response.id].future.reject(response.error); } else if (response.result !== undefined) { diff --git a/src/plugins/bfetch/public/batching/index.ts b/src/plugins/bfetch/public/batching/index.ts new file mode 100644 index 0000000000000..115fd84cbe979 --- /dev/null +++ b/src/plugins/bfetch/public/batching/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { + createStreamingBatchedFunction, + StreamingBatchedFunctionParams, +} from './create_streaming_batched_function'; diff --git a/src/plugins/bfetch/public/plugin.ts b/src/plugins/bfetch/public/plugin.ts index ed97d468eec0b..f97a91a0e70d3 100644 --- a/src/plugins/bfetch/public/plugin.ts +++ b/src/plugins/bfetch/public/plugin.ts @@ -7,12 +7,11 @@ */ import { CoreStart, PluginInitializerContext, CoreSetup, Plugin } from 'src/core/public'; +import { from, Observable, of } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; import { fetchStreaming as fetchStreamingStatic, FetchStreamingParams } from './streaming'; -import { removeLeadingSlash } from '../common'; -import { - createStreamingBatchedFunction, - StreamingBatchedFunctionParams, -} from './batching/create_streaming_batched_function'; +import { DISABLE_BFETCH_COMPRESSION, removeLeadingSlash } from '../common'; +import { createStreamingBatchedFunction, StreamingBatchedFunctionParams } from './batching'; import { BatchedFunc } from './batching/types'; // eslint-disable-next-line @@ -43,12 +42,23 @@ export class BfetchPublicPlugin constructor(private readonly initializerContext: PluginInitializerContext) {} - public setup(core: CoreSetup, plugins: BfetchPublicSetupDependencies): BfetchPublicSetup { + public setup( + core: CoreSetup, + plugins: BfetchPublicSetupDependencies + ): BfetchPublicSetup { const { version } = this.initializerContext.env.packageInfo; const basePath = core.http.basePath.get(); - const fetchStreaming = this.fetchStreaming(version, basePath); - const batchedFunction = this.batchedFunction(fetchStreaming); + const compressionDisabled$ = from(core.getStartServices()).pipe( + switchMap((deps) => { + return of(deps[0]); + }), + switchMap((coreStart) => { + return coreStart.uiSettings.get$(DISABLE_BFETCH_COMPRESSION); + }) + ); + const fetchStreaming = this.fetchStreaming(version, basePath, compressionDisabled$); + const batchedFunction = this.batchedFunction(fetchStreaming, compressionDisabled$); this.contract = { fetchStreaming, @@ -66,7 +76,8 @@ export class BfetchPublicPlugin private fetchStreaming = ( version: string, - basePath: string + basePath: string, + compressionDisabled$: Observable ): BfetchPublicSetup['fetchStreaming'] => (params) => fetchStreamingStatic({ ...params, @@ -76,13 +87,16 @@ export class BfetchPublicPlugin 'kbn-version': version, ...(params.headers || {}), }, + compressionDisabled$, }); private batchedFunction = ( - fetchStreaming: BfetchPublicContract['fetchStreaming'] + fetchStreaming: BfetchPublicContract['fetchStreaming'], + compressionDisabled$: Observable ): BfetchPublicContract['batchedFunction'] => (params) => createStreamingBatchedFunction({ ...params, + compressionDisabled$, fetchStreaming: params.fetchStreaming || fetchStreaming, }); } diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts index e804b3ea94227..a5d066f6d9a24 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.test.ts @@ -8,6 +8,15 @@ import { fetchStreaming } from './fetch_streaming'; import { mockXMLHttpRequest } from '../test_helpers/xhr'; +import { of } from 'rxjs'; +import { promisify } from 'util'; +import { deflate } from 'zlib'; +const pDeflate = promisify(deflate); + +const compressResponse = async (resp: any) => { + const gzipped = await pDeflate(JSON.stringify(resp)); + return gzipped.toString('base64'); +}; const tick = () => new Promise((resolve) => setTimeout(resolve, 1)); @@ -21,6 +30,7 @@ test('returns XHR request', () => { setup(); const { xhr } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(typeof xhr.readyState).toBe('number'); }); @@ -29,6 +39,7 @@ test('returns stream', () => { setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(typeof stream.subscribe).toBe('function'); }); @@ -37,6 +48,7 @@ test('promise resolves when request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); let resolved = false; @@ -65,10 +77,90 @@ test('promise resolves when request completes', async () => { expect(resolved).toBe(true); }); -test('streams incoming text as it comes through', async () => { +test('promise resolves when compressed request completes', async () => { + const env = setup(); + const { stream } = fetchStreaming({ + url: 'http://example.com', + compressionDisabled$: of(false), + }); + + let resolved = false; + let result; + stream.toPromise().then((r) => { + resolved = true; + result = r; + }); + + await tick(); + expect(resolved).toBe(false); + + const msg = { foo: 'bar' }; + + // Whole message in a response + (env.xhr as any).responseText = `${await compressResponse(msg)}\n`; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + (env.xhr as any).readyState = 4; + (env.xhr as any).status = 200; + env.xhr.onreadystatechange!({} as any); + + await tick(); + expect(resolved).toBe(true); + expect(result).toStrictEqual(JSON.stringify(msg)); +}); + +test('promise resolves when compressed chunked request completes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(false), + }); + + let resolved = false; + let result; + stream.toPromise().then((r) => { + resolved = true; + result = r; + }); + + await tick(); + expect(resolved).toBe(false); + + const msg = { veg: 'tomato' }; + const msgToCut = await compressResponse(msg); + const part1 = msgToCut.substr(0, 3); + + // Message and a half in a response + (env.xhr as any).responseText = part1; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + // Half a message in a response + (env.xhr as any).responseText = `${msgToCut}\n`; + env.xhr.onprogress!({} as any); + + await tick(); + expect(resolved).toBe(false); + + (env.xhr as any).readyState = 4; + (env.xhr as any).status = 200; + env.xhr.onreadystatechange!({} as any); + + await tick(); + expect(resolved).toBe(true); + expect(result).toStrictEqual(JSON.stringify(msg)); +}); + +test('streams incoming text as it comes through, according to separators', async () => { + const env = setup(); + const { stream } = fetchStreaming({ + url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -80,16 +172,22 @@ test('streams incoming text as it comes through', async () => { (env.xhr as any).responseText = 'foo'; env.xhr.onprogress!({} as any); + await tick(); + expect(spy).toHaveBeenCalledTimes(0); + + (env.xhr as any).responseText = 'foo\nbar'; + env.xhr.onprogress!({} as any); + await tick(); expect(spy).toHaveBeenCalledTimes(1); expect(spy).toHaveBeenCalledWith('foo'); - (env.xhr as any).responseText = 'foo\nbar'; + (env.xhr as any).responseText = 'foo\nbar\n'; env.xhr.onprogress!({} as any); await tick(); expect(spy).toHaveBeenCalledTimes(2); - expect(spy).toHaveBeenCalledWith('\nbar'); + expect(spy).toHaveBeenCalledWith('bar'); (env.xhr as any).readyState = 4; (env.xhr as any).status = 200; @@ -103,6 +201,7 @@ test('completes stream observable when request finishes', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -127,6 +226,7 @@ test('completes stream observable when aborted', async () => { const { stream } = fetchStreaming({ url: 'http://example.com', signal: abort.signal, + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -152,6 +252,7 @@ test('promise throws when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -178,6 +279,7 @@ test('stream observable errors when request errors', async () => { const env = setup(); const { stream } = fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); const spy = jest.fn(); @@ -210,6 +312,7 @@ test('sets custom headers', async () => { 'Content-Type': 'text/plain', Authorization: 'Bearer 123', }, + compressionDisabled$: of(true), }); expect(env.xhr.setRequestHeader).toHaveBeenCalledWith('Content-Type', 'text/plain'); @@ -223,6 +326,7 @@ test('uses credentials', async () => { fetchStreaming({ url: 'http://example.com', + compressionDisabled$: of(true), }); expect(env.xhr.withCredentials).toBe(true); @@ -238,6 +342,7 @@ test('opens XHR request and sends specified body', async () => { url: 'http://elastic.co', method: 'GET', body: 'foobar', + compressionDisabled$: of(true), }); expect(env.xhr.open).toHaveBeenCalledTimes(1); @@ -250,6 +355,7 @@ test('uses POST request method by default', async () => { const env = setup(); fetchStreaming({ url: 'http://elastic.co', + compressionDisabled$: of(true), }); expect(env.xhr.open).toHaveBeenCalledWith('POST', 'http://elastic.co'); }); diff --git a/src/plugins/bfetch/public/streaming/fetch_streaming.ts b/src/plugins/bfetch/public/streaming/fetch_streaming.ts index d68e4d01b44f5..1af35ef68fb85 100644 --- a/src/plugins/bfetch/public/streaming/fetch_streaming.ts +++ b/src/plugins/bfetch/public/streaming/fetch_streaming.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ +import { Observable, of } from 'rxjs'; +import { map, share, switchMap } from 'rxjs/operators'; +import { inflateResponse } from '.'; import { fromStreamingXhr } from './from_streaming_xhr'; +import { split } from './split'; export interface FetchStreamingParams { url: string; @@ -14,6 +18,7 @@ export interface FetchStreamingParams { method?: 'GET' | 'POST'; body?: string; signal?: AbortSignal; + compressionDisabled$?: Observable; } /** @@ -26,23 +31,49 @@ export function fetchStreaming({ method = 'POST', body = '', signal, + compressionDisabled$ = of(false), }: FetchStreamingParams) { const xhr = new window.XMLHttpRequest(); - // Begin the request - xhr.open(method, url); - xhr.withCredentials = true; + const msgStream = compressionDisabled$.pipe( + switchMap((compressionDisabled) => { + // Begin the request + xhr.open(method, url); + xhr.withCredentials = true; - // Set the HTTP headers - Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); + if (!compressionDisabled) { + headers['X-Chunk-Encoding'] = 'deflate'; + } - const stream = fromStreamingXhr(xhr, signal); + // Set the HTTP headers + Object.entries(headers).forEach(([k, v]) => xhr.setRequestHeader(k, v)); - // Send the payload to the server - xhr.send(body); + const stream = fromStreamingXhr(xhr, signal); + + // Send the payload to the server + xhr.send(body); + + // Return a stream of chunked decompressed messages + return stream.pipe( + split('\n'), + map((msg) => { + return compressionDisabled ? msg : inflateResponse(msg); + }) + ); + }), + share() + ); + + // start execution + const msgStreamSub = msgStream.subscribe({ + error: (e) => {}, + complete: () => { + msgStreamSub.unsubscribe(); + }, + }); return { xhr, - stream, + stream: msgStream, }; } diff --git a/src/plugins/bfetch/public/streaming/index.ts b/src/plugins/bfetch/public/streaming/index.ts index afb442feffb29..545cae87aa3d6 100644 --- a/src/plugins/bfetch/public/streaming/index.ts +++ b/src/plugins/bfetch/public/streaming/index.ts @@ -9,3 +9,4 @@ export * from './split'; export * from './from_streaming_xhr'; export * from './fetch_streaming'; +export { inflateResponse } from './inflate_response'; diff --git a/src/plugins/bfetch/public/streaming/inflate_response.ts b/src/plugins/bfetch/public/streaming/inflate_response.ts new file mode 100644 index 0000000000000..73cb52285987c --- /dev/null +++ b/src/plugins/bfetch/public/streaming/inflate_response.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { unzlibSync, strFromU8 } from 'fflate'; + +export function inflateResponse(response: string) { + const buff = Buffer.from(response, 'base64'); + const unzip = unzlibSync(buff); + return strFromU8(unzip); +} diff --git a/src/plugins/bfetch/server/plugin.ts b/src/plugins/bfetch/server/plugin.ts index 18f0813260f03..7fd46e2f6cc44 100644 --- a/src/plugins/bfetch/server/plugin.ts +++ b/src/plugins/bfetch/server/plugin.ts @@ -16,6 +16,7 @@ import type { RouteMethod, RequestHandler, RequestHandlerContext, + StartServicesAccessor, } from 'src/core/server'; import { schema } from '@kbn/config-schema'; import { Subject } from 'rxjs'; @@ -28,7 +29,8 @@ import { normalizeError, } from '../common'; import { StreamingRequestHandler } from './types'; -import { createNDJSONStream } from './streaming'; +import { createStream } from './streaming'; +import { getUiSettings } from './ui_settings'; // eslint-disable-next-line export interface BfetchServerSetupDependencies {} @@ -112,9 +114,19 @@ export class BfetchServerPlugin public setup(core: CoreSetup, plugins: BfetchServerSetupDependencies): BfetchServerSetup { const logger = this.initializerContext.logger.get(); const router = core.http.createRouter(); - const addStreamingResponseRoute = this.addStreamingResponseRoute({ router, logger }); + + core.uiSettings.register(getUiSettings()); + + const addStreamingResponseRoute = this.addStreamingResponseRoute({ + getStartServices: core.getStartServices, + router, + logger, + }); const addBatchProcessingRoute = this.addBatchProcessingRoute(addStreamingResponseRoute); - const createStreamingRequestHandler = this.createStreamingRequestHandler({ logger }); + const createStreamingRequestHandler = this.createStreamingRequestHandler({ + getStartServices: core.getStartServices, + logger, + }); return { addBatchProcessingRoute, @@ -129,10 +141,16 @@ export class BfetchServerPlugin public stop() {} + private getCompressionDisabled(request: KibanaRequest) { + return request.headers['x-chunk-encoding'] !== 'deflate'; + } + private addStreamingResponseRoute = ({ + getStartServices, router, logger, }: { + getStartServices: StartServicesAccessor; router: ReturnType; logger: Logger; }): BfetchServerSetup['addStreamingResponseRoute'] => (path, handler) => { @@ -146,9 +164,10 @@ export class BfetchServerPlugin async (context, request, response) => { const handlerInstance = handler(request); const data = request.body; + const compressionDisabled = this.getCompressionDisabled(request); return response.ok({ headers: streamingHeaders, - body: createNDJSONStream(handlerInstance.getResponseStream(data), logger), + body: createStream(handlerInstance.getResponseStream(data), logger, compressionDisabled), }); } ); @@ -156,17 +175,20 @@ export class BfetchServerPlugin private createStreamingRequestHandler = ({ logger, + getStartServices, }: { logger: Logger; + getStartServices: StartServicesAccessor; }): BfetchServerSetup['createStreamingRequestHandler'] => (streamHandler) => async ( context, request, response ) => { const response$ = await streamHandler(context, request); + const compressionDisabled = this.getCompressionDisabled(request); return response.ok({ headers: streamingHeaders, - body: createNDJSONStream(response$, logger), + body: createStream(response$, logger, compressionDisabled), }); }; diff --git a/src/plugins/bfetch/server/streaming/create_compressed_stream.ts b/src/plugins/bfetch/server/streaming/create_compressed_stream.ts new file mode 100644 index 0000000000000..6814ed1dd7955 --- /dev/null +++ b/src/plugins/bfetch/server/streaming/create_compressed_stream.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { promisify } from 'util'; +import { Observable } from 'rxjs'; +import { catchError, concatMap, finalize } from 'rxjs/operators'; +import { Logger } from 'src/core/server'; +import { Stream, PassThrough } from 'stream'; +import { constants, deflate } from 'zlib'; + +const delimiter = '\n'; +const pDeflate = promisify(deflate); + +async function zipMessageToStream(output: PassThrough, message: string) { + return new Promise(async (resolve, reject) => { + try { + const gzipped = await pDeflate(message, { + flush: constants.Z_SYNC_FLUSH, + }); + output.write(gzipped.toString('base64')); + output.write(delimiter); + resolve(undefined); + } catch (err) { + reject(err); + } + }); +} + +export const createCompressedStream = ( + results: Observable, + logger: Logger +): Stream => { + const output = new PassThrough(); + + const sub = results + .pipe( + concatMap((message: Response) => { + const strMessage = JSON.stringify(message); + return zipMessageToStream(output, strMessage); + }), + catchError((e) => { + logger.error('Could not serialize or stream a message.'); + logger.error(e); + throw e; + }), + finalize(() => { + output.end(); + sub.unsubscribe(); + }) + ) + .subscribe(); + + return output; +}; diff --git a/src/plugins/bfetch/server/streaming/create_stream.ts b/src/plugins/bfetch/server/streaming/create_stream.ts new file mode 100644 index 0000000000000..7d6981294341b --- /dev/null +++ b/src/plugins/bfetch/server/streaming/create_stream.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Logger } from 'kibana/server'; +import { Stream } from 'stream'; +import { Observable } from 'rxjs'; +import { createCompressedStream } from './create_compressed_stream'; +import { createNDJSONStream } from './create_ndjson_stream'; + +export function createStream( + response$: Observable, + logger: Logger, + compressionDisabled: boolean +): Stream { + return compressionDisabled + ? createNDJSONStream(response$, logger) + : createCompressedStream(response$, logger); +} diff --git a/src/plugins/bfetch/server/streaming/index.ts b/src/plugins/bfetch/server/streaming/index.ts index 2c31cc329295d..dfd472b5034a1 100644 --- a/src/plugins/bfetch/server/streaming/index.ts +++ b/src/plugins/bfetch/server/streaming/index.ts @@ -7,3 +7,5 @@ */ export * from './create_ndjson_stream'; +export * from './create_compressed_stream'; +export * from './create_stream'; diff --git a/src/plugins/bfetch/server/ui_settings.ts b/src/plugins/bfetch/server/ui_settings.ts new file mode 100644 index 0000000000000..cf7b13a9af182 --- /dev/null +++ b/src/plugins/bfetch/server/ui_settings.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { i18n } from '@kbn/i18n'; +import { UiSettingsParams } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; +import { DISABLE_BFETCH_COMPRESSION } from '../common'; + +export function getUiSettings(): Record> { + return { + [DISABLE_BFETCH_COMPRESSION]: { + name: i18n.translate('bfetch.disableBfetchCompression', { + defaultMessage: 'Disable Batch Compression', + }), + value: false, + description: i18n.translate('bfetch.disableBfetchCompressionDesc', { + defaultMessage: + 'Disable batch compression. This allows you to debug individual requests, but increases response size.', + }), + schema: schema.boolean(), + category: [], + }, + }; +} diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index f1592d5a8cf0b..5f70deccba93c 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -388,6 +388,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'long', _meta: { description: 'Non-default value of setting.' }, }, + 'bfetch:disableCompression': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'visualization:visualize:legacyChartsLibrary': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 570b52171be28..bf28bb6cc01f5 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -22,6 +22,7 @@ export interface UsageStats { /** * non-sensitive settings */ + 'bfetch:disableCompression': boolean; 'autocomplete:useTimeRange': boolean; 'search:timeout': number; 'visualization:visualize:legacyChartsLibrary': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 693957057f108..0ca1b863f91a7 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8217,6 +8217,12 @@ "description": "Non-default value of setting." } }, + "bfetch:disableCompression": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "visualization:visualize:legacyChartsLibrary": { "type": "boolean", "_meta": { diff --git a/test/api_integration/apis/search/bsearch.ts b/test/api_integration/apis/search/bsearch.ts index f539d0f9e4544..11fb74200d7dd 100644 --- a/test/api_integration/apis/search/bsearch.ts +++ b/test/api_integration/apis/search/bsearch.ts @@ -8,15 +8,18 @@ import expect from '@kbn/expect'; import request from 'superagent'; +import { inflateResponse } from '../../../../src/plugins/bfetch/public/streaming'; import { FtrProviderContext } from '../../ftr_provider_context'; import { painlessErrReq } from './painless_err_req'; import { verifyErrorResponse } from './verify_error'; -function parseBfetchResponse(resp: request.Response): Array> { +function parseBfetchResponse(resp: request.Response, compressed: boolean = false) { return resp.text .trim() .split('\n') - .map((item) => JSON.parse(item)); + .map((item) => { + return JSON.parse(compressed ? inflateResponse(item) : item); + }); } export default function ({ getService }: FtrProviderContext) { @@ -26,29 +29,69 @@ export default function ({ getService }: FtrProviderContext) { describe('bsearch', () => { describe('post', () => { it('should return 200 a single response', async () => { - const resp = await supertest.post(`/internal/bsearch`).send({ - batch: [ - { - request: { - params: { - body: { - query: { - match_all: {}, + const resp = await supertest + .post(`/internal/bsearch`) + .set({ 'X-Chunk-Encoding': '' }) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, }, }, }, + options: { + strategy: 'es', + }, }, - }, - ], - }); + ], + }); + + const jsonBody = parseBfetchResponse(resp); + + expect(resp.status).to.be(200); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); + }); + + it('should return 200 a single response from compressed', async () => { + const resp = await supertest + .post(`/internal/bsearch`) + .set({ 'X-Chunk-Encoding': 'deflate' }) + .send({ + batch: [ + { + request: { + params: { + index: '.kibana', + body: { + query: { + match_all: {}, + }, + }, + }, + }, + options: { + strategy: 'es', + }, + }, + ], + }); - const jsonBody = JSON.parse(resp.text); + const jsonBody = parseBfetchResponse(resp, true); expect(resp.status).to.be(200); - expect(jsonBody.id).to.be(0); - expect(jsonBody.result).to.have.property('isPartial'); - expect(jsonBody.result).to.have.property('isRunning'); - expect(jsonBody.result).to.have.property('rawResponse'); + expect(jsonBody[0].id).to.be(0); + expect(jsonBody[0].result.isPartial).to.be(false); + expect(jsonBody[0].result.isRunning).to.be(false); + expect(jsonBody[0].result).to.have.property('rawResponse'); }); it('should return a batch of successful responses', async () => { @@ -57,6 +100,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -68,6 +112,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -95,6 +140,7 @@ export default function ({ getService }: FtrProviderContext) { { request: { params: { + index: '.kibana', body: { query: { match_all: {}, @@ -121,6 +167,7 @@ export default function ({ getService }: FtrProviderContext) { batch: [ { request: { + index: '.kibana', indexType: 'baad', params: { body: { diff --git a/yarn.lock b/yarn.lock index ee4fadac018bc..1e0f9a8821d1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13483,6 +13483,11 @@ fetch-mock@^7.3.9: path-to-regexp "^2.2.1" whatwg-url "^6.5.0" +fflate@^0.6.9: + version "0.6.9" + resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.6.9.tgz#fb369b30792a03ff7274e174f3b36e51292d3f99" + integrity sha512-hmAdxNHub7fw36hX7BHiuAO0uekp6ufY2sjxBXWxIf0sw5p7tnS9GVrdM4D12SDYQUHVpiC50fPBYPTjOzRU2Q== + figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" From f7698bd8aa8787d683c728300ba4ca52b202369c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Jun 2021 11:21:29 +0200 Subject: [PATCH 12/46] [Observability] Expose options to customize sidebar route matching (#100886) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../components/shared/page_template/README.md | 38 ++++++++++++++++++- .../shared/page_template/page_template.tsx | 2 + .../public/services/navigation_registry.ts | 10 +++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/x-pack/plugins/observability/public/components/shared/page_template/README.md b/x-pack/plugins/observability/public/components/shared/page_template/README.md index fb2a603cc7a7f..104b365e94fe9 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/README.md +++ b/x-pack/plugins/observability/public/components/shared/page_template/README.md @@ -12,7 +12,42 @@ To register a solution's navigation structure you'll first need to ensure your s ], ``` -Now within your solution's **public** plugin `setup` lifecycle method you can call the `registerSections` method, this will register your solution's specific navigation structure with the overall Observability navigation registry. E.g. +Now within your solution's **public** plugin `setup` lifecycle method you can +call the `registerSections` method, this will register your solution's specific +navigation structure with the overall Observability navigation registry. + +The `registerSections` function takes an `Observable` of an array of +`NavigationSection`s. Each section can be defined as + +```typescript +export interface NavigationSection { + // the label of the section, should be translated + label: string | undefined; + // the key to sort by in ascending order relative to other entries + sortKey: number; + // the entries to render inside the section + entries: NavigationEntry[]; +} +``` + +Each entry inside of a navigation section is defined as + +```typescript +export interface NavigationEntry { + // the label of the menu entry, should be translated + label: string; + // the kibana app id + app: string; + // the path after the application prefix corresponding to this entry + path: string; + // whether to only match when the full path matches, defaults to `false` + matchFullPath?: boolean; + // whether to ignore trailing slashes, defaults to `true` + ignoreTrailingSlash?: boolean; +} +``` + +A registration might therefore look like the following: ```typescript // x-pack/plugins/example_plugin/public/plugin.ts @@ -29,6 +64,7 @@ export class Plugin implements PluginClass { label: 'A solution section', sortKey: 200, entries: [ + { label: 'Home Page', app: 'exampleA', path: '/', matchFullPath: true }, { label: 'Example Page', app: 'exampleA', path: '/example' }, { label: 'Another Example Page', app: 'exampleA', path: '/another-example' }, ], diff --git a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx index 8025c6d658692..bebcd53f8ae6c 100644 --- a/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx +++ b/x-pack/plugins/observability/public/components/shared/page_template/page_template.tsx @@ -71,6 +71,8 @@ export function ObservabilityPageTemplate({ entry.app === currentAppId && matchPath(currentPath, { path: entry.path, + exact: !!entry.matchFullPath, + strict: !entry.ignoreTrailingSlash, }) != null; return { diff --git a/x-pack/plugins/observability/public/services/navigation_registry.ts b/x-pack/plugins/observability/public/services/navigation_registry.ts index f42f34fcfe9bb..79a36731f7ed1 100644 --- a/x-pack/plugins/observability/public/services/navigation_registry.ts +++ b/x-pack/plugins/observability/public/services/navigation_registry.ts @@ -9,15 +9,25 @@ import { combineLatest, Observable, ReplaySubject } from 'rxjs'; import { map, scan, shareReplay, switchMap } from 'rxjs/operators'; export interface NavigationSection { + // the label of the section, should be translated label: string | undefined; + // the key to sort by in ascending order relative to other entries sortKey: number; + // the entries to render inside the section entries: NavigationEntry[]; } export interface NavigationEntry { + // the label of the menu entry, should be translated label: string; + // the kibana app id app: string; + // the path after the application prefix corresponding to this entry path: string; + // whether to only match when the full path matches, defaults to `false` + matchFullPath?: boolean; + // whether to ignore trailing slashes, defaults to `true` + ignoreTrailingSlash?: boolean; } export interface NavigationRegistry { From 1168e116390b990084b89e73d436a1d5aae1cbc4 Mon Sep 17 00:00:00 2001 From: Ahmad Bamieh Date: Tue, 1 Jun 2021 14:09:10 +0300 Subject: [PATCH 13/46] [Deprecations service] add `deprecationType` and use it in configs deprecations (#100983) --- ...ver.deprecationsdetails.deprecationtype.md | 15 +++ ...-plugin-core-server.deprecationsdetails.md | 1 + .../deprecations/deprecations_factory.mock.ts | 27 +++++ .../deprecations/deprecations_factory.test.ts | 2 +- .../deprecations_registry.mock.ts | 37 +++++++ .../deprecations_registry.test.ts | 2 +- .../deprecations/deprecations_registry.ts | 6 +- .../deprecations/deprecations_service.test.ts | 99 +++++++++++++++++++ .../deprecations/deprecations_service.ts | 5 +- src/core/server/deprecations/types.ts | 10 ++ src/core/server/server.api.md | 1 + src/core/server/server.ts | 2 - .../core_plugin_deprecations/server/plugin.ts | 1 + .../test_suites/core/deprecations.ts | 6 +- 14 files changed, 204 insertions(+), 10 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md create mode 100644 src/core/server/deprecations/deprecations_factory.mock.ts create mode 100644 src/core/server/deprecations/deprecations_registry.mock.ts create mode 100644 src/core/server/deprecations/deprecations_service.test.ts diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md new file mode 100644 index 0000000000000..3a76bc60ee630 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) + +## DeprecationsDetails.deprecationType property + +(optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab. + +Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. + +Signature: + +```typescript +deprecationType?: 'config' | 'feature'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md index bb77e4247711f..6e46ce0b8611f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -15,6 +15,7 @@ export interface DeprecationsDetails | Property | Type | Description | | --- | --- | --- | | [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
};
manualSteps?: string[];
} | | +| [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | | [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | | [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | | [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | | diff --git a/src/core/server/deprecations/deprecations_factory.mock.ts b/src/core/server/deprecations/deprecations_factory.mock.ts new file mode 100644 index 0000000000000..91ae4e6fa9af9 --- /dev/null +++ b/src/core/server/deprecations/deprecations_factory.mock.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeprecationsFactory } from './deprecations_factory'; +type DeprecationsFactoryContract = PublicMethodsOf; + +const createDeprecationsFactoryMock = () => { + const mocked: jest.Mocked = { + getRegistry: jest.fn(), + getDeprecations: jest.fn(), + getAllDeprecations: jest.fn(), + }; + + mocked.getDeprecations.mockResolvedValue([]); + mocked.getAllDeprecations.mockResolvedValue([]); + return mocked as jest.Mocked; +}; + +export const mockDeprecationsFactory = { + create: createDeprecationsFactoryMock, +}; diff --git a/src/core/server/deprecations/deprecations_factory.test.ts b/src/core/server/deprecations/deprecations_factory.test.ts index 469451b0020c0..187f3880f9998 100644 --- a/src/core/server/deprecations/deprecations_factory.test.ts +++ b/src/core/server/deprecations/deprecations_factory.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { GetDeprecationsContext } from './types'; +import type { GetDeprecationsContext } from './types'; import { DeprecationsFactory } from './deprecations_factory'; import { loggerMock } from '../logging/logger.mock'; diff --git a/src/core/server/deprecations/deprecations_registry.mock.ts b/src/core/server/deprecations/deprecations_registry.mock.ts new file mode 100644 index 0000000000000..bb178c3935cdc --- /dev/null +++ b/src/core/server/deprecations/deprecations_registry.mock.ts @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { PublicMethodsOf } from '@kbn/utility-types'; +import type { DeprecationsRegistry } from './deprecations_registry'; +import type { GetDeprecationsContext } from './types'; +import { elasticsearchClientMock } from '../elasticsearch/client/mocks'; +import { savedObjectsClientMock } from '../saved_objects/service/saved_objects_client.mock'; +type DeprecationsRegistryContract = PublicMethodsOf; + +const createDeprecationsRegistryMock = () => { + const mocked: jest.Mocked = { + registerDeprecations: jest.fn(), + getDeprecations: jest.fn(), + }; + + return mocked as jest.Mocked; +}; + +const createGetDeprecationsContextMock = () => { + const mocked: jest.Mocked = { + esClient: elasticsearchClientMock.createScopedClusterClient(), + savedObjectsClient: savedObjectsClientMock.create(), + }; + + return mocked; +}; + +export const mockDeprecationsRegistry = { + create: createDeprecationsRegistryMock, + createGetDeprecationsContext: createGetDeprecationsContextMock, +}; diff --git a/src/core/server/deprecations/deprecations_registry.test.ts b/src/core/server/deprecations/deprecations_registry.test.ts index 507677a531861..82b09beaa5123 100644 --- a/src/core/server/deprecations/deprecations_registry.test.ts +++ b/src/core/server/deprecations/deprecations_registry.test.ts @@ -7,7 +7,7 @@ */ /* eslint-disable dot-notation */ -import { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import type { RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; import { DeprecationsRegistry } from './deprecations_registry'; describe('DeprecationsRegistry', () => { diff --git a/src/core/server/deprecations/deprecations_registry.ts b/src/core/server/deprecations/deprecations_registry.ts index f92d807514b82..cc05473923ac8 100644 --- a/src/core/server/deprecations/deprecations_registry.ts +++ b/src/core/server/deprecations/deprecations_registry.ts @@ -6,7 +6,11 @@ * Side Public License, v 1. */ -import { DeprecationsDetails, RegisterDeprecationsConfig, GetDeprecationsContext } from './types'; +import type { + DeprecationsDetails, + RegisterDeprecationsConfig, + GetDeprecationsContext, +} from './types'; export class DeprecationsRegistry { private readonly deprecationContexts: RegisterDeprecationsConfig[] = []; diff --git a/src/core/server/deprecations/deprecations_service.test.ts b/src/core/server/deprecations/deprecations_service.test.ts new file mode 100644 index 0000000000000..d1ed7a83402cb --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +/* eslint-disable dot-notation */ +import { DeprecationsService } from './deprecations_service'; +import { httpServiceMock } from '../http/http_service.mock'; +import { mockRouter } from '../http/router/router.mock'; +import { mockCoreContext } from '../core_context.mock'; +import { mockDeprecationsFactory } from './deprecations_factory.mock'; +import { mockDeprecationsRegistry } from './deprecations_registry.mock'; + +describe('DeprecationsService', () => { + const coreContext = mockCoreContext.create(); + beforeEach(() => jest.clearAllMocks()); + + describe('#setup', () => { + const http = httpServiceMock.createInternalSetupContract(); + const router = mockRouter.create(); + http.createRouter.mockReturnValue(router); + const deprecationsCoreSetupDeps = { http }; + + it('registers routes', () => { + const deprecationsService = new DeprecationsService(coreContext); + deprecationsService.setup(deprecationsCoreSetupDeps); + // Registers correct base api path + expect(http.createRouter).toBeCalledWith('/api/deprecations'); + // registers get route '/' + expect(router.get).toHaveBeenCalledTimes(1); + expect(router.get).toHaveBeenCalledWith({ path: '/', validate: false }, expect.any(Function)); + }); + + it('calls registerConfigDeprecationsInfo', () => { + const deprecationsService = new DeprecationsService(coreContext); + const mockRegisterConfigDeprecationsInfo = jest.fn(); + deprecationsService['registerConfigDeprecationsInfo'] = mockRegisterConfigDeprecationsInfo; + deprecationsService.setup(deprecationsCoreSetupDeps); + expect(mockRegisterConfigDeprecationsInfo).toBeCalledTimes(1); + }); + }); + + describe('#registerConfigDeprecationsInfo', () => { + const deprecationsFactory = mockDeprecationsFactory.create(); + const deprecationsRegistry = mockDeprecationsRegistry.create(); + const getDeprecationsContext = mockDeprecationsRegistry.createGetDeprecationsContext(); + + it('registers config deprecations', () => { + const deprecationsService = new DeprecationsService(coreContext); + coreContext.configService.getHandledDeprecatedConfigs.mockReturnValue([ + [ + 'testDomain', + [ + { + message: 'testMessage', + documentationUrl: 'testDocUrl', + correctiveActions: { + manualSteps: [ + 'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.', + 'Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.', + ], + }, + }, + ], + ], + ]); + + deprecationsFactory.getRegistry.mockReturnValue(deprecationsRegistry); + deprecationsService['registerConfigDeprecationsInfo'](deprecationsFactory); + + expect(coreContext.configService.getHandledDeprecatedConfigs).toBeCalledTimes(1); + expect(deprecationsFactory.getRegistry).toBeCalledTimes(1); + expect(deprecationsFactory.getRegistry).toBeCalledWith('testDomain'); + expect(deprecationsRegistry.registerDeprecations).toBeCalledTimes(1); + const configDeprecations = deprecationsRegistry.registerDeprecations.mock.calls[0][0].getDeprecations( + getDeprecationsContext + ); + expect(configDeprecations).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", + "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.", + ], + }, + "deprecationType": "config", + "documentationUrl": "testDocUrl", + "level": "critical", + "message": "testMessage", + }, + ] + `); + }); + }); +}); diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index 8eca1ba5790c5..205dd964468c1 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -11,8 +11,6 @@ import { RegisterDeprecationsConfig } from './types'; import { registerRoutes } from './routes'; import { CoreContext } from '../core_context'; -import { CoreUsageDataSetup } from '../core_usage_data'; -import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { CoreService } from '../../types'; import { InternalHttpServiceSetup } from '../http'; import { Logger } from '../logging'; @@ -112,8 +110,6 @@ export interface InternalDeprecationsServiceSetup { /** @internal */ export interface DeprecationsSetupDeps { http: InternalHttpServiceSetup; - elasticsearch: InternalElasticsearchServiceSetup; - coreUsageData: CoreUsageDataSetup; } /** @internal */ @@ -156,6 +152,7 @@ export class DeprecationsService implements CoreService { return { level: 'critical', + deprecationType: 'config', message, correctiveActions: correctiveActions ?? {}, documentationUrl, diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts index 31734b51b46bd..50c947591fdf4 100644 --- a/src/core/server/deprecations/types.ts +++ b/src/core/server/deprecations/types.ts @@ -25,6 +25,16 @@ export interface DeprecationsDetails { * - fetch_error: Deprecations service failed to grab the deprecation details for the domain. */ level: 'warning' | 'critical' | 'fetch_error'; + /** + * (optional) Used to identify between different deprecation types. + * Example use case: in Upgrade Assistant, we may want to allow the user to sort by + * deprecation type or show each type in a separate tab. + * + * Feel free to add new types if necessary. + * Predefined types are necessary to reduce having similar definitions with different keywords + * across kibana deprecations. + */ + deprecationType?: 'config' | 'feature'; /* (optional) link to the documentation for more details on the deprecation. */ documentationUrl?: string; /* corrective action needed to fix this deprecation. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 7f108dbeb0086..0c35177f51f99 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -874,6 +874,7 @@ export interface DeprecationsDetails { }; manualSteps?: string[]; }; + deprecationType?: 'config' | 'feature'; // (undocumented) documentationUrl?: string; level: 'warning' | 'critical' | 'fetch_error'; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 4d99368f9bf70..a31b9a061ac5d 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -193,8 +193,6 @@ export class Server { const deprecationsSetup = this.deprecations.setup({ http: httpSetup, - elasticsearch: elasticsearchServiceSetup, - coreUsageData: coreUsageDataSetup, }); const coreSetup: InternalCoreSetup = { diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts index 38565b1e2c0a8..65a2ce02aa0a4 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -18,6 +18,7 @@ async function getDeprecations({ message: `CorePluginDeprecationsPlugin is a deprecated feature for testing.`, documentationUrl: 'test-url', level: 'warning', + deprecationType: 'feature', correctiveActions: { manualSteps: ['Step a', 'Step b'], }, diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index a78527d0d82e2..99b1a79fb51e3 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -17,7 +17,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const PageObjects = getPageObjects(['common']); const browser = getService('browser'); - const CorePluginDeprecationsPluginDeprecations = [ + const CorePluginDeprecationsPluginDeprecations: DomainDeprecationDetails[] = [ { level: 'critical', message: @@ -27,6 +27,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Replace "corePluginDeprecations.oldProperty" with "corePluginDeprecations.newProperty" in the Kibana config file, CLI flag, or environment variable (in Docker only).', ], }, + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -37,6 +38,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only)', ], }, + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -45,6 +47,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', correctiveActions: {}, documentationUrl: 'config-secret-doc-url', + deprecationType: 'config', domainId: 'corePluginDeprecations', }, { @@ -54,6 +57,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide correctiveActions: { manualSteps: ['Step a', 'Step b'], }, + deprecationType: 'feature', domainId: 'corePluginDeprecations', }, { From 69883de634bbb85af6a382a7b92f514778b7fb73 Mon Sep 17 00:00:00 2001 From: Dmitry Tomashevich <39378793+Dmitriynj@users.noreply.github.com> Date: Tue, 1 Jun 2021 14:29:01 +0300 Subject: [PATCH 14/46] [Discover] Add EUIDataGrid to surrounding documents (#99447) * [Discover] migrate remaining context files from js to ts * [Discover] get rid of any types * [Discover] replace constants with enums, update imports * [Discover] use unknown instead of any, correct types * [Discover] skip any type for tests * [Discover] add euiDataGrid view * [Discover] add support dataGrid columns, provide ability to do not change sorting, highlight anchor doc, rename legacy variables * [Discover] update context_legacy test and types * [Discover] update unit tests, add context header * [Discover] update unit and functional tests * [Discover] remove docTable from context test which uses new data grid * [Discover] update EsHitRecord type, use it for context app. add no pagination support * [Discover] resolve type error in test * [Discover] add disabling control columns option, change loading feedback * [Discover] clean up, update functional tests * [Discover] remove invalid translations * [Discover] support both no results found and loading feedback * [Discover] provide loading status for discover * [Discover] fix functional test * [Discover] add useDataGridColumns test, update by comments * [Discover] fix types Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../discover/public/__mocks__/ui_settings.ts | 4 +- .../public/application/angular/context.html | 5 +- .../public/application/angular/context.js | 29 ++-- .../angular/context/api/anchor.test.ts | 15 +- .../application/angular/context/api/anchor.ts | 12 +- .../context/api/context.predecessors.test.ts | 6 +- .../context/api/context.successors.test.ts | 6 +- .../angular/context/api/context.ts | 25 +-- .../api/utils/get_es_query_search_after.ts | 16 +- .../angular/context/query/actions.tsx | 9 +- .../context/query_parameters/actions.test.ts | 10 +- .../application/angular/context_app.html | 10 +- .../public/application/angular/context_app.js | 3 +- .../application/angular/context_app_state.ts | 6 +- .../application/angular/context_state.test.ts | 10 +- .../application/angular/context_state.ts | 8 +- .../application/angular/discover_legacy.html | 6 +- .../angular/doc_table/actions/columns.ts | 13 +- .../angular/doc_table/doc_table.html | 4 +- .../context_app/context_app_legacy.scss | 24 +++ .../context_app/context_app_legacy.test.tsx | 45 ++++-- .../context_app/context_app_legacy.tsx | 142 +++++++++++++----- .../context_app_legacy_directive.ts | 7 +- .../components/create_discover_directive.ts | 1 + .../application/components/discover.tsx | 44 +++--- .../discover_grid/discover_grid.scss | 1 + .../discover_grid/discover_grid.tsx | 87 ++++++++--- .../discover_grid_columns.test.tsx | 13 +- .../discover_grid/discover_grid_columns.tsx | 12 +- .../discover_grid_document_selection.test.tsx | 36 ++++- .../discover_grid_document_selection.tsx | 35 +++-- .../discover_grid_expand_button.tsx | 7 +- .../discover_grid/get_render_cell_value.tsx | 7 +- .../embeddable/search_embeddable.ts | 3 +- .../helpers/use_data_grid_columns.test.tsx | 67 +++++++++ .../helpers/use_data_grid_columns.ts | 70 +++++++++ .../apps/discover/_data_grid_context.ts | 12 +- test/functional/services/data_grid.ts | 12 +- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 40 files changed, 599 insertions(+), 225 deletions(-) create mode 100644 src/plugins/discover/public/application/components/context_app/context_app_legacy.scss create mode 100644 src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx create mode 100644 src/plugins/discover/public/application/helpers/use_data_grid_columns.ts diff --git a/src/plugins/discover/public/__mocks__/ui_settings.ts b/src/plugins/discover/public/__mocks__/ui_settings.ts index e021a39a568e9..8347ff18edd7d 100644 --- a/src/plugins/discover/public/__mocks__/ui_settings.ts +++ b/src/plugins/discover/public/__mocks__/ui_settings.ts @@ -7,7 +7,7 @@ */ import { IUiSettingsClient } from 'kibana/public'; -import { DEFAULT_COLUMNS_SETTING, SAMPLE_SIZE_SETTING } from '../../common'; +import { DEFAULT_COLUMNS_SETTING, DOC_TABLE_LEGACY, SAMPLE_SIZE_SETTING } from '../../common'; export const uiSettingsMock = ({ get: (key: string) => { @@ -15,6 +15,8 @@ export const uiSettingsMock = ({ return 10; } else if (key === DEFAULT_COLUMNS_SETTING) { return ['default_column']; + } else if (key === DOC_TABLE_LEGACY) { + return true; } }, } as unknown) as IUiSettingsClient; diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html index 2c8e9a2a5d6f0..adafb3a62275f 100644 --- a/src/plugins/discover/public/application/angular/context.html +++ b/src/plugins/discover/public/application/angular/context.html @@ -2,8 +2,9 @@ anchor-id="contextAppRoute.anchorId" columns="contextAppRoute.state.columns" index-pattern="contextAppRoute.indexPattern" + app-state="contextAppRoute.state" + state-container="contextAppRoute.stateContainer" filters="contextAppRoute.filters" predecessor-count="contextAppRoute.state.predecessorCount" successor-count="contextAppRoute.state.successorCount" - sort="contextAppRoute.state.sort" -> + sort="contextAppRoute.state.sort"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js index 01a28a5c174b6..10c0fe9db1950 100644 --- a/src/plugins/discover/public/application/angular/context.js +++ b/src/plugins/discover/public/application/angular/context.js @@ -15,19 +15,12 @@ import { getState } from './context_state'; import contextAppRouteTemplate from './context.html'; import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; -const k7Breadcrumbs = ($route) => { - const { indexPattern } = $route.current.locals; - const { id } = $route.current.params; - +const k7Breadcrumbs = () => { return [ ...getRootBreadcrumbs(), { text: i18n.translate('discover.context.breadcrumb', { - defaultMessage: 'Context of {indexPatternTitle}#{docId}', - values: { - indexPatternTitle: indexPattern.title, - docId: id, - }, + defaultMessage: 'Surrounding documents', }), }, ]; @@ -51,6 +44,14 @@ getAngularModule().config(($routeProvider) => { function ContextAppRouteController($routeParams, $scope, $route) { const filterManager = getServices().filterManager; const indexPattern = $route.current.locals.indexPattern.ip; + const stateContainer = getState({ + defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), + timeFieldName: indexPattern.timeFieldName, + storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), + history: getServices().history(), + toasts: getServices().core.notifications.toasts, + uiSettings: getServices().core.uiSettings, + }); const { startSync: startStateSync, stopSync: stopStateSync, @@ -59,14 +60,8 @@ function ContextAppRouteController($routeParams, $scope, $route) { setFilters, setAppState, flushToUrl, - } = getState({ - defaultStepSize: getServices().uiSettings.get(CONTEXT_DEFAULT_SIZE_SETTING), - timeFieldName: indexPattern.timeFieldName, - storeInSessionStorage: getServices().uiSettings.get('state:storeInSessionStorage'), - history: getServices().history(), - toasts: getServices().core.notifications.toasts, - uiSettings: getServices().core.uiSettings, - }); + } = stateContainer; + this.stateContainer = stateContainer; this.state = { ...appState.getState() }; this.anchorId = $routeParams.id; this.indexPattern = indexPattern; diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts index 62c9a2a5e3b90..4da8ddc798003 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.test.ts @@ -8,22 +8,21 @@ import { EsQuerySortValue, SortDirection } from '../../../../../../data/public'; import { createIndexPatternsStub, createSearchSourceStub } from './_stubs'; -import { AnchorHitRecord, fetchAnchorProvider } from './anchor'; +import { fetchAnchorProvider } from './anchor'; +import { EsHitRecord, EsHitRecordList } from './context'; describe('context app', function () { let fetchAnchor: ( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ) => Promise; + ) => Promise; // eslint-disable-next-line @typescript-eslint/no-explicit-any let searchSourceStub: any; describe('function fetchAnchor', function () { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub); }); @@ -139,16 +138,14 @@ describe('context app', function () { { _doc: SortDirection.desc }, ]).then((anchorDocument) => { expect(anchorDocument).toHaveProperty('property1', 'value1'); - expect(anchorDocument).toHaveProperty('$$_isAnchor', true); + expect(anchorDocument).toHaveProperty('isAnchor', true); }); }); }); describe('useNewFields API', () => { beforeEach(() => { - searchSourceStub = createSearchSourceStub([ - { _id: 'hit1', fields: [], sort: [], _source: {} }, - ]); + searchSourceStub = createSearchSourceStub(([{ _id: 'hit1' }] as unknown) as EsHitRecordList); fetchAnchor = fetchAnchorProvider(createIndexPatternsStub(), searchSourceStub, true); }); diff --git a/src/plugins/discover/public/application/angular/context/api/anchor.ts b/src/plugins/discover/public/application/angular/context/api/anchor.ts index da81ce525331a..f2111d020aade 100644 --- a/src/plugins/discover/public/application/angular/context/api/anchor.ts +++ b/src/plugins/discover/public/application/angular/context/api/anchor.ts @@ -16,11 +16,6 @@ import { } from '../../../../../../data/public'; import { EsHitRecord } from './context'; -export interface AnchorHitRecord extends EsHitRecord { - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: boolean; -} - export function fetchAnchorProvider( indexPatterns: IndexPatternsContract, searchSource: ISearchSource, @@ -30,7 +25,7 @@ export function fetchAnchorProvider( indexPatternId: string, anchorId: string, sort: EsQuerySortValue[] - ): Promise { + ): Promise { const indexPattern = await indexPatterns.get(indexPatternId); searchSource .setParent(undefined) @@ -66,8 +61,7 @@ export function fetchAnchorProvider( return { ...get(response, ['hits', 'hits', 0]), - // eslint-disable-next-line @typescript-eslint/naming-convention - $$_isAnchor: true, - } as AnchorHitRecord; + isAnchor: true, + } as EsHitRecord; }; } diff --git a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts index dc097bc110e20..1acf57411c795 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.predecessors.test.ts @@ -11,7 +11,7 @@ import { get, last } from 'lodash'; import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs'; import { EsHitRecordList, fetchContextProvider } from './context'; import { setServices, SortDirection } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { Query } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -267,7 +267,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'predecessors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, diff --git a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts index f8fc7eb343206..957a13e8daf09 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.successors.test.ts @@ -13,7 +13,7 @@ import { createIndexPatternsStub, createContextSearchSourceStub } from './_stubs import { setServices, SortDirection } from '../../../../kibana_services'; import { Query } from '../../../../../../data/public'; import { EsHitRecordList, fetchContextProvider } from './context'; -import { AnchorHitRecord } from './anchor'; +import { EsHitRecord } from './context'; import { DiscoverServices } from '../../../../build_services'; const MS_PER_DAY = 24 * 60 * 60 * 1000; @@ -75,7 +75,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub()).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, @@ -270,7 +270,7 @@ describe('context app', function () { return fetchContextProvider(createIndexPatternsStub(), true).fetchSurroundingDocs( 'successors', indexPatternId, - anchor as AnchorHitRecord, + anchor as EsHitRecord, timeField, tieBreakerField, sortDir, diff --git a/src/plugins/discover/public/application/angular/context/api/context.ts b/src/plugins/discover/public/application/angular/context/api/context.ts index 4309b9ca4c391..cd81ca7b216b2 100644 --- a/src/plugins/discover/public/application/angular/context/api/context.ts +++ b/src/plugins/discover/public/application/angular/context/api/context.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import type { estypes } from '@elastic/elasticsearch'; import { Filter, IndexPatternsContract, IndexPattern } from 'src/plugins/data/public'; import { reverseSortDir, SortDirection } from './utils/sorting'; import { extractNanos, convertIsoToMillis } from './utils/date_conversion'; @@ -14,17 +15,19 @@ import { generateIntervals } from './utils/generate_intervals'; import { getEsQuerySearchAfter } from './utils/get_es_query_search_after'; import { getEsQuerySort } from './utils/get_es_query_sort'; import { getServices } from '../../../../kibana_services'; -import { AnchorHitRecord } from './anchor'; export type SurrDocType = 'successors' | 'predecessors'; -export interface EsHitRecord { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - fields: Record; - sort: number[]; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - _source: Record; - _id: string; -} +export type EsHitRecord = Required< + Pick< + estypes.SearchResponse['hits']['hits'][number], + '_id' | 'fields' | 'sort' | '_index' | '_version' + > +> & { + _source?: Record; + _score?: number; + isAnchor?: boolean; +}; + export type EsHitRecordList = EsHitRecord[]; const DAY_MILLIS = 24 * 60 * 60 * 1000; @@ -53,7 +56,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields async function fetchSurroundingDocs( type: SurrDocType, indexPatternId: string, - anchor: AnchorHitRecord, + anchor: EsHitRecord, timeField: string, tieBreakerField: string, sortDir: SortDirection, @@ -71,7 +74,7 @@ function fetchContextProvider(indexPatterns: IndexPatternsContract, useNewFields const timeValueMillis = nanos !== '' ? convertIsoToMillis(anchor.fields[timeField][0]) : anchor.sort[0]; - const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis, type, sortDir); + const intervals = generateIntervals(LOOKUP_OFFSETS, timeValueMillis as number, type, sortDir); let documents: EsHitRecordList = []; for (const interval of intervals) { diff --git a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts index fb0e58832a202..c703abaf2e523 100644 --- a/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts +++ b/src/plugins/discover/public/application/angular/context/api/utils/get_es_query_search_after.ts @@ -28,23 +28,23 @@ export function getEsQuerySearchAfter( // already surrounding docs -> first or last record is used const afterTimeRecIdx = type === 'successors' && documents.length ? documents.length - 1 : 0; const afterTimeDoc = documents[afterTimeRecIdx]; - let afterTimeValue: string | number = afterTimeDoc.sort[0]; + let afterTimeValue = afterTimeDoc.sort[0] as string | number; if (nanoSeconds) { afterTimeValue = useNewFieldsApi - ? (afterTimeDoc.fields[timeFieldName] as Array)[0] - : (afterTimeDoc._source[timeFieldName] as string | number); + ? afterTimeDoc.fields[timeFieldName][0] + : afterTimeDoc._source?.[timeFieldName]; } - return [afterTimeValue, afterTimeDoc.sort[1]]; + return [afterTimeValue, afterTimeDoc.sort[1] as string | number]; } // if data_nanos adapt timestamp value for sorting, since numeric value was rounded by browser // ES search_after also works when number is provided as string const searchAfter = new Array(2) as EsQuerySearchAfter; - searchAfter[0] = anchor.sort[0]; + searchAfter[0] = anchor.sort[0] as string | number; if (nanoSeconds) { searchAfter[0] = useNewFieldsApi - ? (anchor.fields[timeFieldName] as Array)[0] - : (anchor._source[timeFieldName] as string | number); + ? anchor.fields[timeFieldName][0] + : anchor._source?.[timeFieldName]; } - searchAfter[1] = anchor.sort[1]; + searchAfter[1] = anchor.sort[1] as string | number; return searchAfter; } diff --git a/src/plugins/discover/public/application/angular/context/query/actions.tsx b/src/plugins/discover/public/application/angular/context/query/actions.tsx index 52c56d379d259..f79c28bf6a120 100644 --- a/src/plugins/discover/public/application/angular/context/query/actions.tsx +++ b/src/plugins/discover/public/application/angular/context/query/actions.tsx @@ -13,7 +13,7 @@ import { i18n } from '@kbn/i18n'; import { getServices } from '../../../../kibana_services'; import { SEARCH_FIELDS_FROM_SOURCE } from '../../../../../common'; import { MarkdownSimple, toMountPoint } from '../../../../../../kibana_react/public'; -import { AnchorHitRecord, fetchAnchorProvider } from '../api/anchor'; +import { fetchAnchorProvider } from '../api/anchor'; import { EsHitRecord, EsHitRecordList, fetchContextProvider, SurrDocType } from '../api/context'; import { getQueryParameterActions } from '../query_parameters'; import { @@ -77,11 +77,12 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)('anchor'); + const [[, sortDir]] = sort; return Promise.try(() => - fetchAnchor(indexPatternId, anchorId, [fromPairs([sort]), { [tieBreakerField]: sort[1] }]) + fetchAnchor(indexPatternId, anchorId, [fromPairs(sort), { [tieBreakerField]: sortDir }]) ).then( - (anchorDocument: AnchorHitRecord) => { + (anchorDocument: EsHitRecord) => { setLoadedStatus(state)('anchor'); state.rows.anchor = anchorDocument; return anchorDocument; @@ -120,7 +121,7 @@ export function QueryActionsProvider(Promise: DiscoverPromise) { } setLoadingStatus(state)(type); - const [sortField, sortDir] = sort; + const [[sortField, sortDir]] = sort; return Promise.try(() => fetchSurroundingDocs( diff --git a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts index b54f11e9e6706..fac3e1ea6fad6 100644 --- a/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts +++ b/src/plugins/discover/public/application/angular/context/query_parameters/actions.test.ts @@ -10,6 +10,7 @@ import { getQueryParameterActions } from './actions'; import { FilterManager, SortDirection } from '../../../../../../data/public'; import { coreMock } from '../../../../../../../core/public/mocks'; import { ContextAppState, LoadingStatus, QueryParameters } from '../../context_app_state'; +import { EsHitRecord } from '../api/context'; const setupMock = coreMock.createSetup(); let state: ContextAppState; @@ -29,7 +30,7 @@ beforeEach(() => { anchorId: '', columns: [], filters: [], - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }, loadingStatus: { @@ -39,8 +40,7 @@ beforeEach(() => { }, rows: { all: [], - // eslint-disable-next-line @typescript-eslint/naming-convention - anchor: { $$_isAnchor: true, fields: [], sort: [], _source: [], _id: '' }, + anchor: ({ isAnchor: true, fields: [], sort: [], _id: '' } as unknown) as EsHitRecord, predecessors: [], successors: [], }, @@ -129,7 +129,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); @@ -142,7 +142,7 @@ describe('context query_parameter actions', function () { indexPatternId: 'INDEX_PATTERN', predecessorCount: 100, successorCount: 100, - sort: ['field', SortDirection.asc], + sort: [['field', SortDirection.asc]], tieBreakerField: '', }); }); diff --git a/src/plugins/discover/public/application/angular/context_app.html b/src/plugins/discover/public/application/angular/context_app.html index 3d731459ad8d7..21aad2688d2a3 100644 --- a/src/plugins/discover/public/application/angular/context_app.html +++ b/src/plugins/discover/public/application/angular/context_app.html @@ -3,11 +3,14 @@ filter="contextApp.actions.addFilter" hits="contextApp.state.rows.all" index-pattern="contextApp.indexPattern" + app-state="contextApp.appState" + state-container="contextApp.stateContainer" sorting="contextApp.state.queryParameters.sort" columns="contextApp.state.queryParameters.columns" minimum-visible-rows="contextApp.state.rows.all.length" - status="contextApp.state.loadingStatus.anchor.status" - reason="contextApp.state.loadingStatus.anchor.reason" + anchor-id="contextApp.anchorId" + anchor-status="contextApp.state.loadingStatus.anchor.status" + anchor-reason="contextApp.state.loadingStatus.anchor.reason" default-step-size="contextApp.state.queryParameters.defaultStepSize" predecessor-count="contextApp.state.queryParameters.predecessorCount" predecessor-available="contextApp.state.rows.predecessors.length" @@ -18,5 +21,4 @@ successor-status="contextApp.state.loadingStatus.successors.status" on-change-successor-count="contextApp.actions.fetchGivenSuccessorRows" use-new-fields-api="contextApp.state.useNewFieldsApi" - top-nav-menu="contextApp.topNavMenu" -> + top-nav-menu="contextApp.topNavMenu"> \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/context_app.js b/src/plugins/discover/public/application/angular/context_app.js index a90904fa2ccea..7c9c5f8ce4b42 100644 --- a/src/plugins/discover/public/application/angular/context_app.js +++ b/src/plugins/discover/public/application/angular/context_app.js @@ -34,6 +34,8 @@ getAngularModule().directive('contextApp', function ContextApp() { anchorId: '=', columns: '=', indexPattern: '=', + appState: '=', + stateContainer: '=', filters: '=', predecessorCount: '=', successorCount: '=', @@ -55,7 +57,6 @@ function ContextAppController($scope, Private) { ); this.state.useNewFieldsApi = useNewFieldsApi; this.topNavMenu = navigation.ui.TopNavMenu; - this.actions = _.mapValues( { ...queryParameterActions, diff --git a/src/plugins/discover/public/application/angular/context_app_state.ts b/src/plugins/discover/public/application/angular/context_app_state.ts index 1593b2457019c..0d9d6d6ea5978 100644 --- a/src/plugins/discover/public/application/angular/context_app_state.ts +++ b/src/plugins/discover/public/application/angular/context_app_state.ts @@ -7,7 +7,7 @@ */ import { Filter } from '../../../../data/public'; -import { AnchorHitRecord } from './context/api/anchor'; +import { EsHitRecord } from './context/api/context'; import { EsHitRecordList } from './context/api/context'; import { SortDirection } from './context/api/utils/sorting'; @@ -48,13 +48,13 @@ export interface QueryParameters { indexPatternId: string; predecessorCount: number; successorCount: number; - sort: [string, SortDirection]; + sort: Array<[string, SortDirection]>; tieBreakerField: string; } interface ContextRows { all: EsHitRecordList; - anchor: AnchorHitRecord; + anchor: EsHitRecord; predecessors: EsHitRecordList; successors: EsHitRecordList; } diff --git a/src/plugins/discover/public/application/angular/context_state.test.ts b/src/plugins/discover/public/application/angular/context_state.test.ts index ed4a74c70112b..e9294567032c4 100644 --- a/src/plugins/discover/public/application/angular/context_state.test.ts +++ b/src/plugins/discover/public/application/angular/context_state.test.ts @@ -45,8 +45,10 @@ describe('Test Discover Context State', () => { "filters": Array [], "predecessorCount": 4, "sort": Array [ - "time", - "desc", + Array [ + "time", + "desc", + ], ], "successorCount": 4, } @@ -60,7 +62,7 @@ describe('Test Discover Context State', () => { state.setAppState({ predecessorCount: 10 }); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(time,desc),successorCount:4)"` + `"/#?_a=(columns:!(_source),filters:!(),predecessorCount:10,sort:!(!(time,desc)),successorCount:4)"` ); }); test('getState -> url to appState syncing', async () => { @@ -183,7 +185,7 @@ describe('Test Discover Context State', () => { `); state.flushToUrl(); expect(getCurrentUrl()).toMatchInlineSnapshot( - `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(time,desc),successorCount:4)"` + `"/#?_g=(filters:!(('$state':(store:globalState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!f,params:(query:jpg),type:phrase),query:(match:(extension:(query:jpg,type:phrase))))))&_a=(columns:!(_source),filters:!(('$state':(store:appState),meta:(alias:!n,disabled:!f,index:'logstash-*',key:extension,negate:!t,params:(query:png),type:phrase),query:(match:(extension:(query:png,type:phrase))))),predecessorCount:4,sort:!(!(time,desc)),successorCount:4)"` ); }); }); diff --git a/src/plugins/discover/public/application/angular/context_state.ts b/src/plugins/discover/public/application/angular/context_state.ts index d60f2e655c4eb..9cfea7f01e4ab 100644 --- a/src/plugins/discover/public/application/angular/context_state.ts +++ b/src/plugins/discover/public/application/angular/context_state.ts @@ -35,7 +35,7 @@ export interface AppState { /** * Sorting of the records to be fetched, assumed to be a legacy parameter */ - sort: string[]; + sort: string[][]; /** * Number of records to be fetched after the anchor records (older records) */ @@ -50,7 +50,7 @@ interface GlobalState { filters: Filter[]; } -interface GetStateParams { +export interface GetStateParams { /** * Number of records to be fetched when 'Load' link/button is clicked */ @@ -81,7 +81,7 @@ interface GetStateParams { uiSettings: IUiSettingsClient; } -interface GetStateReturn { +export interface GetStateReturn { /** * Global state, the _g part of the URL */ @@ -276,7 +276,7 @@ function createInitialAppState( columns: ['_source'], filters: [], predecessorCount: parseInt(defaultSize, 10), - sort: [timeFieldName, 'desc'], + sort: [[timeFieldName, 'desc']], successorCount: parseInt(defaultSize, 10), }; if (typeof urlState !== 'object') { diff --git a/src/plugins/discover/public/application/angular/discover_legacy.html b/src/plugins/discover/public/application/angular/discover_legacy.html index fadaffde5c5c3..fa3656d1529d2 100644 --- a/src/plugins/discover/public/application/angular/discover_legacy.html +++ b/src/plugins/discover/public/application/angular/discover_legacy.html @@ -10,13 +10,13 @@ opts="opts" reset-query="resetQuery" result-state="resultState" + fetch-status="fetchStatus" rows="rows" search-source="volatileSearchSource" state="state" top-nav-menu="topNavMenu" use-new-fields-api="useNewFieldsApi" unmapped-fields-config="unmappedFieldsConfig" - refresh-app-state="refreshAppState" - > + refresh-app-state="refreshAppState"> - + \ No newline at end of file diff --git a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts index 8028aa6c08634..0907844aa1c54 100644 --- a/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts +++ b/src/plugins/discover/public/application/angular/doc_table/actions/columns.ts @@ -8,7 +8,14 @@ import { Capabilities, IUiSettingsClient } from 'kibana/public'; import { popularizeField } from '../../../helpers/popularize_field'; import { IndexPattern, IndexPatternsContract } from '../../../../kibana_services'; -import { AppState } from '../../discover_state'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../../discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../../context_state'; import { SORT_DEFAULT_ORDER_SETTING } from '../../../../../common'; /** @@ -67,8 +74,8 @@ export function getStateColumnActions({ indexPattern: IndexPattern; indexPatterns: IndexPatternsContract; useNewFieldsApi: boolean; - setAppState: (state: Partial) => void; - state: AppState; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; }) { function onAddColumn(columnName: string) { if (capabilities.discover.save) { diff --git a/src/plugins/discover/public/application/angular/doc_table/doc_table.html b/src/plugins/discover/public/application/angular/doc_table/doc_table.html index 4f297643a28f7..ecd7aa8f3dcf4 100644 --- a/src/plugins/discover/public/application/angular/doc_table/doc_table.html +++ b/src/plugins/discover/public/application/angular/doc_table/doc_table.html @@ -95,8 +95,8 @@ index-pattern="indexPattern" filter="filter" class="kbnDocTable__row" - ng-class="{'kbnDocTable__row--highlight': row['$$_isAnchor']}" - data-test-subj="docTableRow{{ row['$$_isAnchor'] ? ' docTableAnchorRow' : ''}}" + ng-class="{'kbnDocTable__row--highlight': row['isAnchor']}" + data-test-subj="docTableRow{{ row['isAnchor'] ? ' docTableAnchorRow' : ''}}" on-add-column="onAddColumn" on-remove-column="onRemoveColumn" use-new-fields-api="useNewFieldsApi" diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss new file mode 100644 index 0000000000000..9ff36ca452742 --- /dev/null +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.scss @@ -0,0 +1,24 @@ +@import '../../../../../../core/public/mixins'; + +.dscDocsPage { + @include kibanaFullBodyHeight(54px); // action bar height +} + +.dscDocsContent { + display: flex; + flex-direction: column; + height: 100%; +} + +.dscDocsGrid { + flex: 1 1 100%; + overflow: auto; + + &__cell--highlight { + background-color: tintOrShade($euiColorPrimary, 90%, 70%); + } + + .euiDataGridRowCell.euiDataGridRowCell--firstColumn { + padding: 0; + } +} diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx index 63845ab97b954..7d947d8412be5 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.test.tsx @@ -7,14 +7,34 @@ */ import React from 'react'; -import { ContextAppLegacy } from './context_app_legacy'; -import { IIndexPattern } from '../../../../../data/common/index_patterns'; import { mountWithIntl } from '@kbn/test/jest'; +import { uiSettingsMock as mockUiSettings } from '../../../__mocks__/ui_settings'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; +import { ContextAppLegacy } from './context_app_legacy'; import { DocTableLegacy } from '../../angular/doc_table/create_doc_table_react'; import { findTestSubject } from '@elastic/eui/lib/test'; import { ActionBar } from '../../angular/context/components/action_bar/action_bar'; import { ContextErrorMessage } from '../context_error_message'; import { TopNavMenuMock } from './__mocks__/top_nav_menu'; +import { AppState, GetStateReturn } from '../../angular/context_state'; +import { SortDirection } from 'src/plugins/data/common'; +import { EsHitRecordList } from '../../angular/context/api/context'; + +jest.mock('../../../kibana_services', () => { + return { + getServices: () => ({ + metadata: { + branch: 'test', + }, + capabilities: { + discover: { + save: true, + }, + }, + uiSettings: mockUiSettings, + }), + }; +}); describe('ContextAppLegacy test', () => { const hit = { @@ -35,16 +55,19 @@ describe('ContextAppLegacy test', () => { }; const indexPattern = { id: 'test_index_pattern', - } as IIndexPattern; + } as IndexPattern; const defaultProps = { columns: ['_source'], filter: () => {}, - hits: [hit], - sorting: ['order_date', 'desc'], + hits: ([hit] as unknown) as EsHitRecordList, + sorting: [['order_date', 'desc']] as Array<[string, SortDirection]>, minimumVisibleRows: 5, indexPattern, - status: 'loaded', - reason: 'no reason', + appState: ({} as unknown) as AppState, + stateContainer: ({} as unknown) as GetStateReturn, + anchorId: 'test_anchor_id', + anchorStatus: 'loaded', + anchorReason: 'no reason', defaultStepSize: 5, predecessorCount: 10, successorCount: 10, @@ -55,6 +78,8 @@ describe('ContextAppLegacy test', () => { predecessorStatus: 'loaded', successorStatus: 'loaded', topNavMenu: TopNavMenuMock, + useNewFieldsApi: false, + isPaginationEnabled: false, }; const topNavProps = { appName: 'context', @@ -80,7 +105,7 @@ describe('ContextAppLegacy test', () => { it('renders loading indicator', () => { const props = { ...defaultProps }; - props.status = 'loading'; + props.anchorStatus = 'loading'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); const loadingIndicator = findTestSubject(component, 'contextApp_loadingIndicator'); @@ -91,8 +116,8 @@ describe('ContextAppLegacy test', () => { it('renders error message', () => { const props = { ...defaultProps }; - props.status = 'failed'; - props.reason = 'something went wrong'; + props.anchorStatus = 'failed'; + props.anchorReason = 'something went wrong'; const component = mountWithIntl(); expect(component.find(DocTableLegacy).length).toBe(0); expect(component.find(TopNavMenuMock).length).toBe(0); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx index 55c2208105f13..1251687805af1 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy.tsx @@ -6,29 +6,43 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useState, Fragment } from 'react'; +import classNames from 'classnames'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage } from '@elastic/eui'; +import './context_app_legacy.scss'; +import { EuiHorizontalRule, EuiText, EuiPageContent, EuiPage, EuiSpacer } from '@elastic/eui'; +import { DOC_HIDE_TIME_COLUMN_SETTING, DOC_TABLE_LEGACY } from '../../../../common'; import { ContextErrorMessage } from '../context_error_message'; import { DocTableLegacy, DocTableLegacyProps, } from '../../angular/doc_table/create_doc_table_react'; -import { IIndexPattern, IndexPatternField } from '../../../../../data/common/index_patterns'; +import { IndexPattern } from '../../../../../data/common/index_patterns'; import { LoadingStatus } from '../../angular/context_app_state'; import { ActionBar, ActionBarProps } from '../../angular/context/components/action_bar/action_bar'; import { TopNavMenuProps } from '../../../../../navigation/public'; +import { DiscoverGrid, DiscoverGridProps } from '../discover_grid/discover_grid'; +import { DocViewFilterFn } from '../../doc_views/doc_views_types'; +import { getServices, SortDirection } from '../../../kibana_services'; +import { GetStateReturn, AppState } from '../../angular/context_state'; +import { useDataGridColumns } from '../../helpers/use_data_grid_columns'; +import { EsHitRecord, EsHitRecordList } from '../../angular/context/api/context'; export interface ContextAppProps { topNavMenu: React.ComponentType; columns: string[]; - hits: Array>; - indexPattern: IIndexPattern; - filter: (field: IndexPatternField | string, value: string, type: '+' | '-') => void; + hits: EsHitRecordList; + indexPattern: IndexPattern; + appState: AppState; + stateContainer: GetStateReturn; + filter: DocViewFilterFn; minimumVisibleRows: number; - sorting: string[]; - status: string; - reason: string; + sorting: Array<[string, SortDirection]>; + anchorId: string; + anchorStatus: string; + anchorReason: string; + predecessorStatus: string; + successorStatus: string; defaultStepSize: number; predecessorCount: number; successorCount: number; @@ -36,11 +50,10 @@ export interface ContextAppProps { successorAvailable: number; onChangePredecessorCount: (count: number) => void; onChangeSuccessorCount: (count: number) => void; - predecessorStatus: string; - successorStatus: string; useNewFieldsApi?: boolean; } +const DataGridMemoized = React.memo(DiscoverGrid); const PREDECESSOR_TYPE = 'predecessors'; const SUCCESSOR_TYPE = 'successors'; @@ -49,9 +62,36 @@ function isLoading(status: string) { } export function ContextAppLegacy(renderProps: ContextAppProps) { - const status = renderProps.status; - const isLoaded = status === LoadingStatus.LOADED; - const isFailed = status === LoadingStatus.FAILED; + const services = getServices(); + const { uiSettings: config, capabilities, indexPatterns } = services; + const { + indexPattern, + anchorId, + anchorStatus, + predecessorStatus, + successorStatus, + appState, + stateContainer, + hits: rows, + sorting, + filter, + minimumVisibleRows, + useNewFieldsApi, + } = renderProps; + const [expandedDoc, setExpandedDoc] = useState(undefined); + const isAnchorLoaded = anchorStatus === LoadingStatus.LOADED; + const isFailed = anchorStatus === LoadingStatus.FAILED; + const isLegacy = config.get(DOC_TABLE_LEGACY); + + const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState: stateContainer.setAppState, + state: appState, + useNewFieldsApi: !!useNewFieldsApi, + }); const actionBarProps = (type: string) => { const { @@ -60,8 +100,6 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { predecessorCount, predecessorAvailable, successorAvailable, - predecessorStatus, - successorStatus, onChangePredecessorCount, onChangeSuccessorCount, } = renderProps; @@ -73,27 +111,44 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { onChangeCount: isPredecessorType ? onChangePredecessorCount : onChangeSuccessorCount, isLoading: isPredecessorType ? isLoading(predecessorStatus) : isLoading(successorStatus), type, - isDisabled: !isLoaded, + isDisabled: !isAnchorLoaded, } as ActionBarProps; }; const docTableProps = () => { - const { - hits, - filter, - sorting, + return { + ariaLabelledBy: 'surDocumentsAriaLabel', columns, + rows, indexPattern, - minimumVisibleRows, + expandedDoc, + isLoading: isLoading(anchorStatus), + sampleSize: 0, + sort: sorting, + isSortEnabled: false, + showTimeCol: !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName, + services, useNewFieldsApi, - } = renderProps; + isPaginationEnabled: false, + controlColumnIds: ['openDetails'], + setExpandedDoc, + onFilter: filter, + onAddColumn, + onRemoveColumn, + onSetColumns, + } as DiscoverGridProps; + }; + + const legacyDocTableProps = () => { // @ts-expect-error doesn't implement full DocTableLegacyProps interface return { columns, indexPattern, minimumVisibleRows, - rows: hits, + rows, onFilter: filter, + onAddColumn, + onRemoveColumn, sort: sorting.map((el) => [el]), useNewFieldsApi, } as DocTableLegacyProps; @@ -114,7 +169,7 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { }; const loadingFeedback = () => { - if (status === LoadingStatus.UNINITIALIZED || status === LoadingStatus.LOADING) { + if (anchorStatus === LoadingStatus.UNINITIALIZED || anchorStatus === LoadingStatus.LOADING) { return ( @@ -127,25 +182,42 @@ export function ContextAppLegacy(renderProps: ContextAppProps) { return ( {isFailed ? ( - + ) : ( -
+ - - + + + + + + + + + - {loadingFeedback()} + {isLegacy && loadingFeedback()} - {isLoaded ? ( -
- + {isLegacy ? ( + isAnchorLoaded && ( +
+ +
+ ) + ) : ( +
+
- ) : null} + )} -
+
)} ); diff --git a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts index fc64abfb51025..767ab8c94d80f 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts +++ b/src/plugins/discover/public/application/components/context_app/context_app_legacy_directive.ts @@ -14,11 +14,14 @@ export function createContextAppLegacy(reactDirective: any) { ['filter', { watchDepth: 'reference' }], ['hits', { watchDepth: 'reference' }], ['indexPattern', { watchDepth: 'reference' }], + ['appState', { watchDepth: 'reference' }], + ['stateContainer', { watchDepth: 'reference' }], ['sorting', { watchDepth: 'reference' }], ['columns', { watchDepth: 'collection' }], ['minimumVisibleRows', { watchDepth: 'reference' }], - ['status', { watchDepth: 'reference' }], - ['reason', { watchDepth: 'reference' }], + ['anchorId', { watchDepth: 'reference' }], + ['anchorStatus', { watchDepth: 'reference' }], + ['anchorReason', { watchDepth: 'reference' }], ['defaultStepSize', { watchDepth: 'reference' }], ['predecessorCount', { watchDepth: 'reference' }], ['predecessorAvailable', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/create_discover_directive.ts b/src/plugins/discover/public/application/components/create_discover_directive.ts index f8c74c07457aa..049c9ac177eea 100644 --- a/src/plugins/discover/public/application/components/create_discover_directive.ts +++ b/src/plugins/discover/public/application/components/create_discover_directive.ts @@ -20,6 +20,7 @@ export function createDiscoverDirective(reactDirective: any) { ['opts', { watchDepth: 'reference' }], ['resetQuery', { watchDepth: 'reference' }], ['resultState', { watchDepth: 'reference' }], + ['fetchStatus', { watchDepth: 'reference' }], ['rows', { watchDepth: 'reference' }], ['savedSearch', { watchDepth: 'reference' }], ['searchSource', { watchDepth: 'reference' }], diff --git a/src/plugins/discover/public/application/components/discover.tsx b/src/plugins/discover/public/application/components/discover.tsx index 90dfd2ef9dce9..f962c56cc4690 100644 --- a/src/plugins/discover/public/application/components/discover.tsx +++ b/src/plugins/discover/public/application/components/discover.tsx @@ -34,9 +34,12 @@ import { esFilters, IndexPatternField, search } from '../../../../data/public'; import { DiscoverSidebarResponsive } from './sidebar'; import { DiscoverProps } from './types'; import { SortPairArr } from '../angular/doc_table/lib/get_sort'; -import { SEARCH_FIELDS_FROM_SOURCE } from '../../../common'; +import { + DOC_HIDE_TIME_COLUMN_SETTING, + DOC_TABLE_LEGACY, + SEARCH_FIELDS_FROM_SOURCE, +} from '../../../common'; import { popularizeField } from '../helpers/popularize_field'; -import { getStateColumnActions } from '../angular/doc_table/actions/columns'; import { DocViewFilterFn } from '../doc_views/doc_views_types'; import { DiscoverGrid } from './discover_grid/discover_grid'; import { DiscoverTopNav } from './discover_topnav'; @@ -44,6 +47,7 @@ import { ElasticSearchHit } from '../doc_views/doc_views_types'; import { setBreadcrumbsTitle } from '../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from './help_menu/help_menu_util'; import { InspectorSession } from '../../../../inspector/public'; +import { useDataGridColumns } from '../helpers/use_data_grid_columns'; const DocTableLegacyMemoized = React.memo(DocTableLegacy); const SidebarMemoized = React.memo(DiscoverSidebarResponsive); @@ -96,7 +100,7 @@ export function Discover({ }, [opts.chartAggConfigs]); const contentCentered = resultState === 'uninitialized'; - const isLegacy = services.uiSettings.get('doc_table:legacy'); + const isLegacy = services.uiSettings.get(DOC_TABLE_LEGACY); const useNewFieldsApi = !services.uiSettings.get(SEARCH_FIELDS_FROM_SOURCE); const updateQuery = useCallback( (_payload, isUpdate?: boolean) => { @@ -108,6 +112,16 @@ export function Discover({ [opts] ); + const { columns, onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useDataGridColumns({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }); + useEffect(() => { const pageTitleSuffix = savedSearch.id && savedSearch.title ? `: ${savedSearch.title}` : ''; chrome.docTitle.change(`Discover${pageTitleSuffix}`); @@ -116,20 +130,6 @@ export function Discover({ addHelpMenuToAppChrome(chrome, docLinks); }, [savedSearch, chrome, docLinks]); - const { onAddColumn, onRemoveColumn, onMoveColumn, onSetColumns } = useMemo( - () => - getStateColumnActions({ - capabilities, - config, - indexPattern, - indexPatterns, - setAppState, - state, - useNewFieldsApi, - }), - [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] - ); - const onOpenInspector = useCallback(() => { // prevent overlapping setExpandedDoc(undefined); @@ -225,12 +225,6 @@ export function Discover({ } }; - const columns = useMemo(() => { - if (!state.columns) { - return []; - } - return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; - }, [state, useNewFieldsApi]); return ( @@ -439,13 +433,13 @@ export function Discover({ searchTitle={opts.savedSearch.lastSavedTitle} setExpandedDoc={setExpandedDoc} showTimeCol={ - !config.get('doc_table:hideTimeColumn', false) && + !config.get(DOC_HIDE_TIME_COLUMN_SETTING, false) && !!indexPattern.timeFieldName } services={services} settings={state.grid} - onAddColumn={onAddColumn} onFilter={onAddFilter as DocViewFilterFn} + onAddColumn={onAddColumn} onRemoveColumn={onRemoveColumn} onSetColumns={onSetColumns} onSort={onSort} diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss index cb1b9a8ea191e..053b405b90acb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.scss @@ -66,6 +66,7 @@ text-align: right; } +.euiDataGrid__loading, .euiDataGrid__noResults { display: flex; flex-direction: column; diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx index f969eb32f3791..65a6ee80564e9 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid.tsx @@ -14,11 +14,12 @@ import { EuiDataGridStyle, EuiDataGridProps, EuiDataGrid, - EuiIcon, EuiScreenReaderOnly, EuiSpacer, EuiText, htmlIdGenerator, + EuiLoadingSpinner, + EuiIcon, } from '@elastic/eui'; import { IndexPattern } from '../../../kibana_services'; import { DocViewFilterFn, ElasticSearchHit } from '../../doc_views/doc_views_types'; @@ -88,9 +89,9 @@ export interface DiscoverGridProps { */ onSetColumns: (columns: string[]) => void; /** - * function to change sorting of the documents + * function to change sorting of the documents, skipped when isSortEnabled is set to false */ - onSort: (sort: string[][]) => void; + onSort?: (sort: string[][]) => void; /** * Array of documents provided by Elasticsearch */ @@ -123,6 +124,10 @@ export interface DiscoverGridProps { * Determines whether the time columns should be displayed (legacy settings) */ showTimeCol: boolean; + /** + * Manage user sorting control + */ + isSortEnabled?: boolean; /** * Current sort setting */ @@ -131,6 +136,14 @@ export interface DiscoverGridProps { * How the data is fetched */ useNewFieldsApi: boolean; + /** + * Manage pagination control + */ + isPaginationEnabled?: boolean; + /** + * List of used control columns (available: 'openDetails', 'select') + */ + controlColumnIds?: string[]; } export const EuiDataGridMemoized = React.memo((props: EuiDataGridProps) => { @@ -159,6 +172,9 @@ export const DiscoverGrid = ({ showTimeCol, sort, useNewFieldsApi, + isSortEnabled = true, + isPaginationEnabled = true, + controlColumnIds = ['openDetails', 'select'], }: DiscoverGridProps) => { const [selectedDocs, setSelectedDocs] = useState([]); const [isFilterActive, setIsFilterActive] = useState(false); @@ -210,14 +226,16 @@ export const DiscoverGrid = ({ const onChangePage = (pageIndex: number) => setPagination((paginationData) => ({ ...paginationData, pageIndex })); - return { - onChangeItemsPerPage, - onChangePage, - pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, - pageSize: pagination.pageSize, - pageSizeOptions: pageSizeArr, - }; - }, [pagination, pageCount]); + return isPaginationEnabled + ? { + onChangeItemsPerPage, + onChangePage, + pageIndex: pagination.pageIndex > pageCount - 1 ? 0 : pagination.pageIndex, + pageSize: pagination.pageSize, + pageSizeOptions: pageSizeArr, + } + : undefined; + }, [pagination, pageCount, isPaginationEnabled]); /** * Sorting @@ -226,9 +244,11 @@ export const DiscoverGrid = ({ const onTableSort = useCallback( (sortingColumnsData) => { - onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + if (isSortEnabled && onSort) { + onSort(sortingColumnsData.map(({ id, direction }: SortObj) => [id, direction])); + } }, - [onSort] + [onSort, isSortEnabled] ); /** @@ -253,8 +273,16 @@ export const DiscoverGrid = ({ const randomId = useMemo(() => htmlIdGenerator()(), []); const euiGridColumns = useMemo( - () => getEuiGridColumns(displayedColumns, settings, indexPattern, showTimeCol, defaultColumns), - [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns] + () => + getEuiGridColumns( + displayedColumns, + settings, + indexPattern, + showTimeCol, + defaultColumns, + isSortEnabled + ), + [displayedColumns, indexPattern, showTimeCol, settings, defaultColumns, isSortEnabled] ); const schemaDetectors = useMemo(() => getSchemaDetectors(), []); const columnsVisibility = useMemo( @@ -266,11 +294,16 @@ export const DiscoverGrid = ({ }), [displayedColumns, indexPattern, showTimeCol, onSetColumns] ); - const sorting = useMemo(() => ({ columns: sortingColumns, onSort: onTableSort }), [ - sortingColumns, - onTableSort, - ]); - const lead = useMemo(() => getLeadControlColumns(), []); + const sorting = useMemo(() => { + if (isSortEnabled) { + return { columns: sortingColumns, onSort: onTableSort }; + } + return { columns: sortingColumns, onSort: () => {} }; + }, [sortingColumns, onTableSort, isSortEnabled]); + const lead = useMemo( + () => getLeadControlColumns().filter(({ id }) => controlColumnIds.includes(id)), + [controlColumnIds] + ); const additionalControls = useMemo( () => @@ -286,6 +319,18 @@ export const DiscoverGrid = ({ [usedSelectedDocs, isFilterActive, rows, setIsFilterActive] ); + if (!rowCount && isLoading) { + return ( +
+ + + + + +
+ ); + } + if (!rowCount) { return (
@@ -348,10 +393,12 @@ export const DiscoverGrid = ({ ? { ...toolbarVisibility, showColumnSelector: false, + showSortSelector: isSortEnabled, additionalControls, } : { ...toolbarVisibility, + showSortSelector: isSortEnabled, additionalControls, } } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx index 93b5bf8fde0c1..3cbac90aa39cb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.test.tsx @@ -12,7 +12,14 @@ import { indexPatternWithTimefieldMock } from '../../../__mocks__/index_pattern_ describe('Discover grid columns ', function () { it('returns eui grid columns without time column', async () => { - const actual = getEuiGridColumns(['extension', 'message'], {}, indexPatternMock, false, false); + const actual = getEuiGridColumns( + ['extension', 'message'], + {}, + indexPatternMock, + false, + false, + true + ); expect(actual).toMatchInlineSnapshot(` Array [ Object { @@ -54,6 +61,7 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, false, + true, true ); expect(actual).toMatchInlineSnapshot(` @@ -94,7 +102,8 @@ describe('Discover grid columns ', function () { {}, indexPatternWithTimefieldMock, true, - false + false, + true ); expect(actual).toMatchInlineSnapshot(` Array [ diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx index df7e2285a0754..3a27772662b56 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_columns.tsx @@ -53,7 +53,8 @@ export function buildEuiGridColumn( columnName: string, columnWidth: number | undefined = 0, indexPattern: IndexPattern, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled: boolean ) { const timeString = i18n.translate('discover.timeLabel', { defaultMessage: 'Time', @@ -62,7 +63,7 @@ export function buildEuiGridColumn( const column: EuiDataGridColumn = { id: columnName, schema: getSchemaByKbnType(indexPatternField?.type), - isSortable: indexPatternField?.sortable === true, + isSortable: isSortEnabled && indexPatternField?.sortable === true, display: columnName === '_source' ? i18n.translate('discover.grid.documentHeader', { @@ -100,7 +101,8 @@ export function getEuiGridColumns( settings: DiscoverGridSettings | undefined, indexPattern: IndexPattern, showTimeCol: boolean, - defaultColumns: boolean + defaultColumns: boolean, + isSortEnabled: boolean ) { const timeFieldName = indexPattern.timeFieldName; const getColWidth = (column: string) => settings?.columns?.[column]?.width ?? 0; @@ -108,12 +110,12 @@ export function getEuiGridColumns( if (showTimeCol && indexPattern.timeFieldName && !columns.find((col) => col === timeFieldName)) { const usedColumns = [indexPattern.timeFieldName, ...columns]; return usedColumns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } return columns.map((column) => - buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns) + buildEuiGridColumn(column, getColWidth(column), indexPattern, defaultColumns, isSortEnabled) ); } diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx index 9ebe3ee95f797..41cf3f5a68edb 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.test.tsx @@ -51,7 +51,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -73,7 +80,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -95,7 +109,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); @@ -117,7 +138,14 @@ describe('document selection', () => { const component = mountWithIntl( - + ); diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx index a99819fa9e057..03c17c801fa96 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_document_selection.tsx @@ -5,7 +5,8 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import React, { useCallback, useState, useContext, useMemo } from 'react'; +import React, { useCallback, useState, useContext, useMemo, useEffect } from 'react'; +import classNames from 'classnames'; import { EuiButtonEmpty, EuiContextMenuItem, @@ -13,9 +14,11 @@ import { EuiCopy, EuiPopover, EuiCheckbox, + EuiDataGridCellValueElementProps, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import classNames from 'classnames'; +import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; +import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; @@ -27,11 +30,25 @@ export const getDocId = (doc: ElasticSearchHit & { _routing?: string }) => { const routing = doc._routing ? doc._routing : ''; return [doc._index, doc._id, routing].join('::'); }; -export const SelectButton = ({ rowIndex }: { rowIndex: number }) => { - const ctx = useContext(DiscoverGridContext); - const doc = useMemo(() => ctx.rows[rowIndex], [ctx.rows, rowIndex]); +export const SelectButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueElementProps) => { + const { selectedDocs, expanded, rows, isDarkMode, setSelectedDocs } = useContext( + DiscoverGridContext + ); + const doc = useMemo(() => rows[rowIndex], [rows, rowIndex]); const id = useMemo(() => getDocId(doc), [doc]); - const checked = useMemo(() => ctx.selectedDocs.includes(id), [ctx.selectedDocs, id]); + const checked = useMemo(() => selectedDocs.includes(id), [selectedDocs, id]); + + useEffect(() => { + if (expanded && doc && expanded._id === doc._id) { + setCellProps({ + style: { + backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, + }, + }); + } else { + setCellProps({ style: undefined }); + } + }, [expanded, doc, setCellProps, isDarkMode]); return ( { data-test-subj={`dscGridSelectDoc-${id}`} onChange={() => { if (checked) { - const newSelection = ctx.selectedDocs.filter((docId) => docId !== id); - ctx.setSelectedDocs(newSelection); + const newSelection = selectedDocs.filter((docId) => docId !== id); + setSelectedDocs(newSelection); } else { - ctx.setSelectedDocs([...ctx.selectedDocs, id]); + setSelectedDocs([...selectedDocs, id]); } }} /> diff --git a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx index 115acb84b95d8..b11733c159520 100644 --- a/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/discover_grid_expand_button.tsx @@ -12,6 +12,7 @@ import themeDark from '@elastic/eui/dist/eui_theme_dark.json'; import themeLight from '@elastic/eui/dist/eui_theme_light.json'; import { i18n } from '@kbn/i18n'; import { DiscoverGridContext } from './discover_grid_context'; +import { EsHitRecord } from '../../angular/context/api/context'; /** * Button to expand a given row */ @@ -19,7 +20,11 @@ export const ExpandButton = ({ rowIndex, setCellProps }: EuiDataGridCellValueEle const { expanded, setExpanded, rows, isDarkMode } = useContext(DiscoverGridContext); const current = rows[rowIndex]; useEffect(() => { - if (expanded && current && expanded._id === current._id) { + if ((current as EsHitRecord).isAnchor) { + setCellProps({ + className: 'dscDocsGrid__cell--highlight', + }); + } else if (expanded && current && expanded._id === current._id) { setCellProps({ style: { backgroundColor: isDarkMode ? themeDark.euiColorHighlight : themeLight.euiColorHighlight, diff --git a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx index fc3dd499f92e0..b3c205e072508 100644 --- a/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx +++ b/src/plugins/discover/public/application/components/discover_grid/get_render_cell_value.tsx @@ -21,6 +21,7 @@ import { ElasticSearchHit } from '../../doc_views/doc_views_types'; import { DiscoverGridContext } from './discover_grid_context'; import { JsonCodeEditor } from '../json_code_editor/json_code_editor'; import { defaultMonacoEditorWidth } from './constants'; +import { EsHitRecord } from '../../angular/context/api/context'; export const getRenderCellValueFn = ( indexPattern: IndexPattern, @@ -38,7 +39,11 @@ export const getRenderCellValueFn = ( const ctx = useContext(DiscoverGridContext); useEffect(() => { - if (ctx.expanded && row && ctx.expanded._id === row._id) { + if ((row as EsHitRecord).isAnchor) { + setCellProps({ + className: 'dscDocsGrid__cell--highlight', + }); + } else if (ctx.expanded && row && ctx.expanded._id === row._id) { setCellProps({ style: { backgroundColor: ctx.isDarkMode diff --git a/src/plugins/discover/public/application/embeddable/search_embeddable.ts b/src/plugins/discover/public/application/embeddable/search_embeddable.ts index 99ecb4c11eef2..1e3c7e77d3615 100644 --- a/src/plugins/discover/public/application/embeddable/search_embeddable.ts +++ b/src/plugins/discover/public/application/embeddable/search_embeddable.ts @@ -33,6 +33,7 @@ import { getServices, IndexPattern, ISearchSource } from '../../kibana_services' import { SEARCH_EMBEDDABLE_TYPE } from './constants'; import { SavedSearch } from '../..'; import { + DOC_HIDE_TIME_COLUMN_SETTING, SAMPLE_SIZE_SETTING, SEARCH_FIELDS_FROM_SOURCE, SORT_DEFAULT_ORDER_SETTING, @@ -256,7 +257,7 @@ export class SearchEmbeddable if (this.savedSearch.grid) { searchScope.settings = this.savedSearch.grid; } - searchScope.showTimeCol = !this.services.uiSettings.get('doc_table:hideTimeColumn', false); + searchScope.showTimeCol = !this.services.uiSettings.get(DOC_HIDE_TIME_COLUMN_SETTING, false); searchScope.filter = async (field, value, operator) => { let filters = esFilters.generateFilters( diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx new file mode 100644 index 0000000000000..c9e1899aff8de --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.test.tsx @@ -0,0 +1,67 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { renderHook } from '@testing-library/react-hooks'; +import { useDataGridColumns } from './use_data_grid_columns'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { configMock } from '../../__mocks__/config'; +import { indexPatternsMock } from '../../__mocks__/index_patterns'; +import { AppState } from '../angular/context_state'; +import { Capabilities } from '../../../../../core/types'; + +describe('useDataGridColumns', () => { + const defaultProps = { + capabilities: ({ discover: { save: true } } as unknown) as Capabilities, + config: configMock, + indexPattern: indexPatternMock, + indexPatterns: indexPatternsMock, + setAppState: () => {}, + state: { + columns: ['Time', 'message'], + } as AppState, + useNewFieldsApi: false, + }; + + test('should return valid result', () => { + const { result } = renderHook(() => { + return useDataGridColumns(defaultProps); + }); + + expect(result.current.columns).toEqual(['Time', 'message']); + expect(result.current.onAddColumn).toBeInstanceOf(Function); + expect(result.current.onRemoveColumn).toBeInstanceOf(Function); + expect(result.current.onMoveColumn).toBeInstanceOf(Function); + expect(result.current.onSetColumns).toBeInstanceOf(Function); + }); + + test('should skip _source column when useNewFieldsApi is set to true', () => { + const { result } = renderHook(() => { + return useDataGridColumns({ + ...defaultProps, + state: { + columns: ['Time', '_source'], + }, + useNewFieldsApi: true, + }); + }); + + expect(result.current.columns).toEqual(['Time']); + }); + + test('should return empty columns array', () => { + const { result } = renderHook(() => { + return useDataGridColumns({ + ...defaultProps, + state: { + columns: [], + }, + }); + }); + expect(result.current.columns).toEqual([]); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts new file mode 100644 index 0000000000000..c913b9abd1b43 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_data_grid_columns.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { useMemo } from 'react'; + +import { Capabilities, IUiSettingsClient } from 'kibana/public'; +import { IndexPattern, IndexPatternsContract } from '../../kibana_services'; +import { + AppState as DiscoverState, + GetStateReturn as DiscoverGetStateReturn, +} from '../angular/discover_state'; +import { + AppState as ContextState, + GetStateReturn as ContextGetStateReturn, +} from '../angular/context_state'; +import { getStateColumnActions } from '../angular/doc_table/actions/columns'; + +interface UseDataGridColumnsProps { + capabilities: Capabilities; + config: IUiSettingsClient; + indexPattern: IndexPattern; + indexPatterns: IndexPatternsContract; + useNewFieldsApi: boolean; + setAppState: DiscoverGetStateReturn['setAppState'] | ContextGetStateReturn['setAppState']; + state: DiscoverState | ContextState; +} + +export const useDataGridColumns = ({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, +}: UseDataGridColumnsProps) => { + const { onAddColumn, onRemoveColumn, onSetColumns, onMoveColumn } = useMemo( + () => + getStateColumnActions({ + capabilities, + config, + indexPattern, + indexPatterns, + setAppState, + state, + useNewFieldsApi, + }), + [capabilities, config, indexPattern, indexPatterns, setAppState, state, useNewFieldsApi] + ); + + const columns = useMemo(() => { + if (!state.columns) { + return []; + } + return useNewFieldsApi ? state.columns.filter((col) => col !== '_source') : state.columns; + }, [state, useNewFieldsApi]); + + return { + columns, + onAddColumn, + onRemoveColumn, + onMoveColumn, + onSetColumns, + }; +}; diff --git a/test/functional/apps/discover/_data_grid_context.ts b/test/functional/apps/discover/_data_grid_context.ts index 275ac011820be..ee60660ae4a9e 100644 --- a/test/functional/apps/discover/_data_grid_context.ts +++ b/test/functional/apps/discover/_data_grid_context.ts @@ -19,7 +19,6 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const filterBar = getService('filterBar'); const dataGrid = getService('dataGrid'); - const docTable = getService('docTable'); const PageObjects = getPageObjects([ 'common', 'discover', @@ -67,16 +66,17 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await dataGrid.clickRowToggle({ rowIndex: 0 }); const rowActions = await dataGrid.getRowActions({ rowIndex: 0 }); await rowActions[1].click(); - // entering the context view (contains the legacy type) - const contextFields = await docTable.getFields(); + + const contextFields = await dataGrid.getFields(); const anchorTimestamp = contextFields[0][0]; + return anchorTimestamp === firstTimestamp; }); }); it('should open the context view with the same columns', async () => { - const columnNames = await docTable.getHeaderFields(); - expect(columnNames).to.eql(['Time', ...TEST_COLUMN_NAMES]); + const columnNames = await dataGrid.getHeaderFields(); + expect(columnNames).to.eql(['Time (@timestamp)', ...TEST_COLUMN_NAMES]); }); it('should open the context view with the filters disabled', async () => { @@ -111,7 +111,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { expect(await browser.getCurrentUrl()).to.contain('#/context'); await PageObjects.header.waitUntilLoadingHasFinished(); await retry.waitFor('document table has a length of 6', async () => { - const nrOfDocs = (await docTable.getBodyRows()).length; + const nrOfDocs = (await dataGrid.getBodyRows()).length; return nrOfDocs === 6; }); }); diff --git a/test/functional/services/data_grid.ts b/test/functional/services/data_grid.ts index a00587c978977..f66b0fe5b9e79 100644 --- a/test/functional/services/data_grid.ts +++ b/test/functional/services/data_grid.ts @@ -135,6 +135,7 @@ export class DataGridService extends FtrService { if (!table) { return []; } + const cells = await table.findAllByCssSelector('.euiDataGridRowCell'); const rows: WebElementWrapper[][] = []; @@ -173,14 +174,13 @@ export class DataGridService extends FtrService { } public async getHeaderFields(): Promise { - const result = await this.find.allByCssSelector('.euiDataGridHeaderCell__content'); + const result = await this.find.allByCssSelector( + '.euiDataGridHeaderCell__button > .euiDataGridHeaderCell__content' + ); + const textArr = []; - let idx = 0; for (const cell of result) { - if (idx > 1) { - textArr.push(await cell.getVisibleText()); - } - idx++; + textArr.push(await cell.getVisibleText()); } return Promise.resolve(textArr); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8473a663084f5..68f3e95ba33d3 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -1553,7 +1553,6 @@ "discover.bucketIntervalTooltip.tooLargeBucketsText": "大きすぎるバケット", "discover.bucketIntervalTooltip.tooManyBucketsText": "バケットが多すぎます", "discover.clearSelection": "選択した項目をクリア", - "discover.context.breadcrumb": "{indexPatternTitle}#{docId} のコンテキスト", "discover.context.failedToLoadAnchorDocumentDescription": "アンカードキュメントの読み込みに失敗しました", "discover.context.failedToLoadAnchorDocumentErrorDescription": "アンカードキュメントの読み込みに失敗しました。", "discover.context.loadButtonLabel": "読み込み", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 826c9cf493e62..fa7b9648a95fb 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -1562,7 +1562,6 @@ "discover.bucketIntervalTooltip.tooLargeBucketsText": "存储桶过大", "discover.bucketIntervalTooltip.tooManyBucketsText": "存储桶过多", "discover.clearSelection": "清除所选内容", - "discover.context.breadcrumb": "{indexPatternTitle}#{docId} 的上下文", "discover.context.failedToLoadAnchorDocumentDescription": "无法加载定位点文档", "discover.context.failedToLoadAnchorDocumentErrorDescription": "无法加载定位点文档。", "discover.context.loadButtonLabel": "加载", From 5da47f8969e9edff22b479c749a507f6bef79325 Mon Sep 17 00:00:00 2001 From: Marta Bondyra Date: Tue, 1 Jun 2021 13:46:13 +0200 Subject: [PATCH 15/46] [Lens] Reduce lodash footprint (#101029) * [Lens] extract lodash methods * remove lodash from files that don't use it --- .../editor_frame/config_panel/layer_actions.ts | 4 ++-- .../editor_frame/suggestion_panel.tsx | 4 ++-- .../embeddable/embeddable.tsx | 18 +++++++++--------- .../dimension_panel/dimension_editor.tsx | 1 - .../dimension_panel/field_select.tsx | 8 ++++---- .../dimension_panel/operation_support.ts | 1 - .../dimension_panel/reference_editor.tsx | 1 - .../indexpattern_datasource/indexpattern.tsx | 1 - .../indexpattern_suggestions.ts | 12 ++++++------ .../indexpattern_datasource/loader.test.ts | 6 +++--- .../public/indexpattern_datasource/loader.ts | 6 +++--- .../operations/layer_helpers.ts | 8 ++++---- .../public/metric_visualization/auto_scale.tsx | 4 ++-- .../external_context_middleware.ts | 4 ++-- 14 files changed, 37 insertions(+), 41 deletions(-) diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts index dd95770655d1a..7d8a373192ee5 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/layer_actions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { mapValues } from 'lodash'; import { EditorFrameState } from '../state_management'; import { Datasource, Visualization } from '../../../types'; @@ -35,7 +35,7 @@ export function removeLayer(opts: RemoveLayerOptions): EditorFrameState { return { ...state, - datasourceStates: _.mapValues(state.datasourceStates, (datasourceState, datasourceId) => { + datasourceStates: mapValues(state.datasourceStates, (datasourceState, datasourceId) => { const datasource = datasourceMap[datasourceId!]; return { ...datasourceState, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx index e5acd2a2f47fd..0c2eb4f39d895 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/suggestion_panel.tsx @@ -7,7 +7,7 @@ import './suggestion_panel.scss'; -import _, { camelCase } from 'lodash'; +import { camelCase, pick } from 'lodash'; import React, { useState, useEffect, useMemo, useRef } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -442,7 +442,7 @@ function getPreviewExpression( ) { const datasource = datasources[visualizableState.datasourceId]; const datasourceState = visualizableState.datasourceState; - const updatedLayerApis: Record = _.pick( + const updatedLayerApis: Record = pick( frame.datasourceLayers, visualizableState.keptLayerIds ); diff --git a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx index 3c4412813bb83..89a04f3820169 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/embeddable/embeddable.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { isEqual, uniqBy } from 'lodash'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { @@ -23,7 +23,7 @@ import { Subscription } from 'rxjs'; import { toExpression, Ast } from '@kbn/interpreter/common'; import { DefaultInspectorAdapters, RenderMode } from 'src/plugins/expressions'; import { map, distinctUntilChanged, skip } from 'rxjs/operators'; -import isEqual from 'fast-deep-equal'; +import fastIsEqual from 'fast-deep-equal'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; import { METRIC_TYPE } from '@kbn/analytics'; import { @@ -161,7 +161,7 @@ export class Embeddable input$ .pipe( map((input) => input.enhancements?.dynamicActions), - distinctUntilChanged((a, b) => isEqual(a, b)), + distinctUntilChanged((a, b) => fastIsEqual(a, b)), skip(1) ) .subscribe((input) => { @@ -195,7 +195,7 @@ export class Embeddable input$ .pipe( distinctUntilChanged((a, b) => - isEqual( + fastIsEqual( ['attributes' in a && a.attributes, 'savedObjectId' in a && a.savedObjectId], ['attributes' in b && b.attributes, 'savedObjectId' in b && b.savedObjectId] ) @@ -214,7 +214,7 @@ export class Embeddable .pipe(map(() => this.getInput())) .pipe( distinctUntilChanged((a, b) => - isEqual( + fastIsEqual( [a.filters, a.query, a.timeRange, a.searchSessionId], [b.filters, b.query, b.timeRange, b.searchSessionId] ) @@ -283,9 +283,9 @@ export class Embeddable ? containerState.filters.filter((filter) => !filter.meta.disabled) : undefined; if ( - !_.isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || - !_.isEqual(containerState.query, this.externalSearchContext.query) || - !_.isEqual(cleanedFilters, this.externalSearchContext.filters) || + !isEqual(containerState.timeRange, this.externalSearchContext.timeRange) || + !isEqual(containerState.query, this.externalSearchContext.query) || + !isEqual(cleanedFilters, this.externalSearchContext.filters) || this.externalSearchContext.searchSessionId !== containerState.searchSessionId ) { this.externalSearchContext = { @@ -446,7 +446,7 @@ export class Embeddable return; } const responses = await Promise.allSettled( - _.uniqBy( + uniqBy( this.savedVis.references.filter(({ type }) => type === 'index-pattern'), 'id' ).map(({ id }) => this.deps.indexPatternService.get(id)) diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 8e26713630281..fcca4a41581c2 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -6,7 +6,6 @@ */ import './dimension_editor.scss'; -import _ from 'lodash'; import React, { useState, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx index b80d90ba78b1d..db0a42047a1b8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/field_select.tsx @@ -6,7 +6,7 @@ */ import './field_select.scss'; -import _ from 'lodash'; +import { partition } from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -68,7 +68,7 @@ export function FieldSelect({ return !currentOperationType || operationByField[fieldName]!.has(currentOperationType); } - const [specialFields, normalFields] = _.partition( + const [specialFields, normalFields] = partition( fields, (field) => currentIndexPattern.getFieldByName(field)?.type === 'document' ); @@ -121,11 +121,11 @@ export function FieldSelect({ })); } - const [metaFields, nonMetaFields] = _.partition( + const [metaFields, nonMetaFields] = partition( normalFields, (field) => currentIndexPattern.getFieldByName(field)?.meta ); - const [availableFields, emptyFields] = _.partition(nonMetaFields, containsData); + const [availableFields, emptyFields] = partition(nonMetaFields, containsData); const constructFieldsOptions = (fieldsArr: string[], label: string) => fieldsArr.length > 0 && { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts index 801b1b17a1831..504aa0912f9cc 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/operation_support.ts @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import { DatasourceDimensionDropProps } from '../../types'; import { OperationType } from '../indexpattern'; import { memoizedGetAvailableOperationsByMetadata } from '../operations'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx index e02a014935458..71de1e10300f0 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/reference_editor.tsx @@ -6,7 +6,6 @@ */ import './dimension_editor.scss'; -import _ from 'lodash'; import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index 8fb0994c42fb9..8b60cf134fe6f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import _ from 'lodash'; import React from 'react'; import { render } from 'react-dom'; import { I18nProvider } from '@kbn/i18n/react'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts index bde07c182555e..803ba9f5bae5d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { flatten, minBy, pick, mapValues } from 'lodash'; import { i18n } from '@kbn/i18n'; import { generateId } from '../id_generator'; import { DatasourceSuggestion, TableChangeType } from '../types'; @@ -58,9 +58,9 @@ function buildSuggestion({ // It's fairly easy to accidentally introduce a mismatch between // columnOrder and columns, so this is a safeguard to ensure the // two match up. - const layers = _.mapValues(updatedState.layers, (layer) => ({ + const layers = mapValues(updatedState.layers, (layer) => ({ ...layer, - columns: _.pick(layer.columns, layer.columnOrder) as Record, + columns: pick(layer.columns, layer.columnOrder) as Record, })); const columnOrder = layers[layerId].columnOrder; @@ -111,7 +111,7 @@ export function getDatasourceSuggestionsForField( // The field we're suggesting on matches an existing layer. In this case we find the layer with // the fewest configured columns and try to add the field to this table. If this layer does not // contain any layers yet, behave as if there is no layer. - const mostEmptyLayerId = _.minBy( + const mostEmptyLayerId = minBy( layerIds, (layerId) => state.layers[layerId].columnOrder.length ) as string; @@ -386,7 +386,7 @@ export function getDatasourceSuggestionsFromCurrentState( ]); } - return _.flatten( + return flatten( Object.entries(state.layers || {}) .filter(([_id, layer]) => layer.columnOrder.length && layer.indexPatternId) .map(([layerId, layer]) => { @@ -586,7 +586,7 @@ function createSimplifiedTableSuggestions(state: IndexPatternPrivateState, layer availableReferenceColumns, ] = getExistingColumnGroups(layer); - return _.flatten( + return flatten( availableBucketedColumns.map((_col, index) => { // build suggestions with fewer buckets const bucketedColumns = availableBucketedColumns.slice(0, index + 1); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts index d3913728cb64e..192f3d3c0c535 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.test.ts @@ -6,7 +6,7 @@ */ import { HttpHandler } from 'kibana/public'; -import _ from 'lodash'; +import { last } from 'lodash'; import { loadInitialState, loadIndexPatterns, @@ -841,7 +841,7 @@ describe('loader', () => { it('should call once for each index pattern', async () => { const setState = jest.fn(); const fetchJson = (jest.fn((path: string) => { - const indexPatternTitle = _.last(path.split('/')); + const indexPatternTitle = last(path.split('/')); return { indexPatternTitle, existingFieldNames: ['field_1', 'field_2'].map( @@ -891,7 +891,7 @@ describe('loader', () => { const setState = jest.fn(); const showNoDataPopover = jest.fn(); const fetchJson = (jest.fn((path: string) => { - const indexPatternTitle = _.last(path.split('/')); + const indexPatternTitle = last(path.split('/')); return { indexPatternTitle, existingFieldNames: diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts index 0eb661e92bb1d..2921251babe7f 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/loader.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _ from 'lodash'; +import { uniq, mapValues } from 'lodash'; import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; import { HttpSetup, SavedObjectReference } from 'kibana/public'; import { InitializationOptions, StateSetter } from '../types'; @@ -227,7 +227,7 @@ export async function loadInitialState({ const state = persistedState && references ? injectReferences(persistedState, references) : undefined; - const requiredPatterns: string[] = _.uniq( + const requiredPatterns: string[] = uniq( state ? Object.values(state.layers) .map((l) => l.indexPatternId) @@ -312,7 +312,7 @@ export async function changeIndexPattern({ setState((s) => ({ ...s, layers: isSingleEmptyLayer(state.layers) - ? _.mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) + ? mapValues(state.layers, (layer) => updateLayerIndexPattern(layer, indexPatterns[id])) : state.layers, indexPatterns: { ...s.indexPatterns, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index bc4a61eda3969..92452a11e94c1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import _, { partition } from 'lodash'; +import { partition, mapValues, pickBy } from 'lodash'; import { getSortScoreByPriority } from './operations'; import type { OperationMetadata, VisualizationDimensionGroupConfig } from '../../types'; import { @@ -1071,7 +1071,7 @@ export function getColumnOrder(layer: IndexPatternLayer): string[] { } }); - const [aggregations, metrics] = _.partition(entries, ([, col]) => col.isBucketed); + const [aggregations, metrics] = partition(entries, ([, col]) => col.isBucketed); return aggregations.map(([id]) => id).concat(metrics.map(([id]) => id)); } @@ -1110,10 +1110,10 @@ export function updateLayerIndexPattern( layer: IndexPatternLayer, newIndexPattern: IndexPattern ): IndexPatternLayer { - const keptColumns: IndexPatternLayer['columns'] = _.pickBy(layer.columns, (column) => { + const keptColumns: IndexPatternLayer['columns'] = pickBy(layer.columns, (column) => { return isColumnTransferable(column, newIndexPattern, layer); }); - const newColumns: IndexPatternLayer['columns'] = _.mapValues(keptColumns, (column) => { + const newColumns: IndexPatternLayer['columns'] = mapValues(keptColumns, (column) => { const operationDefinition = operationDefinitionMap[column.operationType]; return operationDefinition.transfer ? operationDefinition.transfer(column, newIndexPattern) diff --git a/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx index cc9c10b678374..9047937093134 100644 --- a/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx +++ b/x-pack/plugins/lens/public/metric_visualization/auto_scale.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import _ from 'lodash'; +import { throttle } from 'lodash'; import { EuiResizeObserver } from '@elastic/eui'; interface Props extends React.HTMLAttributes { @@ -26,7 +26,7 @@ export class AutoScale extends React.Component { constructor(props: Props) { super(props); - this.scale = _.throttle(() => { + this.scale = throttle(() => { const scale = computeScale(this.parent, this.child, this.props.minScale); // Prevent an infinite render loop diff --git a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts index 35d0f7cf197ed..0743dce73eb33 100644 --- a/x-pack/plugins/lens/public/state_management/external_context_middleware.ts +++ b/x-pack/plugins/lens/public/state_management/external_context_middleware.ts @@ -6,7 +6,7 @@ */ import { delay, finalize, switchMap, tap } from 'rxjs/operators'; -import _, { debounce } from 'lodash'; +import { debounce, isEqual } from 'lodash'; import { Dispatch, MiddlewareAPI, PayloadAction } from '@reduxjs/toolkit'; import { trackUiEvent } from '../lens_ui_telemetry'; @@ -44,7 +44,7 @@ function subscribeToExternalContext( const dispatchFromExternal = (searchSessionId = search.session.start()) => { const globalFilters = filterManager.getFilters(); - const filters = _.isEqual(getState().app.filters, globalFilters) + const filters = isEqual(getState().app.filters, globalFilters) ? null : { filters: globalFilters }; dispatch( From 25f569ae3df2a326f924fba404f8ce355fecd9ac Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 1 Jun 2021 12:49:51 +0100 Subject: [PATCH 16/46] skip flaky suite (#64473) --- .../apis/management/index_management/indices.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index cef1bdbba754b..e664d138274ff 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -177,7 +177,8 @@ export default function ({ getService }) { }); }); - describe('list', function () { + // FLAKY: https://github.com/elastic/kibana/issues/64473 + describe.skip('list', function () { this.tags(['skipCloud']); it('should list all the indices with the expected properties and data enrichers', async function () { From 19b8aa798ca2c373e0c96bd47eab9c65bf00c673 Mon Sep 17 00:00:00 2001 From: Tiago Costa Date: Tue, 1 Jun 2021 12:54:15 +0100 Subject: [PATCH 17/46] skip flaky suite (#90565) --- .../apis/management/index_management/indices.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/x-pack/test/api_integration/apis/management/index_management/indices.js b/x-pack/test/api_integration/apis/management/index_management/indices.js index e664d138274ff..d0e17476530b7 100644 --- a/x-pack/test/api_integration/apis/management/index_management/indices.js +++ b/x-pack/test/api_integration/apis/management/index_management/indices.js @@ -209,7 +209,8 @@ export default function ({ getService }) { }); describe('reload', function () { - describe('(not on Cloud)', function () { + // FLAKY: https://github.com/elastic/kibana/issues/90565 + describe.skip('(not on Cloud)', function () { this.tags(['skipCloud']); it('should list all the indices with the expected properties and data enrichers', async function () { From 4aa3920746645ac65e25e732bfaf2645c59b211e Mon Sep 17 00:00:00 2001 From: Shahzad Date: Tue, 1 Jun 2021 14:05:56 +0200 Subject: [PATCH 18/46] [Index Patterns Field Formatter] Added human readable precise formatter for duration (#100540) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../field_formats/converters/duration.test.ts | 174 +++++++++++++- .../field_formats/converters/duration.ts | 137 ++++++++++- .../__snapshots__/duration.test.tsx.snap | 221 ++++++++++++++++++ .../editors/duration/duration.test.tsx | 53 +++++ .../editors/duration/duration.tsx | 55 ++++- .../translations/translations/ja-JP.json | 1 - .../translations/translations/zh-CN.json | 1 - 7 files changed, 628 insertions(+), 14 deletions(-) diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index fc019720425df..72551f4b7b236 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -139,17 +139,182 @@ describe('Duration Format', () => { ], }); + testCase({ + inputFormat: 'nanoseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 2, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '0.00 Milliseconds', + }, + { + input: 658, + output: '0.00 Milliseconds', + }, + { + input: 3857, + output: '0.00 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 2, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '1.99 Milliseconds', + }, + { + input: 658, + output: '0.66 Milliseconds', + }, + { + input: 3857, + output: '3.86 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'microseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 1, + showSuffix: true, + fixtures: [ + { + input: 1988, + output: '2.0 Milliseconds', + }, + { + input: 0, + output: '0.0 Milliseconds', + }, + { + input: 658, + output: '0.7 Milliseconds', + }, + { + input: 3857, + output: '3.9 Milliseconds', + }, + ], + }); + + testCase({ + inputFormat: 'seconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + fixtures: [ + { + input: 600, + output: '10 Minutes', + }, + { + input: 30, + output: '30 Seconds', + }, + { + input: 3000, + output: '50 Minutes', + }, + ], + }); + + testCase({ + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + useShortSuffix: true, + fixtures: [ + { + input: -123, + output: '-123 ms', + }, + { + input: 1, + output: '1 ms', + }, + { + input: 600, + output: '600 ms', + }, + { + input: 30, + output: '30 ms', + }, + { + input: 3000, + output: '3 s', + }, + { + input: 300000, + output: '5 min', + }, + { + input: 30000000, + output: '8 h', + }, + { + input: 90000000, + output: '1 d', + }, + { + input: 9000000000, + output: '3 mon', + }, + { + input: 99999999999, + output: '3 y', + }, + ], + }); + + testCase({ + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + outputPrecision: 0, + showSuffix: true, + useShortSuffix: true, + includeSpaceWithSuffix: false, + fixtures: [ + { + input: -123, + output: '-123ms', + }, + { + input: 1, + output: '1ms', + }, + { + input: 600, + output: '600ms', + }, + ], + }); + function testCase({ inputFormat, outputFormat, outputPrecision, showSuffix, + useShortSuffix, + includeSpaceWithSuffix, fixtures, }: { inputFormat: string; outputFormat: string; outputPrecision: number | undefined; showSuffix: boolean | undefined; + useShortSuffix?: boolean; + includeSpaceWithSuffix?: boolean; fixtures: any[]; }) { fixtures.forEach((fixture: Record) => { @@ -160,7 +325,14 @@ describe('Duration Format', () => { outputPrecision ? `, ${outputPrecision} decimals` : '' }`, () => { const duration = new DurationFormat( - { inputFormat, outputFormat, outputPrecision, showSuffix }, + { + inputFormat, + outputFormat, + outputPrecision, + showSuffix, + useShortSuffix, + includeSpaceWithSuffix, + }, jest.fn() ); expect(duration.convert(input)).toBe(output); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index ef8c1df3704a8..c9a7091db8471 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -18,6 +18,7 @@ const ratioToSeconds: Record = { microseconds: 0.000001, }; const HUMAN_FRIENDLY = 'humanize'; +const HUMAN_FRIENDLY_PRECISE = 'humanizePrecise'; const DEFAULT_OUTPUT_PRECISION = 2; const DEFAULT_INPUT_FORMAT = { text: i18n.translate('data.fieldFormats.duration.inputFormats.seconds', { @@ -89,59 +90,89 @@ const inputFormats = [ }, ]; const DEFAULT_OUTPUT_FORMAT = { - text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize', { - defaultMessage: 'Human Readable', + text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize.approximate', { + defaultMessage: 'Human-readable (approximate)', }), method: 'humanize', }; const outputFormats = [ { ...DEFAULT_OUTPUT_FORMAT }, + { + text: i18n.translate('data.fieldFormats.duration.outputFormats.humanize.precise', { + defaultMessage: 'Human-readable (precise)', + }), + method: 'humanizePrecise', + }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMilliseconds', { defaultMessage: 'Milliseconds', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMilliseconds.short', { + defaultMessage: 'ms', + }), method: 'asMilliseconds', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asSeconds', { defaultMessage: 'Seconds', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asSeconds.short', { + defaultMessage: 's', + }), method: 'asSeconds', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMinutes', { defaultMessage: 'Minutes', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMinutes.short', { + defaultMessage: 'min', + }), method: 'asMinutes', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asHours', { defaultMessage: 'Hours', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asHours.short', { + defaultMessage: 'h', + }), method: 'asHours', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asDays', { defaultMessage: 'Days', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asDays.short', { + defaultMessage: 'd', + }), method: 'asDays', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asWeeks', { defaultMessage: 'Weeks', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asWeeks.short', { + defaultMessage: 'w', + }), method: 'asWeeks', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asMonths', { defaultMessage: 'Months', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asMonths.short', { + defaultMessage: 'mon', + }), method: 'asMonths', }, { text: i18n.translate('data.fieldFormats.duration.outputFormats.asYears', { defaultMessage: 'Years', }), + shortText: i18n.translate('data.fieldFormats.duration.outputFormats.asYears.short', { + defaultMessage: 'y', + }), method: 'asYears', }, ]; @@ -154,6 +185,29 @@ function parseInputAsDuration(val: number, inputFormat: string) { return moment.duration(val * ratio, kind); } +function formatInputHumanPrecise( + val: number, + inputFormat: string, + outputPrecision: number, + useShortSuffix: boolean, + includeSpace: string +) { + const ratio = ratioToSeconds[inputFormat] || 1; + const kind = (inputFormat in ratioToSeconds + ? 'seconds' + : inputFormat) as unitOfTime.DurationConstructor; + const valueInDuration = moment.duration(val * ratio, kind); + + return formatDuration( + val, + valueInDuration, + inputFormat, + outputPrecision, + useShortSuffix, + includeSpace + ); +} + export class DurationFormat extends FieldFormat { static id = FIELD_FORMAT_IDS.DURATION; static title = i18n.translate('data.fieldFormats.duration.title', { @@ -167,11 +221,17 @@ export class DurationFormat extends FieldFormat { isHuman() { return this.param('outputFormat') === HUMAN_FRIENDLY; } + + isHumanPrecise() { + return this.param('outputFormat') === HUMAN_FRIENDLY_PRECISE; + } + getParamDefaults() { return { inputFormat: DEFAULT_INPUT_FORMAT.kind, outputFormat: DEFAULT_OUTPUT_FORMAT.method, outputPrecision: DEFAULT_OUTPUT_PRECISION, + includeSpaceWithSuffix: true, }; } @@ -180,19 +240,84 @@ export class DurationFormat extends FieldFormat { const outputFormat = this.param('outputFormat') as keyof Duration; const outputPrecision = this.param('outputPrecision'); const showSuffix = Boolean(this.param('showSuffix')); + const useShortSuffix = Boolean(this.param('useShortSuffix')); + const includeSpaceWithSuffix = this.param('includeSpaceWithSuffix'); + + const includeSpace = includeSpaceWithSuffix ? ' ' : ''; + const human = this.isHuman(); + const humanPrecise = this.isHumanPrecise(); + const prefix = val < 0 && human ? i18n.translate('data.fieldFormats.duration.negativeLabel', { defaultMessage: 'minus', }) + ' ' : ''; + const duration = parseInputAsDuration(val, inputFormat) as Record; - const formatted = duration[outputFormat](); - const precise = human ? formatted : formatted.toFixed(outputPrecision); + const formatted = humanPrecise + ? formatInputHumanPrecise(val, inputFormat, outputPrecision, useShortSuffix, includeSpace) + : duration[outputFormat](); + + const precise = human || humanPrecise ? formatted : formatted.toFixed(outputPrecision); const type = outputFormats.find(({ method }) => method === outputFormat); - const suffix = showSuffix && type ? ` ${type.text}` : ''; - return prefix + precise + suffix; + const unitText = useShortSuffix ? type?.shortText : type?.text; + + const suffix = showSuffix && unitText && !human ? `${includeSpace}${unitText}` : ''; + + return humanPrecise ? precise : prefix + precise + suffix; }; } + +function formatDuration( + val: number, + duration: moment.Duration, + inputFormat: string, + outputPrecision: number, + useShortSuffix: boolean, + includeSpace: string +) { + // return nothing when the duration is falsy or not correctly parsed (P0D) + if (!duration || !duration.isValid()) return; + const units = [ + { unit: duration.years(), nextUnitRate: 12, method: 'asYears' }, + { unit: duration.months(), nextUnitRate: 4, method: 'asMonths' }, + { unit: duration.weeks(), nextUnitRate: 7, method: 'asWeeks' }, + { unit: duration.days(), nextUnitRate: 24, method: 'asDays' }, + { unit: duration.hours(), nextUnitRate: 60, method: 'asHours' }, + { unit: duration.minutes(), nextUnitRate: 60, method: 'asMinutes' }, + { unit: duration.seconds(), nextUnitRate: 1000, method: 'asSeconds' }, + { unit: duration.milliseconds(), nextUnitRate: 1000, method: 'asMilliseconds' }, + ]; + + const getUnitText = (method: string) => { + const type = outputFormats.find(({ method: methodT }) => method === methodT); + return useShortSuffix ? type?.shortText : type?.text; + }; + + for (let i = 0; i < units.length; i++) { + const unitValue = units[i].unit; + if (unitValue >= 1) { + const unitText = getUnitText(units[i].method); + + const value = Math.floor(unitValue); + if (units?.[i + 1]) { + const decimalPointValue = Math.floor(units[i + 1].unit); + return ( + (value + decimalPointValue / units[i].nextUnitRate).toFixed(outputPrecision) + + includeSpace + + unitText + ); + } else { + return unitValue.toFixed(outputPrecision) + includeSpace + unitText; + } + } + } + + const unitValue = units[units.length - 1].unit; + const unitText = getUnitText(units[units.length - 1].method); + + return unitValue.toFixed(outputPrecision) + includeSpace + unitText; +} diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap index cb7949deda64f..d000af4453cd1 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/__snapshots__/duration.test.tsx.snap @@ -1,5 +1,184 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`DurationFormatEditor should not render show suffix on dynamic output 1`] = ` + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + } + labelType="label" + > + + + + + } + onChange={[Function]} + /> + + + + } + onChange={[Function]} + /> + + + +`; + exports[`DurationFormatEditor should render human readable output normally 1`] = ` + + + } + onChange={[Function]} + /> + + + + } + onChange={[Function]} + /> + true, + isHumanPrecise: () => false, type: { inputFormats: [ { @@ -78,6 +81,7 @@ describe('DurationFormatEditor', () => { inputFormat: 'seconds', outputFormat: 'asMinutes', outputPrecision: 10, + includeSpaceWithSuffix: true, }; }), isHuman: () => false, @@ -91,6 +95,55 @@ describe('DurationFormatEditor', () => { onError={onError} /> ); + const labels = component.find(EuiSwitch); + expect(labels.length).toEqual(3); + expect(labels.get(0).props.label.props.defaultMessage).toEqual('Show suffix'); + expect(labels.get(1).props.label.props.defaultMessage).toEqual('Use short suffix'); + expect(labels.get(2).props.label.props.defaultMessage).toEqual( + 'Include space between suffix and value' + ); + + expect(component).toMatchSnapshot(); + }); + + it('should not render show suffix on dynamic output', async () => { + const newFormat = { + ...format, + getParamDefaults: jest.fn().mockImplementation(() => { + return { + inputFormat: 'seconds', + outputFormat: 'dynamic', + outputPrecision: 2, + includeSpaceWithSuffix: true, + }; + }), + isHuman: () => false, + isHumanPrecise: () => true, + }; + const component = shallow( + + ); + + const labels = component.find(EuiSwitch); + expect(labels.length).toEqual(2); + const useShortSuffixSwitch = labels.get(0); + + expect(useShortSuffixSwitch.props.label.props.defaultMessage).toEqual('Use short suffix'); + expect(useShortSuffixSwitch.props.disabled).toEqual(false); + + const includeSpaceSwitch = labels.get(1); + + expect(includeSpaceSwitch.props.disabled).toEqual(false); + expect(includeSpaceSwitch.props.label.props.defaultMessage).toEqual( + 'Include space between suffix and value' + ); + expect(component).toMatchSnapshot(); }); }); diff --git a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx index de413d02c5011..d61d14aac3fc7 100644 --- a/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/field_format_editor/editors/duration/duration.tsx @@ -41,6 +41,8 @@ interface DurationFormatEditorFormatParams { inputFormat: string; outputFormat: string; showSuffix?: boolean; + useShortSuffix?: boolean; + includeSpaceWithSuffix?: boolean; } export class DurationFormatEditor extends DefaultFormatEditor< @@ -83,9 +85,14 @@ export class DurationFormatEditor extends DefaultFormatEditor< } render() { - const { format, formatParams } = this.props; + const { format } = this.props; const { error, samples, hasDecimalError } = this.state; + const formatParams: DurationFormatEditorFormatParams = { + includeSpaceWithSuffix: format.getParamDefaults().includeSpaceWithSuffix, + ...this.props.formatParams, + }; + return ( + {!(format as DurationFormat).isHumanPrecise() && ( + + + } + checked={Boolean(formatParams.showSuffix)} + onChange={(e) => { + this.onChange({ + showSuffix: !formatParams.showSuffix, + }); + }} + /> + + )} + } + checked={Boolean(formatParams.useShortSuffix)} + onChange={(e) => { + this.onChange({ useShortSuffix: !formatParams.useShortSuffix }); + }} + /> + + + } - checked={Boolean(formatParams.showSuffix)} + checked={Boolean(formatParams.includeSpaceWithSuffix)} onChange={(e) => { - this.onChange({ showSuffix: !formatParams.showSuffix }); + this.onChange({ includeSpaceWithSuffix: !formatParams.includeSpaceWithSuffix }); }} /> diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 68f3e95ba33d3..56548ea7602a7 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -820,7 +820,6 @@ "data.fieldFormats.duration.outputFormats.asSeconds": "秒", "data.fieldFormats.duration.outputFormats.asWeeks": "週間", "data.fieldFormats.duration.outputFormats.asYears": "年", - "data.fieldFormats.duration.outputFormats.humanize": "人間に読解可能", "data.fieldFormats.duration.title": "期間", "data.fieldFormats.histogram.title": "ヒストグラム", "data.fieldFormats.ip.title": "IP アドレス", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index fa7b9648a95fb..c8802633a5556 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -823,7 +823,6 @@ "data.fieldFormats.duration.outputFormats.asSeconds": "秒", "data.fieldFormats.duration.outputFormats.asWeeks": "周", "data.fieldFormats.duration.outputFormats.asYears": "年", - "data.fieldFormats.duration.outputFormats.humanize": "可人工读取", "data.fieldFormats.duration.title": "持续时间", "data.fieldFormats.histogram.title": "直方图", "data.fieldFormats.ip.title": "IP 地址", From 151f5fdee1d8353ab011e5e5e599c0529163e702 Mon Sep 17 00:00:00 2001 From: Kyle Pollich Date: Tue, 1 Jun 2021 09:30:51 -0400 Subject: [PATCH 19/46] Allow for ID in create package policy request (#100908) E2E tests are failing because they include the ID field returned by the package list endpoint. This just updates our request schema to accept an ID, though we don't persist or deal with the ID anywhere. Closes #100897 Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- x-pack/plugins/fleet/server/types/models/package_policy.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index cbf311cac4e3b..3735cfffeaa71 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -82,6 +82,7 @@ const PackagePolicyBaseSchema = { export const NewPackagePolicySchema = schema.object({ ...PackagePolicyBaseSchema, + id: schema.maybe(schema.string()), force: schema.maybe(schema.boolean()), }); From 61a49c6119917c4a292edf7651b893ca16f4cc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20St=C3=BCrmer?= Date: Tue, 1 Jun 2021 15:48:40 +0200 Subject: [PATCH 20/46] [Logs UI] Replace legacy es client usage in category examples (#100716) Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- .../log_entry_categories_analysis.ts | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts index aea946ae87e74..ee2441d591134 100644 --- a/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts +++ b/x-pack/plugins/infra/server/lib/log_analysis/log_entry_categories_analysis.ts @@ -6,7 +6,7 @@ */ import type { estypes } from '@elastic/elasticsearch'; -import type { ILegacyScopedClusterClient } from 'src/core/server'; +import type { ElasticsearchClient } from 'src/core/server'; import { compareDatasetsByMaximumAnomalyScore, getJobId, @@ -136,7 +136,7 @@ export async function getLogEntryCategoryDatasets( export async function getLogEntryCategoryExamples( context: { - core: { elasticsearch: { legacy: { client: ILegacyScopedClusterClient } } }; + core: { elasticsearch: { client: { asCurrentUser: ElasticsearchClient } } }; infra: { mlAnomalyDetectors: MlAnomalyDetectors; mlSystem: MlSystem; @@ -402,7 +402,7 @@ async function fetchTopLogEntryCategoryHistograms( } async function fetchLogEntryCategoryExamples( - requestContext: { core: { elasticsearch: { legacy: { client: ILegacyScopedClusterClient } } } }, + requestContext: { core: { elasticsearch: { client: { asCurrentUser: ElasticsearchClient } } } }, indices: string, runtimeMappings: estypes.RuntimeFields, timestampField: string, @@ -417,19 +417,20 @@ async function fetchLogEntryCategoryExamples( const { hits: { hits }, } = decodeOrThrow(logEntryCategoryExamplesResponseRT)( - await requestContext.core.elasticsearch.legacy.client.callAsCurrentUser( - 'search', - createLogEntryCategoryExamplesQuery( - indices, - runtimeMappings, - timestampField, - tiebreakerField, - startTime, - endTime, - categoryQuery, - exampleCount + ( + await requestContext.core.elasticsearch.client.asCurrentUser.search( + createLogEntryCategoryExamplesQuery( + indices, + runtimeMappings, + timestampField, + tiebreakerField, + startTime, + endTime, + categoryQuery, + exampleCount + ) ) - ) + ).body ); const esSearchSpan = finalizeEsSearchSpan(); From 8ed2add1a2e3311f18413d33b9b2de6ed6286a29 Mon Sep 17 00:00:00 2001 From: Jason Rhodes Date: Tue, 1 Jun 2021 10:58:39 -0400 Subject: [PATCH 21/46] Re-enable _mb suffixed stack monitoring func tests (#98354) * Reenabled _mb suffixed stack monitoring func tests These tests were disabled temporarily in #98238 because of intermittent failures in master. * use test_user instead of basic_monitoring_user * remove security service * remove logout and cleanup Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: neptunian --- .../apps/monitoring/_get_lifecycle_methods.js | 10 ++++---- .../monitoring/enable_monitoring/index.js | 2 +- .../test/functional/apps/monitoring/index.js | 23 ++++++++----------- .../page_objects/monitoring_page.ts | 17 -------------- 4 files changed, 17 insertions(+), 35 deletions(-) diff --git a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js index ae33d275a96b6..fce6fcfff7772 100644 --- a/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js +++ b/x-pack/test/functional/apps/monitoring/_get_lifecycle_methods.js @@ -8,12 +8,15 @@ export const getLifecycleMethods = (getService, getPageObjects) => { const esArchiver = getService('esArchiver'); const security = getService('security'); - const PageObjects = getPageObjects(['monitoring', 'timePicker', 'security']); + const PageObjects = getPageObjects(['monitoring', 'timePicker', 'security', 'common']); let _archive; return { async setup(archive, { from, to, useSuperUser = false }) { _archive = archive; + if (!useSuperUser) { + await security.testUser.setRoles(['monitoring_user', 'kibana_admin']); + } const kibanaServer = getService('kibanaServer'); const browser = getService('browser'); @@ -24,7 +27,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { await esArchiver.load(archive); await kibanaServer.uiSettings.replace({}); - await PageObjects.monitoring.navigateTo(useSuperUser); + await PageObjects.common.navigateToApp('monitoring'); // pause autorefresh in the time filter because we don't wait any ticks, // and we don't want ES to log a warning when data gets wiped out @@ -34,8 +37,7 @@ export const getLifecycleMethods = (getService, getPageObjects) => { }, async tearDown() { - await PageObjects.security.forceLogout(); - await security.user.delete('basic_monitoring_user'); + await security.testUser.restoreDefaults(); return esArchiver.unload(_archive); }, }; diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index cada5ce882760..dca81fd6fc07e 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -22,7 +22,7 @@ export default function ({ getService, getPageObjects }) { before(async () => { const browser = getService('browser'); await browser.setWindowSize(1600, 1000); - await PageObjects.monitoring.navigateTo(true); + await PageObjects.common.navigateToApp('monitoring'); await noData.isOnNoDataPage(); }); diff --git a/x-pack/test/functional/apps/monitoring/index.js b/x-pack/test/functional/apps/monitoring/index.js index 37d5d2083c4b1..24ace88f334f0 100644 --- a/x-pack/test/functional/apps/monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/index.js @@ -8,41 +8,38 @@ export default function ({ loadTestFile }) { describe('Monitoring app', function () { this.tags('ciGroup1'); - loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./cluster/list')); loadTestFile(require.resolve('./cluster/overview')); // loadTestFile(require.resolve('./cluster/license')); - // NOTE: All _mb tests skipped because of various failures: https://github.com/elastic/kibana/issues/98239 - loadTestFile(require.resolve('./elasticsearch/overview')); - // loadTestFile(require.resolve('./elasticsearch/overview_mb')); + loadTestFile(require.resolve('./elasticsearch/overview_mb')); loadTestFile(require.resolve('./elasticsearch/nodes')); - // loadTestFile(require.resolve('./elasticsearch/nodes_mb')); + loadTestFile(require.resolve('./elasticsearch/nodes_mb')); loadTestFile(require.resolve('./elasticsearch/node_detail')); - // loadTestFile(require.resolve('./elasticsearch/node_detail_mb')); + loadTestFile(require.resolve('./elasticsearch/node_detail_mb')); loadTestFile(require.resolve('./elasticsearch/indices')); - // loadTestFile(require.resolve('./elasticsearch/indices_mb')); + loadTestFile(require.resolve('./elasticsearch/indices_mb')); loadTestFile(require.resolve('./elasticsearch/index_detail')); - // loadTestFile(require.resolve('./elasticsearch/index_detail_mb')); + loadTestFile(require.resolve('./elasticsearch/index_detail_mb')); loadTestFile(require.resolve('./elasticsearch/shards')); // loadTestFile(require.resolve('./elasticsearch/shard_activity')); loadTestFile(require.resolve('./kibana/overview')); - // loadTestFile(require.resolve('./kibana/overview_mb')); + loadTestFile(require.resolve('./kibana/overview_mb')); loadTestFile(require.resolve('./kibana/instances')); - // loadTestFile(require.resolve('./kibana/instances_mb')); + loadTestFile(require.resolve('./kibana/instances_mb')); loadTestFile(require.resolve('./kibana/instance')); - // loadTestFile(require.resolve('./kibana/instance_mb')); + loadTestFile(require.resolve('./kibana/instance_mb')); // loadTestFile(require.resolve('./logstash/overview')); // loadTestFile(require.resolve('./logstash/nodes')); // loadTestFile(require.resolve('./logstash/node')); loadTestFile(require.resolve('./logstash/pipelines')); - // loadTestFile(require.resolve('./logstash/pipelines_mb')); + loadTestFile(require.resolve('./logstash/pipelines_mb')); loadTestFile(require.resolve('./beats/cluster')); loadTestFile(require.resolve('./beats/overview')); @@ -53,6 +50,6 @@ export default function ({ loadTestFile }) { loadTestFile(require.resolve('./enable_monitoring')); loadTestFile(require.resolve('./setup/metricbeat_migration')); - // loadTestFile(require.resolve('./setup/metricbeat_migration_mb')); + loadTestFile(require.resolve('./setup/metricbeat_migration_mb')); }); } diff --git a/x-pack/test/functional/page_objects/monitoring_page.ts b/x-pack/test/functional/page_objects/monitoring_page.ts index a499b53c22606..d32528f44613c 100644 --- a/x-pack/test/functional/page_objects/monitoring_page.ts +++ b/x-pack/test/functional/page_objects/monitoring_page.ts @@ -10,24 +10,7 @@ import { FtrProviderContext } from '../ftr_provider_context'; export function MonitoringPageProvider({ getPageObjects, getService }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'header', 'security', 'login']); const testSubjects = getService('testSubjects'); - const security = getService('security'); - return new (class MonitoringPage { - async navigateTo(useSuperUser = false) { - // always create this because our tear down tries to delete it - await security.user.create('basic_monitoring_user', { - password: 'monitoring_user_password', - roles: ['monitoring_user', 'kibana_admin'], - full_name: 'basic monitoring', - }); - - if (!useSuperUser) { - await PageObjects.security.forceLogout(); - await PageObjects.login.login('basic_monitoring_user', 'monitoring_user_password'); - } - await PageObjects.common.navigateToApp('monitoring'); - } - async getAccessDeniedMessage() { return testSubjects.getVisibleText('accessDeniedTitle'); } From 675997731dde84aa6cab5b8a69bee64d5ae0b86c Mon Sep 17 00:00:00 2001 From: gchaps <33642766+gchaps@users.noreply.github.com> Date: Tue, 1 Jun 2021 08:01:02 -0700 Subject: [PATCH 22/46] [DOCS] Remove recommendation of coordinating only node (#100632) (#100954) --- .../production.asciidoc | 58 +------------------ 1 file changed, 2 insertions(+), 56 deletions(-) diff --git a/docs/user/production-considerations/production.asciidoc b/docs/user/production-considerations/production.asciidoc index 726747d5d69d0..1ffca4b6ae6ab 100644 --- a/docs/user/production-considerations/production.asciidoc +++ b/docs/user/production-considerations/production.asciidoc @@ -8,7 +8,6 @@ * <> * <> * <> -* <> * <> * <> * <> @@ -22,9 +21,8 @@ Kibana instances that are all connected to the same Elasticsearch instance. While Kibana isn't terribly resource intensive, we still recommend running Kibana separate from your Elasticsearch data or master nodes. To distribute Kibana -traffic across the nodes in your Elasticsearch cluster, you can run Kibana -and an Elasticsearch client node on the same machine. For more information, see -<>. +traffic across the nodes in your Elasticsearch cluster, +you can configure Kibana to use a list of Elasticsearch hosts. [float] [[configuring-kibana-shield]] @@ -69,58 +67,6 @@ csp.strict: true See <>. -[float] -[[load-balancing-es]] -=== Load Balancing across multiple {es} nodes -If you have multiple nodes in your Elasticsearch cluster, the easiest way to distribute Kibana requests -across the nodes is to run an Elasticsearch _Coordinating only_ node on the same machine as Kibana. -Elasticsearch Coordinating only nodes are essentially smart load balancers that are part of the cluster. They -process incoming HTTP requests, redirect operations to the other nodes in the cluster as needed, and -gather and return the results. For more information, see -{ref}/modules-node.html[Node] in the Elasticsearch reference. - -To use a local client node to load balance Kibana requests: - -. Install Elasticsearch on the same machine as Kibana. -. Configure the node as a Coordinating only node. In `elasticsearch.yml`, set `node.data`, `node.master` and `node.ingest` to `false`: -+ -[source,js] --------- -# 3. You want this node to be neither master nor data node nor ingest node, but -# to act as a "search load balancer" (fetching data from nodes, -# aggregating results, etc.) -# -node.master: false -node.data: false -node.ingest: false --------- -. Configure the client node to join your Elasticsearch cluster. In `elasticsearch.yml`, set the `cluster.name` to the -name of your cluster. -+ -[source,js] --------- -cluster.name: "my_cluster" --------- -. Check your transport and HTTP host configs in `elasticsearch.yml` under `network.host` and `transport.host`. The `transport.host` needs to be on the network reachable to the cluster members, the `network.host` is the network for the HTTP connection for Kibana (localhost:9200 by default). -+ -[source,js] --------- -network.host: localhost -http.port: 9200 - -# by default transport.host refers to network.host -transport.host: -transport.tcp.port: 9300 - 9400 --------- -. Make sure Kibana is configured to point to your local client node. In `kibana.yml`, the `elasticsearch.hosts` setting should be set to -`["localhost:9200"]`. -+ -[source,js] --------- -# The Elasticsearch instance to use for all your queries. -elasticsearch.hosts: ["http://localhost:9200"] --------- - [float] [[load-balancing-kibana]] === Load balancing across multiple Kibana instances From 9734d1cfe8baff1879a2240456159c9b2033c63d Mon Sep 17 00:00:00 2001 From: Matthew Kime Date: Tue, 1 Jun 2021 10:34:06 -0500 Subject: [PATCH 23/46] [Index patterns] Deprecate scripted field API functions (#100907) * deprecate scripted field functions in index patterns api --- ...lugins-data-public.indexpattern.addscriptedfield.md | 4 ++++ ...ns-data-public.indexpattern.getnonscriptedfields.md | 4 ++++ ...ugins-data-public.indexpattern.getscriptedfields.md | 4 ++++ ...ins-data-public.indexpattern.removescriptedfield.md | 4 ++++ ...lugins-data-server.indexpattern.addscriptedfield.md | 4 ++++ ...ns-data-server.indexpattern.getnonscriptedfields.md | 4 ++++ ...ugins-data-server.indexpattern.getscriptedfields.md | 4 ++++ ...ins-data-server.indexpattern.removescriptedfield.md | 4 ++++ .../index_patterns/index_patterns/index_pattern.ts | 10 ++++++++++ src/plugins/data/public/public.api.md | 6 ++++-- src/plugins/data/server/server.api.md | 6 ++++-- 11 files changed, 50 insertions(+), 4 deletions(-) diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md index 99d2fc00a6b7b..812f014b15a6c 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.addscriptedfield.md @@ -4,6 +4,10 @@ ## IndexPattern.addScriptedField() method +> Warning: This API is now obsolete. +> +> + Add scripted field to field list Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md index 77ce6f6f23a67..1792a979bf749 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getnonscriptedfields.md @@ -4,6 +4,10 @@ ## IndexPattern.getNonScriptedFields() method +> Warning: This API is now obsolete. +> +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md index 055f07367c96e..b6b3dcb19bac1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.getscriptedfields.md @@ -4,6 +4,10 @@ ## IndexPattern.getScriptedFields() method +> Warning: This API is now obsolete. +> +> + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md index aaaebdaccca5d..91f25c09ab197 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md @@ -4,6 +4,10 @@ ## IndexPattern.removeScriptedField() method +> Warning: This API is now obsolete. +> +> + Remove scripted field from field list Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md index a86fea3106225..981f28a51ae09 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.addscriptedfield.md @@ -4,6 +4,10 @@ ## IndexPattern.addScriptedField() method +> Warning: This API is now obsolete. +> +> + Add scripted field to field list Signature: diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md index 89d79d9b750fa..cff2c5de98de6 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getnonscriptedfields.md @@ -4,6 +4,10 @@ ## IndexPattern.getNonScriptedFields() method +> Warning: This API is now obsolete. +> +> + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md index edfff8ec5efac..62b8f1b62ac78 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.getscriptedfields.md @@ -4,6 +4,10 @@ ## IndexPattern.getScriptedFields() method +> Warning: This API is now obsolete. +> +> + Signature: ```typescript diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md index 3162a7f42dd12..f6beed7389e43 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.indexpattern.removescriptedfield.md @@ -4,6 +4,10 @@ ## IndexPattern.removeScriptedField() method +> Warning: This API is now obsolete. +> +> + Remove scripted field from field list Signature: diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 41ce7ba4bab4a..1552bed210e8c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -240,6 +240,7 @@ export class IndexPattern implements IIndexPattern { * @param script script code * @param fieldType * @param lang + * @deprecated use runtime field instead */ async addScriptedField(name: string, script: string, fieldType: string = 'string') { const scriptedFields = this.getScriptedFields(); @@ -265,6 +266,7 @@ export class IndexPattern implements IIndexPattern { /** * Remove scripted field from field list * @param fieldName + * @deprecated use runtime field instead */ removeScriptedField(fieldName: string) { @@ -274,10 +276,18 @@ export class IndexPattern implements IIndexPattern { } } + /** + * + * @deprecated use runtime field instead + */ getNonScriptedFields() { return [...this.fields.getAll().filter((field) => !field.scripted)]; } + /** + * + * @deprecated use runtime field instead + */ getScriptedFields() { return [...this.fields.getAll().filter((field) => field.scripted)]; } diff --git a/src/plugins/data/public/public.api.md b/src/plugins/data/public/public.api.md index fde7075d9e760..069b0a21c9c77 100644 --- a/src/plugins/data/public/public.api.md +++ b/src/plugins/data/public/public.api.md @@ -1307,6 +1307,7 @@ export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addRuntimeField(name: string, runtimeField: RuntimeField): void; + // @deprecated addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -1366,7 +1367,7 @@ export class IndexPattern implements IIndexPattern { getFieldByName(name: string): IndexPatternField | undefined; getFormatterForField(field: IndexPatternField | IndexPatternField['spec'] | IFieldType): FieldFormat; getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; - // (undocumented) + // @deprecated (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { fieldAttrs?: string | undefined; @@ -1379,7 +1380,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; - // (undocumented) + // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { excludes: any[]; @@ -1397,6 +1398,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) metaFields: string[]; removeRuntimeField(name: string): void; + // @deprecated removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index 4abf430252164..4f3802cda7a3a 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -749,6 +749,7 @@ export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts constructor({ spec, fieldFormats, shortDotsEnable, metaFields, }: IndexPatternDeps); addRuntimeField(name: string, runtimeField: RuntimeField): void; + // @deprecated addScriptedField(name: string, script: string, fieldType?: string): Promise; readonly allowNoIndex: boolean; // (undocumented) @@ -812,7 +813,7 @@ export class IndexPattern implements IIndexPattern { getFormatterForFieldNoDefault(fieldname: string): FieldFormat | undefined; // Warning: (ae-forgotten-export) The symbol "IndexPatternField" needs to be exported by the entry point index.d.ts // - // (undocumented) + // @deprecated (undocumented) getNonScriptedFields(): IndexPatternField[]; getOriginalSavedObjectBody: () => { fieldAttrs?: string | undefined; @@ -825,7 +826,7 @@ export class IndexPattern implements IIndexPattern { typeMeta?: string | undefined; type?: string | undefined; }; - // (undocumented) + // @deprecated (undocumented) getScriptedFields(): IndexPatternField[]; getSourceFiltering(): { excludes: any[]; @@ -843,6 +844,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) metaFields: string[]; removeRuntimeField(name: string): void; + // @deprecated removeScriptedField(fieldName: string): void; resetOriginalSavedObjectBody: () => void; // (undocumented) From 38fd8a268ad7661d92f0d84c52d6f0a3d84c9801 Mon Sep 17 00:00:00 2001 From: Greg Thompson Date: Tue, 1 Jun 2021 10:53:07 -0500 Subject: [PATCH 24/46] Upgrade EUI to v33.0.0 (#99382) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * eui to 33.0.0 * resize observer type inclusion - revisit * src snapshot updates * x-pack snapshot updates * table sort test updates * code block language sh -> bash * datagrid datetime sort inversion * types * kbn-crypto * refractor yarn resolution * refractor yarn resolution * update cypress tests * url state test * trial * Revert "trial" This reverts commit adc3538145d613279445f0c93308ed712adb7611. * trial anomaly timeout * Revert "trial anomaly timeout" This reverts commit 9a11711ba898b51d9f2979fa64f8907759b33fe4. * kbn-telemetry-tools * Change a useMemo to useCallback so the code executes when intended * Removed no-longer-used import * exitOrFail already retries for longer than tryForTime * Wait for loading indicator to disappear * Intentionally adding `.only` * Revert .only * Increase wait time for the ML chart to load * Remove unused var * overflow * chartWidth Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Alejandro Fernández Haro Co-authored-by: Chandler Prall --- package.json | 3 +- packages/kbn-crypto/BUILD.bazel | 1 + packages/kbn-telemetry-tools/BUILD.bazel | 1 + .../collapsible_nav.test.tsx.snap | 24 +- .../header/__snapshots__/header.test.tsx.snap | 14 +- .../flyout_service.test.tsx.snap | 4 +- .../__snapshots__/modal_service.test.tsx.snap | 6 +- .../__snapshots__/data_view.test.tsx.snap | 153 +++---- ...ver_index_pattern_management.test.tsx.snap | 2 + .../components/tutorial/instruction.js | 2 +- .../url/__snapshots__/url.test.tsx.snap | 8 +- .../warning_call_out.test.tsx.snap | 2 + .../inspector_panel.test.tsx.snap | 3 + .../__snapshots__/header.test.tsx.snap | 4 + .../__snapshots__/intro.test.tsx.snap | 2 + .../not_found_errors.test.tsx.snap | 8 + .../public/components/vega_vis_component.tsx | 4 +- test/functional/services/data_grid.ts | 4 +- test/tsconfig.json | 2 +- tsconfig.base.json | 3 +- .../List/__snapshots__/List.test.tsx.snap | 64 ++- .../ServiceList/service_list.test.tsx | 8 +- .../TransactionActionMenu.test.tsx.snap | 1 + .../time_filter.stories.storyshot | 15 +- .../__snapshots__/asset.stories.storyshot | 8 + .../asset_manager.stories.storyshot | 10 + .../color_manager.stories.storyshot | 8 + .../color_picker.stories.storyshot | 8 + .../custom_element_modal.stories.storyshot | 4 + .../datasource_component.stories.storyshot | 2 + .../keyboard_shortcuts_doc.stories.storyshot | 1 + .../element_controls.stories.storyshot | 2 + .../element_grid.stories.storyshot | 6 + .../saved_elements_modal.stories.storyshot | 11 + .../sidebar_header.stories.storyshot | 4 + .../text_style_picker.stories.storyshot | 12 + .../__snapshots__/toolbar.stories.storyshot | 7 + .../delete_var.stories.storyshot | 1 + .../__snapshots__/edit_var.stories.storyshot | 7 + .../var_config.stories.storyshot | 1 + .../element_menu.stories.storyshot | 1 + .../workpad_templates.stories.storyshot | 20 +- .../api/__snapshots__/shareable.test.tsx.snap | 10 +- .../__snapshots__/canvas.stories.storyshot | 9 + .../__snapshots__/footer.stories.storyshot | 6 + .../page_controls.stories.storyshot | 6 + .../__snapshots__/settings.test.tsx.snap | 10 +- .../__snapshots__/settings.stories.storyshot | 2 + .../components/table/table.test.tsx | 10 +- .../extend_index_management.test.tsx.snap | 3 + .../__snapshots__/policy_table.test.tsx.snap | 1 + .../infra/public/components/auto_sizer.tsx | 2 +- .../upload_license.test.tsx.snap | 9 + .../embeddable_anomaly_charts_container.tsx | 8 +- .../nodes/__snapshots__/cells.test.js.snap | 2 + .../report_listing.test.tsx.snap | 288 ++++++++++---- .../report_info_button.test.tsx.snap | 10 + .../timelines/flyout_button.spec.ts | 2 +- .../cypress/integration/urls/state.spec.ts | 1 + .../cypress/tasks/timeline.ts | 1 + .../components/hosts_table/index.test.tsx | 8 +- .../trusted_app_deletion_dialog.test.tsx.snap | 3 + .../__snapshots__/index.test.tsx.snap | 372 ++++++++--------- .../__snapshots__/index.test.tsx.snap | 376 +++++++++++++----- .../index.test.tsx | 8 +- .../network_top_n_flow_table/index.test.tsx | 8 +- .../components/tls_table/index.test.tsx | 2 +- .../components/users_table/index.test.tsx | 4 +- .../__snapshots__/index.test.tsx.snap | 8 + .../add_data_provider_popover.tsx | 1 - .../fingerprint_col.test.tsx.snap | 2 + .../uptime_date_picker.test.tsx.snap | 2 + .../__snapshots__/license_info.test.tsx.snap | 1 + .../ml/__snapshots__/ml_flyout.test.tsx.snap | 2 + .../availability_reporting.test.tsx.snap | 12 +- .../location_status_tags.test.tsx.snap | 38 +- .../__snapshots__/monitor_list.test.tsx.snap | 37 +- .../services/ml/dashboard_embeddables.ts | 2 +- yarn.lock | 57 +-- 79 files changed, 1152 insertions(+), 622 deletions(-) diff --git a/package.json b/package.json index e5b9ca1ef98cc..9a076ee28bc04 100644 --- a/package.json +++ b/package.json @@ -86,6 +86,7 @@ "**/prismjs": "1.23.0", "**/react-syntax-highlighter": "^15.3.1", "**/react-syntax-highlighter/**/highlight.js": "^10.4.1", + "**/refractor": "^3.3.1", "**/request": "^2.88.2", "**/trim": "1.0.1", "**/typescript": "4.1.3", @@ -102,7 +103,7 @@ "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath/npm_module", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.4", "@elastic/ems-client": "7.13.0", - "@elastic/eui": "32.1.0", + "@elastic/eui": "33.0.0", "@elastic/filesaver": "1.1.2", "@elastic/good": "^9.0.1-kibana3", "@elastic/maki": "6.3.0", diff --git a/packages/kbn-crypto/BUILD.bazel b/packages/kbn-crypto/BUILD.bazel index b1723e4120e79..20793e27de629 100644 --- a/packages/kbn-crypto/BUILD.bazel +++ b/packages/kbn-crypto/BUILD.bazel @@ -38,6 +38,7 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/node-forge", "@npm//@types/testing-library__jest-dom", + "@npm//resize-observer-polyfill" ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/packages/kbn-telemetry-tools/BUILD.bazel b/packages/kbn-telemetry-tools/BUILD.bazel index 9a6b4a10bd190..d394b0c93d45f 100644 --- a/packages/kbn-telemetry-tools/BUILD.bazel +++ b/packages/kbn-telemetry-tools/BUILD.bazel @@ -47,6 +47,7 @@ TYPES_DEPS = [ "@npm//@types/node", "@npm//@types/normalize-path", "@npm//@types/testing-library__jest-dom", + "@npm//resize-observer-polyfill" ] DEPS = SRC_DEPS + TYPES_DEPS diff --git a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap index 575a247ffeccb..0f5efe667ec2f 100644 --- a/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap +++ b/src/core/public/chrome/ui/header/__snapshots__/collapsible_nav.test.tsx.snap @@ -587,10 +587,12 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` > @@ -1921,6 +1923,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` > @@ -1999,6 +2004,7 @@ exports[`CollapsibleNav renders links grouped by category 1`] = ` @@ -3084,6 +3094,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` > @@ -3162,6 +3175,7 @@ exports[`CollapsibleNav renders the default nav 3`] = ` @@ -5469,6 +5473,7 @@ exports[`Header renders 1`] = ` > @@ -5547,6 +5554,7 @@ exports[`Header renders 1`] = `
Flyout content
"`; +exports[`FlyoutService openFlyout() renders a flyout to the DOM 2`] = `"
Flyout content
"`; exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 1`] = ` Array [ @@ -59,4 +59,4 @@ Array [ ] `; -exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; +exports[`FlyoutService openFlyout() with a currently active flyout replaces the current flyout with a new one 2`] = `"
Flyout content 2
"`; diff --git a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap index 19ebb5a9113c3..9c39776fcea5c 100644 --- a/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap +++ b/src/core/public/overlays/modal/__snapshots__/modal_service.test.tsx.snap @@ -29,7 +29,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; +exports[`ModalService openConfirm() renders a mountpoint confirm message 2`] = `"
Modal content
"`; exports[`ModalService openConfirm() renders a string confirm message 1`] = ` Array [ @@ -49,7 +49,7 @@ Array [ ] `; -exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; +exports[`ModalService openConfirm() renders a string confirm message 2`] = `"

Some message

"`; exports[`ModalService openConfirm() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ @@ -131,7 +131,7 @@ Array [ ] `; -exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; +exports[`ModalService openModal() renders a modal to the DOM 2`] = `"
Modal content
"`; exports[`ModalService openModal() with a currently active confirm replaces the current confirm with the new one 1`] = ` Array [ diff --git a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap index 9896a6dbdc7b7..a0a7e54d27532 100644 --- a/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap +++ b/src/plugins/data/public/utils/table_inspector_view/components/__snapshots__/data_view.test.tsx.snap @@ -718,11 +718,13 @@ exports[`Inspector Data View component should render single table without select > @@ -996,6 +998,7 @@ exports[`Inspector Data View component should render single table without select - - - - - column1 - - - - - - - Click to sort in ascending order - + + + + column1 + + + - + @@ -1322,6 +1320,7 @@ exports[`Inspector Data View component should render single table without select
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with height specified 1`] = `"
"`; @@ -21,7 +21,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with height specified
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with page specified 1`] = `"
"`; @@ -33,7 +33,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with page specified 2`
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width and height specified 1`] = `"
"`; @@ -45,7 +45,7 @@ exports[`Canvas Shareable Workpad API Placed successfully with width and height
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; exports[`Canvas Shareable Workpad API Placed successfully with width specified 1`] = `"
"`; @@ -57,5 +57,5 @@ exports[`Canvas Shareable Workpad API Placed successfully with width specified 2
markdown mock
markdown mock
My Canvas Workpad
" +
markdown mock
My Canvas Workpad
" `; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot index 3432e479bff97..d23371d0c3499 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot +++ b/x-pack/plugins/canvas/shareable_runtime/components/__stories__/__snapshots__/canvas.stories.storyshot @@ -1383,6 +1383,7 @@ exports[`Storyshots shareables/Canvas component 1`] = ` -
Value -
+ @@ -6536,6 +6542,7 @@ exports[`TrustedAppsGrid renders correctly when loading data for the second time > -
Value -
+
@@ -7218,7 +7227,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -7507,7 +7516,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -7796,7 +7805,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -8085,7 +8094,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -8374,7 +8383,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -8663,7 +8672,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -8952,7 +8961,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -9241,7 +9250,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -9530,7 +9539,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not scope="col" style="width: 30%;" > -
Field -
+ -
Operator -
+ -
Value -
+ @@ -9688,6 +9697,7 @@ exports[`TrustedAppsGrid renders correctly when new page and page size set (not > -
Date Created -
+
-
Created By -
+ -
Actions -
+ -
-
+ @@ -195,7 +195,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the firs role="columnheader" scope="col" > -
Name -
+ -
OS -
+ -
Date Created -
+ -
Created By -
+ -
Actions -
+ -
-
+ @@ -358,7 +358,7 @@ exports[`TrustedAppsList renders correctly when failed loading data for the seco role="columnheader" scope="col" > -
Name -
+ -
OS -
+ -
Date Created -
+ -
Created By -
+ -
Actions -
+ -
-
+ @@ -533,7 +533,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` role="columnheader" scope="col" > -
Name -
+ -
OS -
+ -
Date Created -
+ -
Created By -
+ -
Actions -
+ -
-
+ @@ -726,6 +726,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = ` -
Value -
+ @@ -1157,6 +1159,7 @@ exports[`TrustedAppsList renders correctly when item details expanded 1`] = `