diff --git a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts index 27d8849bd2585..d9561fa1845d6 100644 --- a/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts +++ b/src/platform/packages/shared/kbn-management/settings/setting_ids/index.ts @@ -51,10 +51,7 @@ export const TIMEPICKER_REFRESH_INTERVAL_DEFAULTS_ID = 'timepicker:refreshInterv export const TIMEPICKER_TIME_DEFAULTS_ID = 'timepicker:timeDefaults'; // Presentation labs settings -export const LABS_CANVAS_BY_VALUE_EMBEDDABLE_ID = 'labs:canvas:byValueEmbeddable'; -export const LABS_CANVAS_ENABLE_UI_ID = 'labs:canvas:enable_ui'; export const LABS_DASHBOARD_DEFER_BELOW_FOLD_ID = 'labs:dashboard:deferBelowFold'; -export const LABS_DASHBOARDS_ENABLE_UI_ID = 'labs:dashboard:enable_ui'; // Accessibility settings export const ACCESSIBILITY_DISABLE_ANIMATIONS_ID = 'accessibility:disableAnimations'; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/index.ts b/src/platform/packages/shared/presentation/presentation_publishing/index.ts index 1e8d4d779d4e2..11e101eded214 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/index.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/index.ts @@ -26,11 +26,25 @@ export { getViewModeSubject, type CanAccessViewMode, } from './interfaces/can_access_view_mode'; +export { + type PublishesPauseFetch, + apiPublishesPauseFetch, +} from './interfaces/fetch/publishes_pause_fetch'; export { apiCanLockHoverActions, type CanLockHoverActions, } from './interfaces/can_lock_hover_actions'; -export { fetch$, useFetchContext, type FetchContext } from './interfaces/fetch/fetch'; +export { fetch$, useFetchContext } from './interfaces/fetch/fetch'; +export type { FetchContext } from './interfaces/fetch/fetch_context'; +export { + type FetchSetting, + type PublishesFetchSetting, + type PublishesIsVisible, + apiPublishesIsVisible, + apiPublishesFetchSetting, + onVisibilityChange, + initializeVisibility, +} from './interfaces/fetch/fetch_only_visible'; export { initializeTimeRangeManager, timeRangeComparators, diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts index bb4682c78e62e..eef95845a7304 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.test.ts @@ -131,6 +131,58 @@ describe('onFetchContextChanged', () => { }); }); + describe('with isFetchPaused$', () => { + test('should skip emits while fetch is paused', async () => { + const isFetchPaused$ = new BehaviorSubject(true); + const api = { + parentApi, + isFetchPaused$, + }; + const subscription = fetch$(api).subscribe(onFetchMock); + + parentApi.filters$.next([]); + parentApi.query$.next({ language: 'kquery', query: 'hello' }); + parentApi.reload$.next(); + + await new Promise((resolve) => setTimeout(resolve, 0)); + expect(onFetchMock).not.toHaveBeenCalled(); + + subscription.unsubscribe(); + }); + + test('should emit most recent context when fetch becomes un-paused', async () => { + const isFetchPaused$ = new BehaviorSubject(true); + const api = { + parentApi, + isFetchPaused$, + }; + const subscription = fetch$(api).subscribe(onFetchMock); + + parentApi.filters$.next([]); + parentApi.query$.next({ language: 'kquery', query: '' }); + parentApi.reload$.next(); + + isFetchPaused$.next(false); + + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(onFetchMock).toHaveBeenCalledTimes(1); + const fetchContext = onFetchMock.mock.calls[0][0]; + expect(fetchContext).toEqual({ + filters: [], + isReload: true, + query: { + language: 'kquery', + query: '', + }, + searchSessionId: undefined, + timeRange: undefined, + timeslice: undefined, + }); + + subscription.unsubscribe(); + }); + }); + describe('no searchSession$', () => { test('should emit once on reload', async () => { const subscription = fetch$({ parentApi }).pipe(skip(1)).subscribe(onFetchMock); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts index 4d2488cae41e6..53d9c1389960e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch.ts @@ -7,49 +7,49 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Observable } from 'rxjs'; +import { useEffect, useMemo } from 'react'; import { BehaviorSubject, + Subject, combineLatest, + combineLatestWith, debounceTime, delay, + distinctUntilChanged, filter, map, merge, of, skip, startWith, - Subject, switchMap, takeUntil, tap, + type Observable, } from 'rxjs'; -import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; -import { useMemo, useEffect } from 'react'; -import type { PublishesTimeRange, PublishesUnifiedSearch } from './publishes_unified_search'; -import { apiPublishesTimeRange, apiPublishesUnifiedSearch } from './publishes_unified_search'; -import type { PublishesSearchSession } from './publishes_search_session'; -import { apiPublishesSearchSession } from './publishes_search_session'; -import type { HasParentApi } from '../has_parent_api'; -import { apiHasParentApi } from '../has_parent_api'; -import { apiPublishesReload } from './publishes_reload'; import { useStateFromPublishingSubject } from '../../publishing_subject'; +import { apiHasParentApi, type HasParentApi } from '../has_parent_api'; +import { + type FetchContext, + type ReloadTimeFetchContext, + isReloadTimeFetchContextEqual, +} from './fetch_context'; +import { apiPublishesPauseFetch } from './publishes_pause_fetch'; +import { apiPublishesReload } from './publishes_reload'; +import { apiPublishesSearchSession, type PublishesSearchSession } from './publishes_search_session'; +import { + apiPublishesTimeRange, + apiPublishesUnifiedSearch, + type PublishesTimeRange, + type PublishesUnifiedSearch, +} from './publishes_unified_search'; -export interface FetchContext { - isReload: boolean; - filters: Filter[] | undefined; - query: Query | AggregateQuery | undefined; - searchSessionId: string | undefined; - timeRange: TimeRange | undefined; - timeslice: [number, number] | undefined; -} - -function getFetchContext(api: unknown, isReload: boolean) { +function getReloadTimeFetchContext(api: unknown, reloadTimestamp?: number): ReloadTimeFetchContext { const typeApi = api as Partial< PublishesTimeRange & HasParentApi> >; return { - isReload, + reloadTimestamp, filters: typeApi?.parentApi?.filters$?.value, query: typeApi?.parentApi?.query$?.value, searchSessionId: typeApi?.parentApi?.searchSessionId$?.value, @@ -121,36 +121,60 @@ export function fetch$(api: unknown): Observable { const batchedObservables = getBatchedObservables(api); const immediateObservables = getImmediateObservables(api); - if (immediateObservables.length === 0) { - return merge(...batchedObservables).pipe( - startWith(getFetchContext(api, false)), - debounceTime(0), - map(() => getFetchContext(api, false)) - ); - } - - const interrupt = new Subject(); - const batchedChanges$ = merge(...batchedObservables).pipe( - switchMap((value) => - of(value).pipe( - delay(0), - takeUntil(interrupt), - map(() => getFetchContext(api, false)) + const fetchContext$ = (() => { + if (immediateObservables.length === 0) { + return merge(...batchedObservables).pipe( + startWith(getReloadTimeFetchContext(api)), + debounceTime(0), + map(() => getReloadTimeFetchContext(api)) + ); + } + const interrupt = new Subject(); + const batchedChanges$ = merge(...batchedObservables).pipe( + switchMap((value) => + of(value).pipe( + delay(0), + takeUntil(interrupt), + map(() => getReloadTimeFetchContext(api)) + ) ) - ) - ); + ); - const immediateChange$ = merge(...immediateObservables).pipe( - tap(() => interrupt.next()), - map(() => getFetchContext(api, true)) + const immediateChange$ = merge(...immediateObservables).pipe( + tap(() => { + interrupt.next(); + }), + map(() => getReloadTimeFetchContext(api, Date.now())) + ); + return merge(immediateChange$, batchedChanges$).pipe(startWith(getReloadTimeFetchContext(api))); + })(); + + const isFetchPaused$ = apiPublishesPauseFetch(api) ? api.isFetchPaused$ : of(false); + + return fetchContext$.pipe( + combineLatestWith(isFetchPaused$), + filter(([, isFetchPaused]) => !isFetchPaused), + map(([fetchContext]) => fetchContext), + distinctUntilChanged((prevContext, nextContext) => + isReloadTimeFetchContextEqual(prevContext, nextContext) + ), + map((reloadTimeFetchContext) => ({ + isReload: Boolean(reloadTimeFetchContext.reloadTimestamp), + filters: reloadTimeFetchContext.filters, + query: reloadTimeFetchContext.query, + timeRange: reloadTimeFetchContext.timeRange, + timeslice: reloadTimeFetchContext.timeslice, + searchSessionId: reloadTimeFetchContext.searchSessionId, + })) ); - - return merge(immediateChange$, batchedChanges$).pipe(startWith(getFetchContext(api, false))); } export const useFetchContext = (api: unknown): FetchContext => { const context$: BehaviorSubject = useMemo(() => { - return new BehaviorSubject(getFetchContext(api, false)); + return new BehaviorSubject({ + ...getReloadTimeFetchContext(api), + isReload: false, + }); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts new file mode 100644 index 0000000000000..d090c55d43f08 --- /dev/null +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_context.ts @@ -0,0 +1,71 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; +import { COMPARE_ALL_OPTIONS, onlyDisabledFiltersChanged } from '@kbn/es-query'; +import fastIsEqual from 'fast-deep-equal'; + +export interface FetchContext { + isReload: boolean; + filters: Filter[] | undefined; + query: Query | AggregateQuery | undefined; + searchSessionId: string | undefined; + timeRange: TimeRange | undefined; + timeslice: [number, number] | undefined; +} + +export interface ReloadTimeFetchContext extends Omit { + reloadTimestamp?: number; +} + +export function isReloadTimeFetchContextEqual( + currentContext: ReloadTimeFetchContext, + lastContext: ReloadTimeFetchContext +): boolean { + if (currentContext.searchSessionId !== lastContext.searchSessionId) return false; + + return ( + isReloadTimestampEqualForFetch(currentContext.reloadTimestamp, lastContext.reloadTimestamp) && + areFiltersEqualForFetch(currentContext.filters, lastContext.filters) && + isQueryEqualForFetch(currentContext.query, lastContext.query) && + isTimeRangeEqualForFetch(currentContext.timeRange, lastContext.timeRange) && + isTimeSliceEqualForFetch(currentContext.timeslice, lastContext.timeslice) + ); +} + +export const areFiltersEqualForFetch = (currentFilters?: Filter[], lastFilters?: Filter[]) => { + return onlyDisabledFiltersChanged(currentFilters, lastFilters, { + ...COMPARE_ALL_OPTIONS, + // do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results) + state: false, + }); +}; + +export const isReloadTimestampEqualForFetch = ( + currentReloadTimestamp?: number, + lastReloadTimestamp?: number +) => { + if (!currentReloadTimestamp) return true; // if current reload timestamp is not set, this is not a force refresh. + return currentReloadTimestamp === lastReloadTimestamp; +}; + +export const isQueryEqualForFetch = ( + currentQuery: Query | AggregateQuery | undefined, + lastQuery: Query | AggregateQuery | undefined +) => fastIsEqual(currentQuery, lastQuery); + +export const isTimeRangeEqualForFetch = ( + currentTimeRange: TimeRange | undefined, + lastTimeRange: TimeRange | undefined +) => fastIsEqual(currentTimeRange, lastTimeRange); + +export const isTimeSliceEqualForFetch = ( + currentTimeslice: [number, number] | undefined, + lastTimeslice: [number, number] | undefined +) => fastIsEqual(currentTimeslice, lastTimeslice); diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_only_visible.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_only_visible.ts new file mode 100644 index 0000000000000..0cab7d5fffadf --- /dev/null +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/fetch_only_visible.ts @@ -0,0 +1,58 @@ +/* + * 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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public + * License v3.0 only", or the "Server Side Public License, v 1". + */ + +import { BehaviorSubject, skip, type Observable } from 'rxjs'; +import { combineLatestWith, map } from 'rxjs'; +import type { PublishesPauseFetch } from '../..'; + +export type FetchSetting = 'onlyVisible' | 'always'; + +/** + * Parent APIs can publish a fetch setting that determines when child components should fetch data. + */ +export interface PublishesFetchSetting { + fetchSetting$: Observable; +} + +export const apiPublishesFetchSetting = ( + unknownApi?: unknown +): unknownApi is PublishesFetchSetting => { + return Boolean(unknownApi && (unknownApi as PublishesFetchSetting)?.fetchSetting$ !== undefined); +}; + +export interface PublishesIsVisible { + isVisible$: BehaviorSubject; +} + +export const apiPublishesIsVisible = (unknownApi?: unknown): unknownApi is PublishesIsVisible => { + return Boolean(unknownApi && (unknownApi as PublishesIsVisible)?.isVisible$ !== undefined); +}; + +export const onVisibilityChange = (api: unknown, isVisible: boolean) => { + if (apiPublishesIsVisible(api)) api.isVisible$.next(isVisible); +}; + +export const initializeVisibility = ( + parentApi: unknown +): (PublishesPauseFetch & PublishesIsVisible) | {} => { + if (!apiPublishesFetchSetting(parentApi)) return {}; + + const isVisible$ = new BehaviorSubject(false); + const isFetchPaused$ = parentApi.fetchSetting$.pipe( + combineLatestWith(isVisible$.pipe(skip(1))), + map(([parentFetchSetting, isVisible]) => { + if (parentFetchSetting === 'onlyVisible') return !isVisible; + return false; // If the fetch setting is 'always', we do not pause the fetch + }) + ); + return { + isVisible$, + isFetchPaused$, + }; +}; diff --git a/src/platform/plugins/shared/presentation_util/server/index.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts similarity index 58% rename from src/platform/plugins/shared/presentation_util/server/index.ts rename to src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts index 497c18fae711d..154ef639b2fbf 100644 --- a/src/platform/plugins/shared/presentation_util/server/index.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_pause_fetch.ts @@ -7,8 +7,14 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export { SETTING_CATEGORY } from './ui_settings'; -export const plugin = async () => { - const { PresentationUtilPlugin } = await import('./plugin'); - return new PresentationUtilPlugin(); +import type { Observable } from 'rxjs'; + +export interface PublishesPauseFetch { + isFetchPaused$: Observable; +} + +export const apiPublishesPauseFetch = ( + unknownApi: null | unknown +): unknownApi is PublishesPauseFetch => { + return Boolean(unknownApi && (unknownApi as PublishesPauseFetch)?.isFetchPaused$ !== undefined); }; diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts index cfd8c11340028..56d0e1863be1e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_reload.ts @@ -10,7 +10,7 @@ import type { Observable } from 'rxjs'; export interface PublishesReload { - reload$: Omit, 'next'>; + reload$: Observable; } export const apiPublishesReload = (unknownApi: null | unknown): unknownApi is PublishesReload => { diff --git a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts index 5f8c335cb7ffe..736bd5452947e 100644 --- a/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts +++ b/src/platform/packages/shared/presentation/presentation_publishing/interfaces/fetch/publishes_unified_search.ts @@ -8,11 +8,14 @@ */ import type { AggregateQuery, Filter, Query, TimeRange } from '@kbn/es-query'; -import { COMPARE_ALL_OPTIONS, onlyDisabledFiltersChanged } from '@kbn/es-query'; -import fastIsEqual from 'fast-deep-equal'; import { useEffect, useMemo } from 'react'; import { BehaviorSubject } from 'rxjs'; import type { PublishingSubject } from '../../publishing_subject'; +import { + areFiltersEqualForFetch, + isQueryEqualForFetch, + isTimeRangeEqualForFetch, +} from './fetch_context'; export interface PublishesTimeslice { timeslice$: PublishingSubject<[number, number] | undefined>; @@ -115,25 +118,19 @@ export function useSearchApi({ }, []); useEffect(() => { - if ( - !onlyDisabledFiltersChanged(searchApi.filters$.getValue(), filters, { - ...COMPARE_ALL_OPTIONS, - // do not compare $state to avoid refreshing when filter is pinned/unpinned (which does not impact results) - state: false, - }) - ) { + if (!areFiltersEqualForFetch(searchApi.filters$.getValue(), filters)) { searchApi.filters$.next(filters); } }, [filters, searchApi.filters$]); useEffect(() => { - if (!fastIsEqual(searchApi.query$.getValue(), query)) { + if (!isQueryEqualForFetch(searchApi.query$.getValue(), query)) { searchApi.query$.next(query); } }, [query, searchApi.query$]); useEffect(() => { - if (!fastIsEqual(searchApi.timeRange$.getValue(), timeRange)) { + if (!isTimeRangeEqualForFetch(searchApi.timeRange$.getValue(), timeRange)) { searchApi.timeRange$.next(timeRange); } }, [timeRange, searchApi.timeRange$]); diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts index 057f9d20cbd8e..37654ad3fbb1a 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/schema.ts @@ -523,26 +523,6 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'text', _meta: { description: 'Non-default value of setting.' }, }, - 'labs:presentation:timeToPresent': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, - 'labs:canvas:enable_ui': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, - 'labs:canvas:byValueEmbeddable': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, - 'labs:canvas:useDataService': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, - 'labs:dashboard:enable_ui': { - type: 'boolean', - _meta: { description: 'Non-default value of setting.' }, - }, 'labs:dashboard:deferBelowFold': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts index 22a91311576a0..f6f95c7a808f6 100644 --- a/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/platform/plugins/private/kibana_usage_collection/server/collectors/management/types.ts @@ -141,11 +141,6 @@ export interface UsageStats { 'banners:textColor': string; 'banners:linkColor': string; 'banners:backgroundColor': string; - 'labs:canvas:enable_ui': boolean; - 'labs:canvas:byValueEmbeddable': boolean; - 'labs:canvas:useDataService': boolean; - 'labs:presentation:timeToPresent': boolean; - 'labs:dashboard:enable_ui': boolean; 'labs:dashboard:deferBelowFold': boolean; 'discover:rowHeightOption': number; hideAnnouncements: boolean; diff --git a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx index db9a769fa38e9..12c65474540fe 100644 --- a/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx +++ b/src/platform/plugins/private/presentation_panel/public/panel_component/presentation_panel_internal.tsx @@ -20,6 +20,7 @@ import classNames from 'classnames'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { PresentationPanelHeader } from './panel_header/presentation_panel_header'; import { PresentationPanelErrorInternal } from './presentation_panel_error_internal'; +import { usePresentationPanelVisibilityManager } from './presentation_panel_visibility_manager'; import type { DefaultPresentationPanelApi, PresentationPanelInternalProps } from './types'; import { usePanelErrorCss } from './use_panel_error_css'; import { PresentationPanelHoverActionsWrapper } from './panel_header/presentation_panel_hover_actions_wrapper'; @@ -47,6 +48,7 @@ export const PresentationPanelInternal = < const [api, setApi] = useState(null); const headerId = useMemo(() => htmlIdGenerator()(), []); + const visibilityTrackerRef = usePresentationPanelVisibilityManager(api); const dragHandles = useRef<{ [dragHandleKey: string]: HTMLElement | null }>({}); const viewModeSubject = useMemo(() => { @@ -131,6 +133,7 @@ export const PresentationPanelInternal = < {...contentAttrs} css={styles.embPanel} > +
{!hideHeader && api && ( ( + api: ApiType | null +) => { + const [intersection, updateIntersection] = useState(); + + const visibilityTrackerRef = useRef(null); + const intersectionObserverRef = useRef( + window.IntersectionObserver + ? new window.IntersectionObserver( + ([value]) => { + updateIntersection(value); + }, + { + root: visibilityTrackerRef.current, + } + ) + : undefined + ); + + useEffect(() => { + const { current: intersectionObserver } = intersectionObserverRef; + if (!intersectionObserver) { + onVisibilityChange(api, true); + return; + } + intersectionObserver.disconnect(); + const { current: visibilityTracker } = visibilityTrackerRef; + if (visibilityTracker) intersectionObserver.observe(visibilityTracker); + + return () => intersectionObserver.disconnect(); + }, [visibilityTrackerRef, api]); + + useEffect(() => { + onVisibilityChange(api, Boolean(intersection?.isIntersecting)); + }, [intersection, api]); + + return visibilityTrackerRef; +}; diff --git a/src/platform/plugins/shared/dashboard/common/content_management/constants.ts b/src/platform/plugins/shared/dashboard/common/content_management/constants.ts index c29a765d7697d..0996dc9c87a43 100644 --- a/src/platform/plugins/shared/dashboard/common/content_management/constants.ts +++ b/src/platform/plugins/shared/dashboard/common/content_management/constants.ts @@ -16,6 +16,7 @@ export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2; export const DEFAULT_PANEL_HEIGHT = 15; export const DEFAULT_DASHBOARD_OPTIONS = { + fetchOnlyVisible: false, hidePanelTitles: false, useMargins: true, syncColors: true, diff --git a/src/platform/plugins/shared/dashboard/kibana.jsonc b/src/platform/plugins/shared/dashboard/kibana.jsonc index c03b46f7a5b59..22a2363d996f9 100644 --- a/src/platform/plugins/shared/dashboard/kibana.jsonc +++ b/src/platform/plugins/shared/dashboard/kibana.jsonc @@ -25,7 +25,6 @@ "screenshotMode", "uiActions", "urlForwarding", - "presentationUtil", "unifiedSearch" ], "optionalPlugins": [ @@ -44,9 +43,9 @@ "requiredBundles": [ "kibanaReact", "kibanaUtils", - "presentationUtil", "presentationPanel", - "savedObjects" + "savedObjects", + "presentationUtil" ] } } diff --git a/src/platform/plugins/shared/presentation_util/public/i18n/index.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/data_fetch_manager.ts similarity index 90% rename from src/platform/plugins/shared/presentation_util/public/i18n/index.ts rename to src/platform/plugins/shared/dashboard/public/dashboard_api/data_fetch_manager.ts index e0fc0724769db..661cd324f5ba9 100644 --- a/src/platform/plugins/shared/presentation_util/public/i18n/index.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/data_fetch_manager.ts @@ -7,4 +7,4 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -export * from './labs'; +export const initializeDataFetchManager = () => {}; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts index 29658dbbcf757..81522196817ae 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/default_dashboard_state.ts @@ -24,4 +24,13 @@ export const DEFAULT_DASHBOARD_STATE: DashboardState = { syncCursor: true, syncTooltips: false, hidePanelTitles: false, + fetchOnlyVisible: false, +}; + +/** + * Some features should be opt-in for existing Dashboards and opt-out for new Dashboards. + * Any overrides added here will be spread on top of the state for new Dashboards only + */ +export const NEW_DASHBOARD_OVERRIDES: Partial = { + fetchOnlyVisible: true, }; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts index c54b9546dc1ca..724dc5f5d52bd 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts @@ -67,11 +67,13 @@ export function getDashboardApi({ return getReferencesForPanelId(id, references$.value ?? []); }; + const settingsManager = initializeSettingsManager(viewModeManager.api.viewMode$, initialState); const layoutManager = initializeLayoutManager( incomingEmbeddable, initialState.panels, trackPanel, - getReferences + getReferences, + settingsManager.api.fetchSetting$ ); const controlGroupManager = initializeControlGroupManager( initialState.controlGroupInput, @@ -82,7 +84,6 @@ export function getDashboardApi({ controlGroupManager.api.controlGroupApi$, layoutManager.api.children$ ); - const settingsManager = initializeSettingsManager(initialState); const unifiedSearchManager = initializeUnifiedSearchManager( initialState, controlGroupManager.api.controlGroupApi$, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts index 7cffecc3f9ddd..735574fe0ca89 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.test.ts @@ -66,6 +66,7 @@ describe('getSerializedState', () => { }, }, "options": Object { + "fetchOnlyVisible": true, "hidePanelTitles": false, "syncColors": false, "syncCursor": true, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts index a544177622e77..43f9a144ce202 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/get_serialized_state.ts @@ -62,6 +62,7 @@ export const getSerializedState = ({ syncTooltips, hidePanelTitles, controlGroupInput, + fetchOnlyVisible, } = dashboardState; let { panels } = dashboardState; @@ -83,6 +84,7 @@ export const getSerializedState = ({ syncCursor, syncTooltips, hidePanelTitles, + fetchOnlyVisible, }; /** diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts index a0ab8881b44d9..c0bd7e47ca59f 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.test.ts @@ -11,6 +11,7 @@ import { initializeLayoutManager } from './layout_manager'; import type { initializeTrackPanel } from '../track_panel'; import type { DefaultEmbeddableApi } from '@kbn/embeddable-plugin/public'; import type { + FetchSetting, HasLibraryTransforms, PhaseEvent, PublishingSubject, @@ -32,6 +33,8 @@ describe('layout manager', () => { jest.clearAllMocks(); }); + const fetchSetting$ = new BehaviorSubject('always'); + const PANEL_ONE_ID = 'panelOne'; const panel1 = { @@ -63,7 +66,13 @@ describe('layout manager', () => { }; test('can register child APIs', () => { - const layoutManager = initializeLayoutManager(undefined, [panel1], trackPanelMock, () => []); + const layoutManager = initializeLayoutManager( + undefined, + [panel1], + trackPanelMock, + () => [], + fetchSetting$ + ); layoutManager.internalApi.registerChildApi(panel1Api); expect(layoutManager.api.children$.getValue()[PANEL_ONE_ID]).toBe(panel1Api); }); @@ -86,7 +95,8 @@ describe('layout manager', () => { incomingEmbeddable, [panel1], trackPanelMock, - () => [] + () => [], + fetchSetting$ ); const layout = layoutManager.internalApi.layout$.value; @@ -110,7 +120,13 @@ describe('layout manager', () => { describe('duplicatePanel', () => { test('should add duplicated panel to layout', async () => { - const layoutManager = initializeLayoutManager(undefined, [panel1], trackPanelMock, () => []); + const layoutManager = initializeLayoutManager( + undefined, + [panel1], + trackPanelMock, + () => [], + fetchSetting$ + ); layoutManager.internalApi.registerChildApi(panel1Api); await layoutManager.api.duplicatePanel('panelOne'); @@ -135,7 +151,13 @@ describe('layout manager', () => { }); test('should clone by reference embeddable as by value', async () => { - const layoutManager = initializeLayoutManager(undefined, [panel1], trackPanelMock, () => []); + const layoutManager = initializeLayoutManager( + undefined, + [panel1], + trackPanelMock, + () => [], + fetchSetting$ + ); layoutManager.internalApi.registerChildApi({ ...panel1Api, checkForDuplicateTitle: jest.fn(), @@ -160,7 +182,13 @@ describe('layout manager', () => { }); test('should give a correct title to the clone of a clone', async () => { - const layoutManager = initializeLayoutManager(undefined, [panel1], trackPanelMock, () => []); + const layoutManager = initializeLayoutManager( + undefined, + [panel1], + trackPanelMock, + () => [], + fetchSetting$ + ); const titleManagerOfClone = initializeTitleManager({ title: 'Panel One (copy)' }); layoutManager.internalApi.registerChildApi({ ...panel1Api, @@ -188,7 +216,8 @@ describe('layout manager', () => { ...trackPanelMock, expandedPanelId$: new BehaviorSubject(undefined), }, - () => [] + () => [], + fetchSetting$ ); expect(layoutManager.api.canRemovePanels()).toBe(true); }); @@ -201,7 +230,8 @@ describe('layout manager', () => { ...trackPanelMock, expandedPanelId$: new BehaviorSubject('1'), }, - () => [] + () => [], + fetchSetting$ ); expect(layoutManager.api.canRemovePanels()).toBe(false); }); @@ -209,7 +239,13 @@ describe('layout manager', () => { describe('getChildApi', () => { test('should return api when api is available', (done) => { - const layoutManager = initializeLayoutManager(undefined, [panel1], trackPanelMock, () => []); + const layoutManager = initializeLayoutManager( + undefined, + [panel1], + trackPanelMock, + () => [], + fetchSetting$ + ); layoutManager.api.getChildApi(PANEL_ONE_ID).then((api) => { expect(api).toBe(panel1Api); @@ -229,7 +265,8 @@ describe('layout manager', () => { }, ], trackPanelMock, - () => [] + () => [], + fetchSetting$ ); layoutManager.api.getChildApi(PANEL_ONE_ID).then((api) => { @@ -250,7 +287,8 @@ describe('layout manager', () => { }, ], trackPanelMock, - () => [] + () => [], + fetchSetting$ ); layoutManager.api.getChildApi(PANEL_ONE_ID).then((api) => { @@ -262,4 +300,88 @@ describe('layout manager', () => { // because api will never become available }); }); + + describe('panelCounts', () => { + const PANEL_TWO_ID = 'panelTwo'; + const panel2 = { + gridData: { w: 1, h: 1, x: 1, y: 0, i: PANEL_TWO_ID }, + type: 'testPanelType', + panelConfig: { title: 'Panel Two' }, + panelIndex: PANEL_TWO_ID, + }; + + const PANEL_THREE_ID = 'panelThree'; + const panel3 = { + gridData: { w: 1, h: 1, x: 0, y: 0, i: PANEL_THREE_ID }, + type: 'testPanelType', + panelConfig: { title: 'Panel Three' }, + panelIndex: PANEL_THREE_ID, + }; + + const section2 = { + title: 'Section two', + collapsed: true, + gridData: { + y: 2, + i: 'section2', + }, + panels: [panel3], + }; + + test('should return the correct number of visible panels when fetchSetting is always', async () => { + const localFetchSetting$ = new BehaviorSubject('always'); + + const layoutManager = initializeLayoutManager( + undefined, + [section1, panel2, section2], + trackPanelMock, + () => [], + localFetchSetting$ + ); + + const panelCounts = layoutManager.internalApi.panelCounters$.getValue(); + expect(panelCounts.panelCount).toBe(3); + expect(panelCounts.visiblePanelsCount).toBe(2); // panel3 is in a collapsed section + expect(panelCounts.sectionCount).toBe(2); + }); + + test('should return the correct number of visible panels when fetchSetting is onlyVisible', async () => { + const localFetchSetting$ = new BehaviorSubject('onlyVisible'); + + const layoutManager = initializeLayoutManager( + undefined, + [section1, panel2, section2], + trackPanelMock, + () => [], + localFetchSetting$ + ); + + const panel1ApiWithVisibility = { + ...panel1Api, + isVisible$: new BehaviorSubject(true), + getIsVisible$: () => new BehaviorSubject(true), + }; + const panel2ApiWithVisibility = { + ...panel1Api, + uuid: PANEL_TWO_ID, + isVisible$: new BehaviorSubject(true), + getIsVisible$: () => new BehaviorSubject(true), + }; + const panel3ApiWithVisibility = { + ...panel1Api, + uuid: PANEL_THREE_ID, + isVisible$: new BehaviorSubject(false), + getIsVisible$: () => new BehaviorSubject(false), + }; + + layoutManager.internalApi.registerChildApi(panel1ApiWithVisibility); + layoutManager.internalApi.registerChildApi(panel2ApiWithVisibility); + layoutManager.internalApi.registerChildApi(panel3ApiWithVisibility); + + const panelCounts = layoutManager.internalApi.panelCounters$.getValue(); + expect(panelCounts.panelCount).toBe(3); + expect(panelCounts.visiblePanelsCount).toBe(2); // panel1 and panel2 are visible + expect(panelCounts.sectionCount).toBe(2); + }); + }); }); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts index 4350cdf9d98df..2eb5b96b13984 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/layout_manager.ts @@ -18,7 +18,9 @@ import { map, merge, mergeMap, + of, startWith, + switchMap, tap, type Observable, } from 'rxjs'; @@ -31,10 +33,15 @@ import { PanelNotFoundError } from '@kbn/embeddable-plugin/public'; import type { GridLayoutData, GridPanelData, GridSectionData } from '@kbn/grid-layout'; import { i18n } from '@kbn/i18n'; import type { PanelPackage } from '@kbn/presentation-containers'; -import type { SerializedPanelState, SerializedTitles } from '@kbn/presentation-publishing'; +import type { + FetchSetting, + SerializedPanelState, + SerializedTitles, +} from '@kbn/presentation-publishing'; import { apiHasLibraryTransforms, apiHasSerializableState, + apiPublishesIsVisible, apiPublishesTitle, apiPublishesUnsavedChanges, getTitle, @@ -58,13 +65,19 @@ import type { initializeTrackPanel } from '../track_panel'; import { areLayoutsEqual } from './are_layouts_equal'; import { deserializeLayout } from './deserialize_layout'; import { serializeLayout } from './serialize_layout'; -import type { DashboardChildren, DashboardLayout, DashboardLayoutPanel } from './types'; +import type { + DashboardChildren, + DashboardLayout, + DashboardLayoutPanel, + PanelCounts, +} from './types'; export function initializeLayoutManager( incomingEmbeddable: EmbeddablePackageState | undefined, initialPanels: DashboardState['panels'], trackPanel: ReturnType, - getReferences: (id: string) => Reference[] + getReferences: (id: string) => Reference[], + fetchSetting$: BehaviorSubject ) { // -------------------------------------------------------------------------------------- // Set up panel state manager @@ -361,12 +374,57 @@ export function initializeLayoutManager( return Boolean(sectionId && sections[sectionId].collapsed); } + const getPanelCounts = (): PanelCounts => { + const layout = layout$.value; + const panels = Object.values(layout.panels); + const uncollapsedPanels = panels.filter(({ gridData }) => { + return !isSectionCollapsed(gridData.sectionId); + }); + const panelsInViewport = Object.values(children$.value).filter( + (api) => apiPublishesIsVisible(api) && api.isVisible$.value + ); + return { + panelCount: panels.length, + visiblePanelsCount: + fetchSetting$.value === 'always' ? uncollapsedPanels.length : panelsInViewport.length, + sectionCount: Object.keys(layout.sections).length, + }; + }; + const panelCounters$ = new BehaviorSubject(getPanelCounts()); + const anyChildVisibilityChange$ = children$.pipe( + startWith({}), + switchMap((children) => { + const childrenWhoPublishVisibility = Object.values(children) + .map((child) => (apiPublishesIsVisible(child) ? child.isVisible$ : null)) + .filter((entry) => entry !== null) as BehaviorSubject[]; + return childrenWhoPublishVisibility.length > 0 + ? combineLatest(childrenWhoPublishVisibility) + : of([]); + }) + ); + const panelCounterSubscription = combineLatest([ + layout$, + anyChildVisibilityChange$, + fetchSetting$, + ]) + .pipe( + map(() => getPanelCounts()), + distinctUntilChanged( + (a, b) => + a.panelCount === b.panelCount && + a.visiblePanelsCount === b.visiblePanelsCount && + a.sectionCount === b.sectionCount + ) + ) + .subscribe((counts) => panelCounters$.next(counts)); + return { internalApi: { getSerializedStateForPanel: (panelId: string) => currentChildState[panelId], getLastSavedStateForPanel: (panelId: string) => lastSavedChildState[panelId], layout$, gridLayout$, + panelCounters$, reset: resetLayout, serializeLayout: () => serializeLayout(layout$.value, currentChildState), startComparing$: ( @@ -447,6 +505,7 @@ export function initializeLayoutManager( }, cleanup: () => { gridLayoutSubscription.unsubscribe(); + panelCounterSubscription.unsubscribe(); }, }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts index 6fef16ca457bf..15166a8d60e47 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/layout_manager/types.ts @@ -20,6 +20,12 @@ export interface DashboardLayoutPanel { type: DashboardPanel['type']; } +export interface PanelCounts { + panelCount: number; + visiblePanelsCount: number; + sectionCount: number; +} + export interface DashboardLayout { panels: { [uuid: string]: DashboardLayoutPanel; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/settings_manager.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_api/settings_manager.tsx index e9bc0cdafca79..11fd9e6ac6fdf 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/settings_manager.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/settings_manager.tsx @@ -6,20 +6,26 @@ * your election, the "Elastic License 2.0", the "GNU Affero General Public * License v3.0 only", or the "Server Side Public License, v 1". */ - -import type { StateComparators } from '@kbn/presentation-publishing'; import { + type PublishingSubject, + type StateComparators, + type ViewMode, diffComparators, initializeTitleManager, titleComparators, + type FetchSetting, } from '@kbn/presentation-publishing'; import fastIsEqual from 'fast-deep-equal'; import { BehaviorSubject, combineLatest, combineLatestWith, debounceTime, map } from 'rxjs'; import type { DashboardSettings, DashboardState } from '../../common'; import { DEFAULT_DASHBOARD_STATE } from './default_dashboard_state'; +import { coreServices } from '../services/kibana_services'; // SERIALIZED STATE ONLY TODO: This could be simplified by using src/platform/packages/shared/presentation/presentation_publishing/state_manager/state_manager.ts -export function initializeSettingsManager(initialState?: DashboardState) { +export function initializeSettingsManager( + viewMode$: PublishingSubject, + initialState?: DashboardState +) { const syncColors$ = new BehaviorSubject( initialState?.syncColors ?? DEFAULT_DASHBOARD_STATE.syncColors ); @@ -56,12 +62,32 @@ export function initializeSettingsManager(initialState?: DashboardState) { if (useMargins !== useMargins$.value) useMargins$.next(useMargins); } + // fetch setting + const fetchOnlyVisible$ = new BehaviorSubject( + initialState?.fetchOnlyVisible ?? DEFAULT_DASHBOARD_STATE.fetchOnlyVisible + ); + function setFetchOnlyVisible(fetchOnlyVisible: boolean) { + if (fetchOnlyVisible !== fetchOnlyVisible$.value) fetchOnlyVisible$.next(fetchOnlyVisible); + } + + const deferBelowFold = coreServices.uiSettings.get('labs:dashboard:deferBelowFold', false); + const getFetchSetting = (): FetchSetting => { + if (viewMode$.value === 'print') return 'always'; + if (deferBelowFold) return 'onlyVisible'; + return fetchOnlyVisible$.value ? 'onlyVisible' : 'always'; + }; + const fetchSetting$ = new BehaviorSubject(getFetchSetting()); + const fetchSettingSubscription = combineLatest([fetchOnlyVisible$, viewMode$]) + .pipe(map(() => getFetchSetting())) + .subscribe((nextSetting) => fetchSetting$.next(nextSetting)); + function getSettings(): DashboardSettings { const titleState = titleManager.getLatestState(); return { title: titleState.title ?? '', description: titleState.description, hidePanelTitles: titleState.hidePanelTitles ?? DEFAULT_DASHBOARD_STATE.hidePanelTitles, + fetchOnlyVisible: fetchOnlyVisible$.value, syncColors: syncColors$.value, syncCursor: syncCursor$.value, syncTooltips: syncTooltips$.value, @@ -72,6 +98,7 @@ export function initializeSettingsManager(initialState?: DashboardState) { } function setSettings(settings: DashboardSettings) { + setFetchOnlyVisible(settings.fetchOnlyVisible); setSyncColors(settings.syncColors); setSyncCursor(settings.syncCursor); setSyncTooltips(settings.syncTooltips); @@ -86,6 +113,7 @@ export function initializeSettingsManager(initialState?: DashboardState) { const comparators: StateComparators = { title: titleComparators.title, description: titleComparators.description, + fetchOnlyVisible: 'referenceEquality', hidePanelTitles: 'referenceEquality', syncColors: 'referenceEquality', syncCursor: 'referenceEquality', @@ -98,12 +126,14 @@ export function initializeSettingsManager(initialState?: DashboardState) { return { api: { ...titleManager.api, + fetchSetting$, getSettings, settings: { syncColors$, syncCursor$, syncTooltips$, useMargins$, + fetchOnlyVisible$, }, setSettings, setTags, @@ -133,5 +163,8 @@ export function initializeSettingsManager(initialState?: DashboardState) { setSettings(lastSavedState); }, }, + cleanup: () => { + fetchSettingSubscription.unsubscribe(); + }, }; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts index 194cf9959cfc9..a16d3f66422d3 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts @@ -34,6 +34,7 @@ import type { PublishesDataLoading, PublishesDataViews, PublishesDescription, + PublishesFetchSetting, PublishesSavedObjectId, PublishesTitle, PublishesUnifiedSearch, @@ -54,7 +55,7 @@ import type { LoadDashboardReturn, SaveDashboardReturn, } from '../services/dashboard_content_management_service/types'; -import type { DashboardLayout } from './layout_manager/types'; +import type { DashboardLayout, PanelCounts } from './layout_manager/types'; export const DASHBOARD_API_TYPE = 'dashboard'; @@ -103,6 +104,7 @@ export type DashboardApi = CanExpandPanels & PassThroughContext & PresentationContainer & PublishesDataLoading & + PublishesFetchSetting & PublishesDataViews & PublishesDescription & Pick & @@ -162,6 +164,7 @@ export interface DashboardInternalApi { panelsReload$: Subject; layout$: BehaviorSubject; gridLayout$: BehaviorSubject; + panelCounters$: BehaviorSubject; registerChildApi: (api: DefaultEmbeddableApi) => void; setControlGroupApi: (controlGroupApi: ControlGroupApi) => void; serializeLayout: () => Pick; diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts index 455c1b575ce72..3127a1ac610aa 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/unsaved_changes_manager.test.ts @@ -93,7 +93,7 @@ describe('unsavedChangesManager', () => { describe('onUnsavedChanges', () => { describe('onSettingsChanges', () => { test('should have unsaved changes when tags change', (done) => { - const settingsManager = initializeSettingsManager(); + const settingsManager = initializeSettingsManager(new BehaviorSubject('view')); const unsavedChangesManager = initializeUnsavedChangesManager({ viewMode$, storeUnsavedChanges: false, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx index 3a10d6fee854d..1e257d83b90a7 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/top_nav/use_dashboard_menu_items.tsx @@ -7,32 +7,24 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { Dispatch, SetStateAction } from 'react'; -import { useCallback, useMemo, useState } from 'react'; - import type { TopNavMenuData } from '@kbn/navigation-plugin/public'; -import useMountedState from 'react-use/lib/useMountedState'; - import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import { UI_SETTINGS } from '../../../common/constants'; +import { useCallback, useMemo, useState } from 'react'; +import useMountedState from 'react-use/lib/useMountedState'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; -import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays'; +import { openSettingsFlyout } from '../../dashboard_renderer/settings/open_settings_flyout'; import { getDashboardBackupService } from '../../services/dashboard_backup_service'; import type { SaveDashboardReturn } from '../../services/dashboard_content_management_service/types'; -import { coreServices, shareService } from '../../services/kibana_services'; +import { shareService } from '../../services/kibana_services'; import { getDashboardCapabilities } from '../../utils/get_dashboard_capabilities'; import { topNavStrings } from '../_dashboard_app_strings'; import { ShowShareModal } from './share/show_share_modal'; export const useDashboardMenuItems = ({ - isLabsShown, - setIsLabsShown, maybeRedirect, showResetChange, }: { - isLabsShown: boolean; - setIsLabsShown: Dispatch>; maybeRedirect: (result?: SaveDashboardReturn) => void; showResetChange?: boolean; }) => { @@ -128,13 +120,6 @@ export const useDashboardMenuItems = ({ disableButton: disableTopNav, } as TopNavMenuData, - labs: { - ...topNavStrings.labs, - id: 'labs', - testId: 'dashboardLabs', - run: () => setIsLabsShown(!isLabsShown), - } as TopNavMenuData, - edit: { ...topNavStrings.edit, emphasize: true, @@ -226,8 +211,6 @@ export const useDashboardMenuItems = ({ viewMode, showShare, dashboardApi, - setIsLabsShown, - isLabsShown, quickSaveDashboard, resetChanges, isResetting, @@ -259,7 +242,6 @@ export const useDashboardMenuItems = ({ /** * Build ordered menus for view and edit mode. */ - const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); const hasExportIntegration = useMemo(() => { if (!shareService) return false; @@ -269,7 +251,6 @@ export const useDashboardMenuItems = ({ const viewModeTopNavConfig = useMemo(() => { const { showWriteControls } = getDashboardCapabilities(); - const labsMenuItem = isLabsEnabled ? [menuItems.labs] : []; const shareMenuItem = shareService ? ([ // Only show the export button if the current user meets the requirements for at least one registered export integration @@ -282,7 +263,6 @@ export const useDashboardMenuItems = ({ const mayberesetChangesMenuItem = showResetChange ? [resetChangesMenuItem] : []; return [ - ...labsMenuItem, menuItems.fullScreen, ...duplicateMenuItem, ...mayberesetChangesMenuItem, @@ -290,8 +270,6 @@ export const useDashboardMenuItems = ({ ...editMenuItem, ]; }, [ - isLabsEnabled, - menuItems.labs, menuItems.export, menuItems.share, menuItems.interactiveSave, @@ -304,7 +282,6 @@ export const useDashboardMenuItems = ({ ]); const editModeTopNavConfig = useMemo(() => { - const labsMenuItem = isLabsEnabled ? [menuItems.labs] : []; const shareMenuItem = shareService ? ([ // Only show the export button if the current user meets the requirements for at least one registered export integration @@ -327,15 +304,13 @@ export const useDashboardMenuItems = ({ editModeItems.push(menuItems.switchToViewMode, menuItems.interactiveSave); } - const editModeTopNavConfigItems = [...labsMenuItem, menuItems.settings, ...editModeItems]; + const editModeTopNavConfigItems = [menuItems.settings, ...editModeItems]; // insert share menu item before the last item in edit mode editModeTopNavConfigItems.splice(-1, 0, ...shareMenuItem); return editModeTopNavConfigItems; }, [ - isLabsEnabled, - menuItems.labs, menuItems.export, menuItems.share, menuItems.settings, diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_settings.ts b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_settings.ts index 3a849c8d95763..9da70f9d7b080 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_settings.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_app/url/bwc/extract_dashboard_settings.ts @@ -24,6 +24,10 @@ export function extractSettings(state: { [key: string]: unknown }): Partial { const component = render( - + ); diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx index 9bd19c64147c2..b4ba51393f8f2 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/grid/dashboard_grid_item.tsx @@ -11,16 +11,12 @@ import type { UseEuiTheme } from '@elastic/eui'; import { EuiLoadingChart, useEuiTheme } from '@elastic/eui'; import { css } from '@emotion/react'; import { EmbeddableRenderer } from '@kbn/embeddable-plugin/public'; -import { - useBatchedPublishingSubjects, - useStateFromPublishingSubject, -} from '@kbn/presentation-publishing'; -import classNames from 'classnames'; -import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; +import classNames from 'classnames'; +import React, { useLayoutEffect, useMemo } from 'react'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { useDashboardInternalApi } from '../../dashboard_api/use_dashboard_internal_api'; -import { presentationUtilService } from '../../services/kibana_services'; import { printViewportVisStyles } from '../print_styles'; import { DASHBOARD_MARGIN_SIZE } from './constants'; import { getHighlightStyles } from './highlight_styles'; @@ -38,7 +34,7 @@ export interface Props extends DivProps { setDragHandles?: (refs: Array) => void; } -export const Item = React.forwardRef( +export const DashboardGridItem = React.forwardRef( ( { appFixedViewport, @@ -173,51 +169,6 @@ export const Item = React.forwardRef( } ); -export const ObservedItem = React.forwardRef((props, panelRef) => { - const [intersection, updateIntersection] = useState(); - const [isRenderable, setIsRenderable] = useState(false); - - const observerRef = useRef( - new window.IntersectionObserver(([value]) => updateIntersection(value), { - root: (panelRef as React.RefObject).current, - }) - ); - - useEffect(() => { - const { current: currentObserver } = observerRef; - currentObserver.disconnect(); - const { current } = panelRef as React.RefObject; - - if (current) { - currentObserver.observe(current); - } - - return () => currentObserver.disconnect(); - }, [panelRef]); - - useEffect(() => { - if (intersection?.isIntersecting && !isRenderable) { - setIsRenderable(true); - } - }, [intersection, isRenderable]); - - return ; -}); - -export const DashboardGridItem = React.forwardRef((props, ref) => { - const dashboardApi = useDashboardApi(); - const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode$); - - const deferBelowFoldEnabled = useMemo( - () => presentationUtilService.labsService.isProjectEnabled('labs:dashboard:deferBelowFold'), - [] - ); - - const isEnabled = viewMode !== 'print' && deferBelowFoldEnabled; - - return isEnabled ? : ; -}); - const dashboardGridItemStyles = { item: (context: UseEuiTheme) => css([ diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx index 7302c94e7bb8c..8f5f1bc48d1b8 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/settings/settings_flyout.tsx @@ -23,6 +23,7 @@ import { EuiForm, EuiFormRow, EuiIconTip, + EuiSpacer, EuiSwitch, EuiText, EuiTextArea, @@ -34,7 +35,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import type { DashboardSettings } from '../../../common'; import { useDashboardApi } from '../../dashboard_api/use_dashboard_api'; import { getDashboardContentManagementService } from '../../services/dashboard_content_management_service'; -import { savedObjectsTaggingService } from '../../services/kibana_services'; +import { coreServices, savedObjectsTaggingService } from '../../services/kibana_services'; interface DashboardSettingsProps { onClose: () => void; @@ -62,21 +63,20 @@ export const DashboardSettingsFlyout = ({ onClose, ariaLabelledBy }: DashboardSe const onApply = async () => { setIsApplying(true); - const validTitle = await getDashboardContentManagementService().checkForDuplicateDashboardTitle( - { + const isTitleValid = + await getDashboardContentManagementService().checkForDuplicateDashboardTitle({ title: localSettings.title, copyOnSave: false, lastSavedTitle: dashboardApi.title$.value ?? '', onTitleDuplicate, isTitleDuplicateConfirmed, - } - ); + }); if (!isMounted()) return; setIsApplying(false); - if (validTitle) { + if (isTitleValid) { dashboardApi.setSettings(localSettings); onClose(); } @@ -142,6 +142,26 @@ export const DashboardSettingsFlyout = ({ onClose, ariaLabelledBy }: DashboardSe ); }; + const renderFetchOnlyVisible = () => { + const deferBelowFold = coreServices.uiSettings.get('labs:dashboard:deferBelowFold', false); + return ( + + updateDashboardSetting({ fetchOnlyVisible: event.target.checked })} + label={ + + } + /> + + ); + }; + return ( <> @@ -211,6 +231,8 @@ export const DashboardSettingsFlyout = ({ onClose, ariaLabelledBy }: DashboardSe /> {renderTagSelector()} + + {renderFetchOnlyVisible()} ; }) => { const dashboardApi = useDashboardApi(); + const dashboardInternalApi = useDashboardInternalApi(); const [hasControls, setHasControls] = useState(false); const [ @@ -38,35 +39,25 @@ export const DashboardViewport = ({ dashboardTitle, description, expandedPanelId, - layout, viewMode, useMargins, fullScreenMode, + panelCounters, ] = useBatchedPublishingSubjects( dashboardApi.controlGroupApi$, dashboardApi.title$, dashboardApi.description$, dashboardApi.expandedPanelId$, - dashboardInternalApi.layout$, dashboardApi.viewMode$, dashboardApi.settings.useMargins$, - dashboardApi.fullScreenMode$ + dashboardApi.fullScreenMode$, + dashboardInternalApi.panelCounters$ ); const onExit = useCallback(() => { dashboardApi.setFullScreenMode(false); }, [dashboardApi]); - const { panelCount, visiblePanelCount, sectionCount } = useMemo(() => { - const panels = Object.values(layout.panels); - const visiblePanels = panels.filter(({ gridData }) => { - return !dashboardInternalApi.isSectionCollapsed(gridData.sectionId); - }); - return { - panelCount: panels.length, - visiblePanelCount: visiblePanels.length, - sectionCount: Object.keys(layout.sections).length, - }; - }, [layout, dashboardInternalApi]); + const { panelCount, sectionCount, visiblePanelsCount } = panelCounters; const classes = classNames('dshDashboardViewport', { 'dshDashboardViewport--empty': panelCount === 0 && sectionCount === 0, @@ -143,7 +134,7 @@ export const DashboardViewport = ({ data-shared-items-container data-title={dashboardTitle} data-description={description} - data-shared-items-count={visiblePanelCount} + data-shared-items-count={visiblePanelsCount} data-test-subj={'dshDashboardViewport'} > {panelCount === 0 && sectionCount === 0 ? ( diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx index 129e2a49b604e..a0abf42f2f8ed 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_top_nav/internal_dashboard_top_nav.tsx @@ -7,10 +7,6 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; -import UseUnmount from 'react-use/lib/useUnmount'; - -import type { EuiBreadcrumb, EuiToolTipProps, UseEuiTheme } from '@elastic/eui'; import { EuiBadge, EuiHorizontalRule, @@ -18,19 +14,22 @@ import { EuiLink, EuiPopover, EuiScreenReaderOnly, + type EuiBreadcrumb, + type EuiToolTipProps, + type UseEuiTheme, } from '@elastic/eui'; import { css } from '@emotion/react'; import type { MountPoint } from '@kbn/core/public'; -import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; import type { Query } from '@kbn/es-query'; import { FormattedMessage } from '@kbn/i18n-react'; import { getManagedContentBadge } from '@kbn/managed-content-badge'; import type { TopNavMenuBadgeProps, TopNavMenuProps } from '@kbn/navigation-plugin/public'; import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing'; -import { LazyLabsFlyout, withSuspense } from '@kbn/presentation-util-plugin/public'; import { MountPointPortal } from '@kbn/react-kibana-mount'; - -import { DASHBOARD_APP_ID, UI_SETTINGS } from '../../common/constants'; +import { useMemoCss } from '@kbn/css-utils/public/use_memo_css'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import UseUnmount from 'react-use/lib/useUnmount'; +import { DASHBOARD_APP_ID } from '../../common/constants'; import { useDashboardApi } from '../dashboard_api/use_dashboard_api'; import { dashboardManagedBadge, @@ -66,8 +65,6 @@ export interface InternalDashboardTopNavProps { showResetChange?: boolean; } -const LabsFlyout = withSuspense(LazyLabsFlyout, null); - export function InternalDashboardTopNav({ customLeadingBreadCrumbs = [], embedSettings, @@ -78,10 +75,8 @@ export function InternalDashboardTopNav({ showResetChange = true, }: InternalDashboardTopNavProps) { const [isChromeVisible, setIsChromeVisible] = useState(false); - const [isLabsShown, setIsLabsShown] = useState(false); const dashboardTitleRef = useRef(null); - const isLabsEnabled = useMemo(() => coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI), []); const { setHeaderActionMenu, onAppLeave } = useDashboardMountContext(); const dashboardApi = useDashboardApi(); @@ -263,8 +258,6 @@ export function InternalDashboardTopNav({ ); const { viewModeTopNavConfig, editModeTopNavConfig } = useDashboardMenuItems({ - isLabsShown, - setIsLabsShown, maybeRedirect, showResetChange, }); @@ -387,9 +380,6 @@ export function InternalDashboardTopNav({ }} onSavedQueryIdChange={setSavedQueryId} /> - {viewMode !== 'print' && isLabsEnabled && isLabsShown ? ( - setIsLabsShown(false)} /> - ) : null} {viewMode === 'edit' ? : null} {showBorderBottom && } diff --git a/src/platform/plugins/shared/dashboard/public/mocks.tsx b/src/platform/plugins/shared/dashboard/public/mocks.tsx index d9c91b148e02c..ef7c9476e6e14 100644 --- a/src/platform/plugins/shared/dashboard/public/mocks.tsx +++ b/src/platform/plugins/shared/dashboard/public/mocks.tsx @@ -112,6 +112,7 @@ export function getSampleDashboardState(overrides?: Partial): Da syncCursor: true, syncTooltips: false, hidePanelTitles: false, + fetchOnlyVisible: true, tags: [], filters: [], diff --git a/src/platform/plugins/shared/dashboard/public/plugin.tsx b/src/platform/plugins/shared/dashboard/public/plugin.tsx index cc38171149055..cf72968c1d2af 100644 --- a/src/platform/plugins/shared/dashboard/public/plugin.tsx +++ b/src/platform/plugins/shared/dashboard/public/plugin.tsx @@ -44,7 +44,6 @@ import type { ObservabilityAIAssistantPublicSetup, ObservabilityAIAssistantPublicStart, } from '@kbn/observability-ai-assistant-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { SavedObjectsManagementPluginStart } from '@kbn/saved-objects-management-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { @@ -96,7 +95,6 @@ export interface DashboardStartDependencies { fieldFormats: FieldFormatsStart; inspector: InspectorStartContract; navigation: NavigationPublicPluginStart; - presentationUtil: PresentationUtilPluginStart; contentManagement: ContentManagementPublicStart; savedObjectsManagement: SavedObjectsManagementPluginStart; savedObjectsTaggingOss?: SavedObjectTaggingOssPluginStart; diff --git a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts index 1043a02e85b7a..20635fa0fa02b 100644 --- a/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts +++ b/src/platform/plugins/shared/dashboard/public/services/dashboard_content_management_service/lib/load_dashboard_state.ts @@ -11,7 +11,10 @@ import { SavedObjectNotFound } from '@kbn/kibana-utils-plugin/public'; import { getDashboardContentManagementCache } from '..'; import type { DashboardGetIn, DashboardGetOut } from '../../../../server/content_management'; -import { DEFAULT_DASHBOARD_STATE } from '../../../dashboard_api/default_dashboard_state'; +import { + DEFAULT_DASHBOARD_STATE, + NEW_DASHBOARD_OVERRIDES, +} from '../../../dashboard_api/default_dashboard_state'; import { DASHBOARD_CONTENT_ID } from '../../../utils/telemetry_constants'; import { contentManagementService, savedObjectsTaggingService } from '../../kibana_services'; import type { LoadDashboardFromSavedObjectProps, LoadDashboardReturn } from '../types'; @@ -23,7 +26,7 @@ export const loadDashboardState = async ({ const savedObjectId = id; - const newDashboardState = { ...DEFAULT_DASHBOARD_STATE }; + const newDashboardState = { ...DEFAULT_DASHBOARD_STATE, ...NEW_DASHBOARD_OVERRIDES }; /** * This is a newly created dashboard, so there is no saved object state to load. diff --git a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts index 9aed7fffb6497..bf5e47a70b79d 100644 --- a/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts +++ b/src/platform/plugins/shared/dashboard/public/services/kibana_services.ts @@ -19,7 +19,6 @@ import type { NavigationPublicPluginStart } from '@kbn/navigation-plugin/public' import type { NoDataPagePluginStart } from '@kbn/no-data-page-plugin/public'; import type { ObservabilityAIAssistantPublicStart } from '@kbn/observability-ai-assistant-plugin/public'; import type { LensPublicStart } from '@kbn/lens-plugin/public'; -import type { PresentationUtilPluginStart } from '@kbn/presentation-util-plugin/public'; import type { SavedObjectTaggingOssPluginStart } from '@kbn/saved-objects-tagging-oss-plugin/public'; import type { ScreenshotModePluginStart } from '@kbn/screenshot-mode-plugin/public'; import type { ServerlessPluginStart } from '@kbn/serverless/public'; @@ -41,7 +40,6 @@ export let navigationService: NavigationPublicPluginStart; export let noDataPageService: NoDataPagePluginStart | undefined; export let observabilityAssistantService: ObservabilityAIAssistantPublicStart | undefined; export let lensService: LensPublicStart | undefined; -export let presentationUtilService: PresentationUtilPluginStart; export let savedObjectsTaggingService: SavedObjectTaggingOssPluginStart | undefined; export let screenshotModeService: ScreenshotModePluginStart; export let serverlessService: ServerlessPluginStart | undefined; @@ -64,7 +62,6 @@ export const setKibanaServices = (kibanaCore: CoreStart, deps: DashboardStartDep noDataPageService = deps.noDataPage; observabilityAssistantService = deps.observabilityAIAssistant; lensService = deps.lens; - presentationUtilService = deps.presentationUtil; savedObjectsTaggingService = deps.savedObjectsTaggingOss; serverlessService = deps.serverless; screenshotModeService = deps.screenshotMode; diff --git a/src/platform/plugins/shared/dashboard/public/services/mocks.ts b/src/platform/plugins/shared/dashboard/public/services/mocks.ts index 437244cec17f9..6967c73e52755 100644 --- a/src/platform/plugins/shared/dashboard/public/services/mocks.ts +++ b/src/platform/plugins/shared/dashboard/public/services/mocks.ts @@ -7,35 +7,33 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import { serverlessMock } from '@kbn/serverless/public/mocks'; -import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; -import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; -import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; -import { coreMock } from '@kbn/core/public/mocks'; import { contentManagementMock } from '@kbn/content-management-plugin/public/mocks'; import { customBrandingServiceMock } from '@kbn/core-custom-branding-browser-mocks'; +import { coreMock } from '@kbn/core/public/mocks'; import { dataPluginMock } from '@kbn/data-plugin/public/mocks'; +import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; import { embeddablePluginMock } from '@kbn/embeddable-plugin/public/mocks'; import { fieldFormatsServiceMock } from '@kbn/field-formats-plugin/public/mocks'; -import { indexPatternEditorPluginMock } from '@kbn/data-view-editor-plugin/public/mocks'; import { inspectorPluginMock } from '@kbn/inspector-plugin/public/mocks'; import { navigationPluginMock } from '@kbn/navigation-plugin/public/mocks'; import { noDataPagePublicMock } from '@kbn/no-data-page-plugin/public/mocks'; import { observabilityAIAssistantPluginMock } from '@kbn/observability-ai-assistant-plugin/public/mock'; -import { presentationUtilPluginMock } from '@kbn/presentation-util-plugin/public/mocks'; import { savedObjectsManagementPluginMock } from '@kbn/saved-objects-management-plugin/public/mocks'; -import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks'; import { savedObjectTaggingOssPluginMock } from '@kbn/saved-objects-tagging-oss-plugin/public/mocks'; +import { screenshotModePluginMock } from '@kbn/screenshot-mode-plugin/public/mocks'; +import { serverlessMock } from '@kbn/serverless/public/mocks'; +import { sharePluginMock } from '@kbn/share-plugin/public/mocks'; +import { spacesPluginMock } from '@kbn/spaces-plugin/public/mocks'; import { uiActionsPluginMock } from '@kbn/ui-actions-plugin/public/mocks'; import { unifiedSearchPluginMock } from '@kbn/unified-search-plugin/public/mocks'; import { urlForwardingPluginMock } from '@kbn/url-forwarding-plugin/public/mocks'; - -import { setKibanaServices } from './kibana_services'; -import { setLogger } from './logger'; -import type { DashboardAttributes } from '../../server/content_management'; +import { usageCollectionPluginMock } from '@kbn/usage-collection-plugin/public/mocks'; import type { DashboardCapabilities } from '../../common'; -import type { LoadDashboardReturn } from './dashboard_content_management_service/types'; +import type { DashboardAttributes } from '../../server/content_management'; import type { SearchDashboardsResponse } from './dashboard_content_management_service/lib/find_dashboards'; +import type { LoadDashboardReturn } from './dashboard_content_management_service/types'; +import { setKibanaServices } from './kibana_services'; +import { setLogger } from './logger'; const defaultDashboardCapabilities: DashboardCapabilities = { show: true, @@ -60,7 +58,6 @@ export const setStubKibanaServices = () => { navigation: navigationPluginMock.createStartContract(), noDataPage: noDataPagePublicMock.createStart(), observabilityAIAssistant: observabilityAIAssistantPluginMock.createStartContract(), - presentationUtil: presentationUtilPluginMock.createStartContract(), savedObjectsManagement: savedObjectsManagementPluginMock.createStartContract(), savedObjectsTaggingOss: savedObjectTaggingOssPluginMock.createStart(), screenshotMode: screenshotModePluginMock.createStartContract(), diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts index 677c41258e6f4..1e797bfd8b8be 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/cm_services.ts @@ -199,6 +199,13 @@ export const sectionSchema = schema.object({ }); export const optionsSchema = schema.object({ + fetchOnlyVisible: schema.boolean({ + defaultValue: DEFAULT_DASHBOARD_OPTIONS.fetchOnlyVisible, + meta: { + description: + 'Fetch data for only visible panels in the dashboard. This can improve performance.', + }, + }), hidePanelTitles: schema.boolean({ defaultValue: DEFAULT_DASHBOARD_OPTIONS.hidePanelTitles, meta: { description: 'Hide the panel titles in the dashboard.' }, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts index b28caed78d5d9..16313b03efb2b 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transform_utils.test.ts @@ -56,6 +56,7 @@ describe('savedObjectToItem', () => { ]), optionsJSON: JSON.stringify({ hidePanelTitles: true, + fetchOnlyVisible: true, useMargins: false, syncColors: false, syncTooltips: false, @@ -89,6 +90,7 @@ describe('savedObjectToItem', () => { ], options: { hidePanelTitles: true, + fetchOnlyVisible: true, useMargins: false, syncColors: false, syncTooltips: false, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts index 83ade34871a8d..833519e2acaab 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/in/transform_dashboard_in.test.ts @@ -38,6 +38,7 @@ describe('transformDashboardIn', () => { description: 'description', kibanaSavedObjectMeta: { searchSource: { query: { query: 'test', language: 'KQL' } } }, options: { + fetchOnlyVisible: true, hidePanelTitles: true, useMargins: false, syncColors: false, @@ -80,7 +81,7 @@ describe('transformDashboardIn', () => { "kibanaSavedObjectMeta": Object { "searchSourceJSON": "{\\"query\\":{\\"query\\":\\"test\\",\\"language\\":\\"KQL\\"}}", }, - "optionsJSON": "{\\"hidePanelTitles\\":true,\\"useMargins\\":false,\\"syncColors\\":false,\\"syncTooltips\\":false,\\"syncCursor\\":false}", + "optionsJSON": "{\\"fetchOnlyVisible\\":true,\\"hidePanelTitles\\":true,\\"useMargins\\":false,\\"syncColors\\":false,\\"syncTooltips\\":false,\\"syncCursor\\":false}", "panelsJSON": "[{\\"title\\":\\"title1\\",\\"type\\":\\"type1\\",\\"version\\":\\"2\\",\\"embeddableConfig\\":{\\"enhancements\\":{},\\"savedObjectId\\":\\"1\\"},\\"panelIndex\\":\\"1\\",\\"gridData\\":{\\"x\\":0,\\"y\\":0,\\"w\\":10,\\"h\\":10,\\"i\\":\\"1\\"}}]", "refreshInterval": Object { "pause": true, @@ -115,7 +116,7 @@ describe('transformDashboardIn', () => { "kibanaSavedObjectMeta": Object { "searchSourceJSON": "{}", }, - "optionsJSON": "{\\"hidePanelTitles\\":false,\\"useMargins\\":true,\\"syncColors\\":true,\\"syncCursor\\":true,\\"syncTooltips\\":true}", + "optionsJSON": "{\\"fetchOnlyVisible\\":false,\\"hidePanelTitles\\":false,\\"useMargins\\":true,\\"syncColors\\":true,\\"syncCursor\\":true,\\"syncTooltips\\":true}", "panelsJSON": "[]", "timeRestore": false, "title": "title", diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts index 08407d27cd37e..1032c40e141c1 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_dashboard_out.test.ts @@ -117,6 +117,7 @@ describe('transformDashboardOut', () => { }, optionsJSON: JSON.stringify({ hidePanelTitles: true, + fetchOnlyVisible: true, useMargins: false, syncColors: false, syncTooltips: false, @@ -159,6 +160,7 @@ describe('transformDashboardOut', () => { }, options: { hidePanelTitles: true, + fetchOnlyVisible: true, useMargins: false, syncColors: false, syncTooltips: false, diff --git a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_options_out.ts b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_options_out.ts index afa255e6dfbbb..41f6b84a78d07 100644 --- a/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_options_out.ts +++ b/src/platform/plugins/shared/dashboard/server/content_management/v1/transforms/out/transform_options_out.ts @@ -29,6 +29,7 @@ function transformOptionsProperties({ syncColors, syncCursor, syncTooltips, + fetchOnlyVisible, }: DashboardAttributes['options']) { return { hidePanelTitles, @@ -36,5 +37,6 @@ function transformOptionsProperties({ syncColors, syncCursor, syncTooltips, + fetchOnlyVisible, }; } diff --git a/src/platform/plugins/shared/dashboard/server/plugin.ts b/src/platform/plugins/shared/dashboard/server/plugin.ts index dd43b19cabe76..5febdb02abda8 100644 --- a/src/platform/plugins/shared/dashboard/server/plugin.ts +++ b/src/platform/plugins/shared/dashboard/server/plugin.ts @@ -7,44 +7,46 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ +import { schema } from '@kbn/config-schema'; +import { registerContentInsights } from '@kbn/content-management-content-insights-server'; +import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; +import type { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, + UiSettingsParams, +} from '@kbn/core/server'; +import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/server'; +import { i18n } from '@kbn/i18n'; +import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/server'; +import type { SharePluginStart } from '@kbn/share-plugin/server'; import type { TaskManagerSetupContract, TaskManagerStartContract, } from '@kbn/task-manager-plugin/server'; -import type { EmbeddableSetup, EmbeddableStart } from '@kbn/embeddable-plugin/server'; import type { UsageCollectionSetup, UsageCollectionStart, } from '@kbn/usage-collection-plugin/server'; -import type { ContentManagementServerSetup } from '@kbn/content-management-plugin/server'; -import type { SharePluginStart } from '@kbn/share-plugin/server'; -import type { - PluginInitializerContext, - CoreSetup, - CoreStart, - Plugin, - Logger, -} from '@kbn/core/server'; -import { registerContentInsights } from '@kbn/content-management-content-insights-server'; - -import type { SavedObjectTaggingStart } from '@kbn/saved-objects-tagging-plugin/server'; +import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; +import { DashboardAppLocatorDefinition } from '../common/locator/locator'; +import { registerAPIRoutes } from './api'; +import { capabilitiesProvider } from './capabilities_provider'; +import { DashboardStorage } from './content_management'; +import { dashboardPersistableStateServiceFactory } from './dashboard_container/dashboard_container_embeddable_factory'; +import { createDashboardSavedObjectType } from './dashboard_saved_object'; +import type { DashboardPluginSetup, DashboardPluginStart } from './types'; import { + TASK_ID, initializeDashboardTelemetryTask, scheduleDashboardTelemetry, - TASK_ID, } from './usage/dashboard_telemetry_collection_task'; -import { getUISettings } from './ui_settings'; -import { DashboardStorage } from './content_management'; -import { capabilitiesProvider } from './capabilities_provider'; -import type { DashboardPluginSetup, DashboardPluginStart } from './types'; -import { createDashboardSavedObjectType } from './dashboard_saved_object'; -import { CONTENT_ID, LATEST_VERSION } from '../common/content_management'; import { registerDashboardUsageCollector } from './usage/register_collector'; -import { dashboardPersistableStateServiceFactory } from './dashboard_container/dashboard_container_embeddable_factory'; -import { registerAPIRoutes } from './api'; -import { DashboardAppLocatorDefinition } from '../common/locator/locator'; import { setKibanaServices } from './kibana_services'; +export const DEFER_BELOW_FOLD = `labs:dashboard:deferBelowFold` as const; interface SetupDeps { embeddable: EmbeddableSetup; usageCollection?: UsageCollectionSetup; @@ -133,7 +135,22 @@ export class DashboardPlugin dashboardPersistableStateServiceFactory(plugins.embeddable) ); - core.uiSettings.register(getUISettings()); + const dashboardUiSettings: Record> = { + [DEFER_BELOW_FOLD]: { + schema: schema.boolean(), + requiresPageReload: true, + category: ['Dashboard'], + value: false, + name: i18n.translate('dashboard.labs.enableDeferBelowFoldProjectName', { + defaultMessage: 'Defer loading panels below "the fold"', + }), + description: i18n.translate('dashboard.labs.enableDeferBelowFoldProjectDescription', { + defaultMessage: + 'Any panels below "the fold"-- the area hidden beyond the bottom of the window, accessed by scrolling-- will not be loaded immediately, but only when they enter the viewport', + }), + }, + }; + core.uiSettings.register(dashboardUiSettings); registerAPIRoutes({ http: core.http, diff --git a/src/platform/plugins/shared/dashboard/server/ui_settings.ts b/src/platform/plugins/shared/dashboard/server/ui_settings.ts deleted file mode 100644 index c7e24bc41e29d..0000000000000 --- a/src/platform/plugins/shared/dashboard/server/ui_settings.ts +++ /dev/null @@ -1,34 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; -import { schema } from '@kbn/config-schema'; -import { SETTING_CATEGORY } from '@kbn/presentation-util-plugin/server'; -import type { UiSettingsParams } from '@kbn/core/types'; -import { UI_SETTINGS } from '../common/constants'; - -/** - * uiSettings definitions for Dashboard. - */ -export const getUISettings = (): Record> => ({ - [UI_SETTINGS.ENABLE_LABS_UI]: { - name: i18n.translate('dashboard.labs.enableUI', { - defaultMessage: 'Enable labs button in Dashboard', - }), - description: i18n.translate('dashboard.labs.enableLabsDescription', { - defaultMessage: - 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable technical preview features in Dashboard.', - }), - value: false, - type: 'boolean', - schema: schema.boolean(), - category: [SETTING_CATEGORY], - requiresPageReload: true, - }, -}); diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx index 5e7b7a3b74920..10810005fb2a4 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.test.tsx @@ -285,7 +285,7 @@ describe('saved search embeddable', () => { expect(resolveDataSourceProfileSpy).not.toHaveBeenCalled(); // trigger a refetch - dashboadFilters.next([]); + dashboadFilters.next([{ meta: {} }]); await waitOneTick(); expect(resolveDataSourceProfileSpy).toHaveBeenCalled(); }); diff --git a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx index 8842753265345..527f1830fa356 100644 --- a/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx +++ b/src/platform/plugins/shared/discover/public/embeddable/get_search_embeddable_factory.tsx @@ -105,26 +105,6 @@ export const getSearchEmbeddableFactory = ({ const searchEmbeddable = await initializeSearchEmbeddableApi(runtimeState, { discoverServices, }); - const unsubscribeFromFetch = initializeFetch({ - api: { - parentApi, - ...titleManager.api, - ...timeRangeManager.api, - defaultTitle$, - savedSearch$: searchEmbeddable.api.savedSearch$, - dataViews$: searchEmbeddable.api.dataViews$, - savedObjectId$, - dataLoading$, - blockingError$, - fetchContext$, - fetchWarnings$, - }, - discoverServices, - stateManager: searchEmbeddable.stateManager, - scopedProfilesManager, - setDataLoading: (dataLoading: boolean | undefined) => dataLoading$.next(dataLoading), - setBlockingError: (error: Error | undefined) => blockingError$.next(error), - }); const serialize = (savedObjectId?: string) => serializeState({ @@ -239,6 +219,28 @@ export const getSearchEmbeddableFactory = ({ }, }); + const unsubscribeFromFetch = initializeFetch({ + api: { + ...api, + parentApi, + ...titleManager.api, + ...timeRangeManager.api, + defaultTitle$, + savedSearch$: searchEmbeddable.api.savedSearch$, + dataViews$: searchEmbeddable.api.dataViews$, + savedObjectId$, + dataLoading$, + blockingError$, + fetchContext$, + fetchWarnings$, + }, + discoverServices, + stateManager: searchEmbeddable.stateManager, + scopedProfilesManager, + setDataLoading: (dataLoading: boolean | undefined) => dataLoading$.next(dataLoading), + setBlockingError: (error: Error | undefined) => blockingError$.next(error), + }); + return { api, Component: () => { diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx index a9626e713b66e..c335dd9b3c310 100644 --- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx +++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.test.tsx @@ -153,15 +153,17 @@ describe('embeddable renderer', () => { /> ); await waitFor(() => - expect(onApiAvailable).toHaveBeenCalledWith({ - type: 'test', - uuid: '12345', - parentApi: expect.any(Object), - serializeState: expect.any(Function), - phase$: expect.any(Object), - hasLockedHoverActions$: expect.any(Object), - lockHoverActions: expect.any(Function), - }) + expect(onApiAvailable).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'test', + uuid: '12345', + parentApi: expect.any(Object), + serializeState: expect.any(Function), + phase$: expect.any(Object), + hasLockedHoverActions$: expect.any(Object), + lockHoverActions: expect.any(Function), + }) + ) ); }); diff --git a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx index 8db76475d8151..2b72f6a0c1b4d 100644 --- a/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx +++ b/src/platform/plugins/shared/embeddable/public/react_embeddable_system/react_embeddable_renderer.tsx @@ -7,10 +7,15 @@ * License v3.0 only", or the "Server Side Public License, v 1". */ -import type { HasSerializedChildState } from '@kbn/presentation-containers'; -import { apiIsPresentationContainer } from '@kbn/presentation-containers'; -import type { PresentationPanelProps } from '@kbn/presentation-panel-plugin/public'; -import { PresentationPanel } from '@kbn/presentation-panel-plugin/public'; +import { + apiIsPresentationContainer, + type HasSerializedChildState, +} from '@kbn/presentation-containers'; +import { + PresentationPanel, + type PresentationPanelProps, +} from '@kbn/presentation-panel-plugin/public'; +import { initializeVisibility } from '@kbn/presentation-publishing'; import React, { useImperativeHandle, useMemo, useRef } from 'react'; import { BehaviorSubject } from 'rxjs'; import { v4 as generateId } from 'uuid'; @@ -70,8 +75,10 @@ export const EmbeddableRenderer = < apiRegistration: EmbeddableApiRegistration ) => { const hasLockedHoverActions$ = new BehaviorSubject(false); + return { ...apiRegistration, + ...initializeVisibility(parentApi), uuid, phase$: phaseTracker.current.getPhase$(), parentApi, diff --git a/src/platform/plugins/shared/presentation_util/common/index.ts b/src/platform/plugins/shared/presentation_util/common/index.ts index b6bf587e24fda..fe1daaef28afd 100644 --- a/src/platform/plugins/shared/presentation_util/common/index.ts +++ b/src/platform/plugins/shared/presentation_util/common/index.ts @@ -15,22 +15,3 @@ export const PLUGIN_NAME = 'presentationUtil'; * and CodeEditor components. */ export const EXPRESSIONS_LANGUAGE_ID = 'kibana-expressions'; - -export type { - EnvironmentName, - EnvironmentStatus, - Project, - ProjectConfig, - ProjectID, - ProjectStatus, - SolutionName, -} from './labs'; - -export { - LABS_PROJECT_PREFIX, - environmentNames, - projectIDs, - projects, - getProjectIDs, - isProjectEnabledByStatus, -} from './labs'; diff --git a/src/platform/plugins/shared/presentation_util/common/labs.ts b/src/platform/plugins/shared/presentation_util/common/labs.ts deleted file mode 100644 index 827ffe76c734b..0000000000000 --- a/src/platform/plugins/shared/presentation_util/common/labs.ts +++ /dev/null @@ -1,87 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { i18n } from '@kbn/i18n'; - -export const LABS_PROJECT_PREFIX = 'labs:'; -export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; -export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const; - -export const projectIDs = [DEFER_BELOW_FOLD, BY_VALUE_EMBEDDABLE] as const; -export const environmentNames = ['kibana', 'browser', 'session'] as const; -export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; - -/** - * This is a list of active Labs Projects for the Presentation Team. It is the "source of truth" for all projects - * provided to users of our solutions in Kibana. - */ -export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { - [DEFER_BELOW_FOLD]: { - id: DEFER_BELOW_FOLD, - isActive: false, - isDisplayed: true, - environments: ['kibana', 'browser', 'session'], - name: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectName', { - defaultMessage: 'Defer loading panels below "the fold"', - }), - description: i18n.translate('presentationUtil.labs.enableDeferBelowFoldProjectDescription', { - defaultMessage: - 'Any panels below "the fold"-- the area hidden beyond the bottom of the window, accessed by scrolling-- will not be loaded immediately, but only when they enter the viewport', - }), - solutions: ['dashboard'], - }, - [BY_VALUE_EMBEDDABLE]: { - id: BY_VALUE_EMBEDDABLE, - isActive: true, - isDisplayed: true, - environments: ['kibana', 'browser', 'session'], - name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', { - defaultMessage: 'By-Value Embeddables', - }), - description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', { - defaultMessage: 'Enables support for by-value embeddables in Canvas', - }), - solutions: ['canvas'], - }, -}; - -export type ProjectID = (typeof projectIDs)[number]; -export type EnvironmentName = (typeof environmentNames)[number]; -export type SolutionName = (typeof solutionNames)[number]; - -export type EnvironmentStatus = { - [env in EnvironmentName]?: boolean; -}; - -export type ProjectStatus = { - defaultValue: boolean; - isEnabled: boolean; - isOverride: boolean; -} & EnvironmentStatus; - -export interface ProjectConfig { - id: ProjectID; - name: string; - isActive: boolean; - isDisplayed: boolean; - environments: EnvironmentName[]; - description: string; - solutions: SolutionName[]; -} - -export type Project = ProjectConfig & { status: ProjectStatus }; - -export const getProjectIDs = () => projectIDs; - -export const isProjectEnabledByStatus = (active: boolean, status: EnvironmentStatus): boolean => { - // If the project is enabled by default, then any false flag will flip the switch, and vice-versa. - return active - ? Object.values(status).every((value) => value === true) - : Object.values(status).some((value) => value === true); -}; diff --git a/src/platform/plugins/shared/presentation_util/jest.config.js b/src/platform/plugins/shared/presentation_util/jest.config.js index c059676c0cd10..6389af8240a95 100644 --- a/src/platform/plugins/shared/presentation_util/jest.config.js +++ b/src/platform/plugins/shared/presentation_util/jest.config.js @@ -15,6 +15,6 @@ module.exports = { '/target/kibana-coverage/jest/src/platform/plugins/shared/presentation_util', coverageReporters: ['text', 'html'], collectCoverageFrom: [ - '/src/platform/plugins/shared/presentation_util/{common,public,server}/**/*.{ts,tsx}', + '/src/platform/plugins/shared/presentation_util/{common,public}/**/*.{ts,tsx}', ], }; diff --git a/src/platform/plugins/shared/presentation_util/kibana.jsonc b/src/platform/plugins/shared/presentation_util/kibana.jsonc index 681877d132b7d..67f59f7eda12d 100644 --- a/src/platform/plugins/shared/presentation_util/kibana.jsonc +++ b/src/platform/plugins/shared/presentation_util/kibana.jsonc @@ -10,7 +10,7 @@ "plugin": { "id": "presentationUtil", "browser": true, - "server": true, + "server": false, "requiredPlugins": [ "kibanaReact", "contentManagement", diff --git a/src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx b/src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx index c998e4bc95ba0..5b7d3202d08e9 100644 --- a/src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx +++ b/src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_options.tsx @@ -11,7 +11,7 @@ import React from 'react'; import { EuiFormRow, EuiSpacer, EuiSwitch } from '@elastic/eui'; import type { DashboardDrilldownOptions } from './types'; -import { dashboardDrilldownConfigStrings } from '../../i18n/dashboard_drilldown_config'; +import { dashboardDrilldownConfigStrings } from './dashboard_drilldown_strings'; export interface DashboardDrilldownOptionsProps { options: DashboardDrilldownOptions; diff --git a/src/platform/plugins/shared/presentation_util/public/i18n/dashboard_drilldown_config.tsx b/src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_strings.tsx similarity index 100% rename from src/platform/plugins/shared/presentation_util/public/i18n/dashboard_drilldown_config.tsx rename to src/platform/plugins/shared/presentation_util/public/components/dashboard_drilldown_options/dashboard_drilldown_strings.tsx diff --git a/src/platform/plugins/shared/presentation_util/public/components/index.tsx b/src/platform/plugins/shared/presentation_util/public/components/index.tsx index 7899a02295bbe..42325d82c8f09 100644 --- a/src/platform/plugins/shared/presentation_util/public/components/index.tsx +++ b/src/platform/plugins/shared/presentation_util/public/components/index.tsx @@ -30,10 +30,6 @@ export const withSuspense =

( ); }); -export const LazyLabsBeakerButton = React.lazy(() => import('./labs/labs_beaker_button')); - -export const LazyLabsFlyout = React.lazy(() => import('./labs/labs_flyout')); - export const LazyDashboardPicker = React.lazy(() => import('./dashboard_picker/dashboard_picker')); export const LazySavedObjectSaveModalDashboard = React.lazy( diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/environment_switch.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/environment_switch.tsx deleted file mode 100644 index 71e758784713b..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/environment_switch.tsx +++ /dev/null @@ -1,73 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useMemo } from 'react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiSwitch, - EuiIconTip, - EuiSpacer, - EuiScreenReaderOnly, -} from '@elastic/eui'; - -import type { EnvironmentName } from '../../../common/labs'; -import { LabsStrings } from '../../i18n'; -import { getPresentationCapabilities } from '../../utils/get_presentation_capabilities'; - -const { Switch: strings } = LabsStrings.Components; - -const switchText: { [env in EnvironmentName]: { name: string; help: string } } = { - kibana: strings.getKibanaSwitchText(), - browser: strings.getBrowserSwitchText(), - session: strings.getSessionSwitchText(), -}; - -export interface Props { - env: EnvironmentName; - isChecked: boolean; - onChange: (checked: boolean) => void; - name: string; -} - -export const EnvironmentSwitch = ({ env, isChecked, onChange, name }: Props) => { - const { canSetAdvancedSettings } = useMemo(() => { - return getPresentationCapabilities(); - }, []); - - const canSet = env === 'kibana' ? canSetAdvancedSettings : true; - - return ( - - - - - - {name} - - - {switchText[env].name} - - } - onChange={(e) => onChange(e.target.checked)} - compressed - /> - - - - - - - - ); -}; diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/labs.stories.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/labs.stories.tsx deleted file mode 100644 index a294fa7a48160..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/labs.stories.tsx +++ /dev/null @@ -1,38 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { action } from '@storybook/addon-actions'; - -import { LabsBeakerButton } from './labs_beaker_button'; -import { LabsFlyout } from './labs_flyout'; - -export default { - title: 'Labs/Flyout', - description: - 'A set of components used for providing Labs controls and projects in another solution.', - argTypes: { - canSetAdvancedSettings: { - control: 'boolean', - defaultValue: true, - }, - }, -}; - -export function BeakerButton() { - return ; -} - -export function Flyout() { - return ; -} - -export function EmptyFlyout() { - return ; -} diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/labs_beaker_button.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/labs_beaker_button.tsx deleted file mode 100644 index 98d0bcf850e61..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/labs_beaker_button.tsx +++ /dev/null @@ -1,51 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useMemo, useState } from 'react'; -import type { EuiButtonProps } from '@elastic/eui'; -import { EuiButton, EuiIcon, EuiNotificationBadge } from '@elastic/eui'; - -import type { Props as FlyoutProps } from './labs_flyout'; -import { LabsFlyout } from './labs_flyout'; -import { getPresentationLabsService } from '../../services/presentation_labs_service'; - -export type Props = EuiButtonProps & Pick; - -export const LabsBeakerButton = ({ solutions, ...props }: Props) => { - const labsService = useMemo(() => getPresentationLabsService(), []); - - const [isOpen, setIsOpen] = useState(false); - - const projects = labsService.getProjects(); - - const [overrideCount, onEnabledCountChange] = useState( - Object.values(projects).filter((project) => project.status.isOverride).length - ); - - const onButtonClick = () => setIsOpen((open) => !open); - const onClose = () => setIsOpen(false); - - return ( - <> - - - {overrideCount > 0 ? ( - - {overrideCount} - - ) : null} - - {isOpen ? : null} - - ); -}; - -// required for dynamic import using React.lazy() -// eslint-disable-next-line import/no-default-export -export default LabsBeakerButton; diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/labs_flyout.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/labs_flyout.tsx deleted file mode 100644 index 8aeca965de763..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/labs_flyout.tsx +++ /dev/null @@ -1,164 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { - EuiButton, - EuiButtonEmpty, - EuiFlexGroup, - EuiFlexItem, - EuiFlyout, - EuiFlyoutBody, - EuiFlyoutFooter, - EuiFlyoutHeader, - EuiIcon, - EuiSpacer, - EuiText, - EuiTitle, -} from '@elastic/eui'; -import type { ReactNode } from 'react'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; - -import type { - EnvironmentName, - Project, - ProjectID, - ProjectStatus, - SolutionName, -} from '../../../common'; -import { LabsStrings } from '../../i18n'; - -import { getPresentationLabsService } from '../../services/presentation_labs_service'; -import { ProjectList } from './project_list'; - -const { Flyout: strings } = LabsStrings.Components; - -export interface Props { - onClose: () => void; - solutions?: SolutionName[]; - onEnabledCountChange?: (overrideCount: number) => void; -} - -const hasStatusChanged = ( - original: Record, - current: Record -): boolean => { - for (const id of Object.keys(original) as ProjectID[]) { - for (const key of Object.keys(original[id].status) as Array) { - if (original[id].status[key] !== current[id].status[key]) { - return true; - } - } - } - return false; -}; - -export const getOverridenCount = (projects: Record) => - Object.values(projects).filter((project) => project.status.isOverride).length; - -export const LabsFlyout = (props: Props) => { - const { solutions, onEnabledCountChange = () => {}, onClose } = props; - const labsService = useMemo(() => getPresentationLabsService(), []); - - const [projects, setProjects] = useState(labsService.getProjects()); - const [overrideCount, setOverrideCount] = useState(getOverridenCount(projects)); - const initialStatus = useRef(labsService.getProjects()); - - const isChanged = hasStatusChanged(initialStatus.current, projects); - - useEffect(() => { - setOverrideCount(getOverridenCount(projects)); - }, [projects]); - - useEffect(() => { - onEnabledCountChange(overrideCount); - }, [onEnabledCountChange, overrideCount]); - - const onStatusChange = (id: ProjectID, env: EnvironmentName, enabled: boolean) => { - labsService.setProjectStatus(id, env, enabled); - setProjects(labsService.getProjects()); - }; - - let footer: ReactNode = null; - - const resetButton = ( - { - labsService.reset(); - setProjects(labsService.getProjects()); - }} - isDisabled={!overrideCount} - > - {strings.getResetToDefaultLabel()} - - ); - - const refreshButton = ( - { - window.location.reload(); - }} - isDisabled={!isChanged} - > - {strings.getRefreshLabel()} - - ); - - footer = ( - - - - onClose()} flush="left"> - {strings.getCloseButtonLabel()} - - - - - {resetButton} - {refreshButton} - - - - - ); - - return ( - - - -

- - - - - {strings.getTitleLabel()} - -

- - - -

{strings.getDescriptionMessage()}

-
-
- - - - {footer} - - ); -}; - -// required for dynamic import using React.lazy() -// eslint-disable-next-line import/no-default-export -export default LabsFlyout; diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/project_list.tsx deleted file mode 100644 index 331704a5dc477..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list.tsx +++ /dev/null @@ -1,96 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useMemo } from 'react'; -import { EuiFlexGroup, EuiCallOut, useEuiTheme } from '@elastic/eui'; - -import { css } from '@emotion/react'; -import type { SolutionName, ProjectID, Project } from '../../../common'; -import type { Props as ProjectListItemProps } from './project_list_item'; -import { ProjectListItem } from './project_list_item'; - -import { LabsStrings } from '../../i18n'; - -const { List: strings } = LabsStrings.Components; - -export interface Props { - solutions?: SolutionName[]; - projects: Record; - onStatusChange: ProjectListItemProps['onStatusChange']; -} - -const EmptyList = ({ solutions }: { solutions?: SolutionName[] }) => { - let title = strings.getNoProjectsMessage(); - - if (solutions?.length === 1) { - const solution = solutions[0]; - switch (solution) { - case 'dashboard': - title = strings.getNoProjectsInSolutionMessage('Dashboard'); - case 'canvas': - title = strings.getNoProjectsInSolutionMessage('Canvas'); - } - } - return ; -}; - -export const ProjectList = (props: Props) => { - const { solutions, projects, onStatusChange } = props; - const styles = useStyles(); - - const items = Object.values(projects) - .map((project) => { - if (!project.isDisplayed) { - return null; - } - - // Filter out any panels that don't match the solutions filter, (if provided). - if (solutions && !solutions.some((solution) => project.solutions.includes(solution))) { - return null; - } - - return ( -
  • - -
  • - ); - }) - .filter((item) => item !== null); - - return ( - - {items.length > 0 ?
      {items}
    : } -
    - ); -}; - -const useStyles = () => { - const { euiTheme } = useEuiTheme(); - const styles = useMemo(() => { - return css({ - position: 'relative', - padding: `${euiTheme.size.m} ${euiTheme.size.xs}`, - background: euiTheme.colors.emptyShade, - minWidth: '500px', - '&:first-child': { - paddingTop: 0, - }, - '&:not(:first-child):after': { - position: 'absolute', - top: 0, - right: 0, - left: 0, - height: '1px', - background: euiTheme.colors.lightShade, - content: '""', - }, - }); - }, [euiTheme]); - return styles; -}; diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.stories.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.stories.tsx deleted file mode 100644 index 11c0e082ec82d..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.stories.tsx +++ /dev/null @@ -1,83 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { action } from '@storybook/addon-actions'; -import { mapValues } from 'lodash'; -import React from 'react'; - -import type { EnvironmentStatus, ProjectConfig, ProjectID, ProjectStatus } from '../../../common'; -import type { Props } from './project_list_item'; -import { ProjectListItem } from './project_list_item'; - -import { projects as projectConfigs } from '../../../common'; -import { applyProjectStatus } from '../../services/presentation_labs_service'; -import { ProjectList } from './project_list'; - -export default { - title: 'Labs/ProjectList', - description: 'A set of controls for displaying and manipulating projects.', -}; - -const projects = mapValues(projectConfigs, (project) => - applyProjectStatus(project, { kibana: false, session: false, browser: false }) -); - -export function List() { - return ; -} - -export function EmptyList() { - return ; -} - -export const ListItem = { - render: ( - props: Pick< - Props['project'], - 'description' | 'isActive' | 'name' | 'solutions' | 'environments' | 'isDisplayed' - > & - Omit - ) => { - const { kibana, browser, session, ...rest } = props; - const status: EnvironmentStatus = { kibana, browser, session }; - const projectConfig: ProjectConfig = { - ...rest, - id: 'storybook:component' as ProjectID, - }; - - return ( -
    - ({ ...status, [env]: enabled })} - /> -
    - ); - }, - - args: { - isActive: false, - name: 'Demo Project', - description: 'This is a demo project, and this is the description of the demo project.', - kibana: false, - browser: false, - session: false, - solutions: ['dashboard', 'canvas'], - environments: ['kibana', 'browser', 'session'], - }, - - argTypes: { - environments: { - control: { - type: 'check', - options: ['kibana', 'browser', 'session'], - }, - }, - }, -}; diff --git a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.tsx b/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.tsx deleted file mode 100644 index a56a07a374a3e..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/components/labs/project_list_item.tsx +++ /dev/null @@ -1,163 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React, { useMemo } from 'react'; -import { css } from '@emotion/react'; -import { - EuiFlexGroup, - EuiFlexItem, - EuiBadge, - EuiTitle, - EuiText, - EuiFormFieldset, - EuiScreenReaderOnly, - EuiSpacer, - EuiIconTip, - useEuiTheme, -} from '@elastic/eui'; -import classnames from 'classnames'; - -import type { ProjectID, EnvironmentName, Project } from '../../../common/labs'; -import { environmentNames } from '../../../common/labs'; -import { EnvironmentSwitch } from './environment_switch'; - -import { LabsStrings } from '../../i18n'; -const { ListItem: strings } = LabsStrings.Components; - -export interface Props { - project: Project; - onStatusChange: (id: ProjectID, env: EnvironmentName, enabled: boolean) => void; -} - -export const ProjectListItem = ({ project, onStatusChange }: Props) => { - const { id, status, isActive, name, description, solutions } = project; - const { isEnabled, isOverride } = status; - const { projectListItemStyles, pendingChangesIndicatorStyles, solutionsStyles } = useStyles(); - - return ( - - - - - - -

    - {name} - {isOverride ? ( - - - - ) : null} -

    -
    -
    - -
    - {solutions.map((solution) => ( - {solution} - ))} -
    -
    - - - - {description} - - - - - - {isActive ? strings.getEnabledStatusMessage() : strings.getDisabledStatusMessage()} - - -
    -
    - - - - {name} - - {strings.getOverrideLegend()} - - ), - }} - > - {environmentNames.map((env) => { - const envStatus = status[env]; - if (envStatus !== undefined) { - return ( - onStatusChange(id, env, checked)} - {...{ env, name }} - /> - ); - } - })} - - -
    -
    - ); -}; - -const useStyles = () => { - const { euiTheme } = useEuiTheme(); - const styles = useMemo(() => { - return { - projectListItemStyles: css({ - position: 'relative', - '&.projectListItem--isOverridden:before': { - position: 'absolute', - top: euiTheme.size.m, - left: euiTheme.size.xs, - bottom: 0, - width: euiTheme.size.xs, - background: euiTheme.colors.success, - content: '""', - }, - '.euiSwitch__label': { - width: '100%', - }, - '.euiFlyout &': { - '&.projectListItem--isOverridden:before': { - left: `-${euiTheme.size.m}`, - }, - '&.projectListItem--isOverridden:first-child:before': { - top: 0, - }, - }, - }), - pendingChangesIndicatorStyles: css({ - marginLeft: euiTheme.size.s, - position: 'relative', - top: '-1px', - }), - solutionsStyles: css({ - textTransform: 'capitalize', - }), - }; - }, [euiTheme]); - return styles; -}; diff --git a/src/platform/plugins/shared/presentation_util/public/i18n/labs.tsx b/src/platform/plugins/shared/presentation_util/public/i18n/labs.tsx deleted file mode 100644 index dd26d928ffd47..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/i18n/labs.tsx +++ /dev/null @@ -1,113 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n-react'; -import { EuiCode } from '@elastic/eui'; - -export const LabsStrings = { - Components: { - Switch: { - getKibanaSwitchText: () => ({ - name: i18n.translate('presentationUtil.labs.components.kibanaSwitchName', { - defaultMessage: 'Kibana', - }), - help: i18n.translate('presentationUtil.labs.components.kibanaSwitchHelp', { - defaultMessage: 'Enables this lab for all Kibana users.', - }), - }), - getBrowserSwitchText: () => ({ - name: i18n.translate('presentationUtil.labs.components.browserSwitchName', { - defaultMessage: 'Browser', - }), - help: i18n.translate('presentationUtil.labs.components.browserSwitchHelp', { - defaultMessage: 'Enables the lab for this browser and persists after it closes.', - }), - }), - getSessionSwitchText: () => ({ - name: i18n.translate('presentationUtil.labs.components.sessionSwitchName', { - defaultMessage: 'Session', - }), - help: i18n.translate('presentationUtil.labs.components.sessionSwitchHelp', { - defaultMessage: 'Enables the lab for this browser session, so it resets when it closes.', - }), - }), - }, - List: { - getNoProjectsMessage: () => - i18n.translate('presentationUtil.labs.components.noProjectsMessage', { - defaultMessage: 'No labs currently available.', - }), - getNoProjectsInSolutionMessage: (solutionName: string) => - i18n.translate('presentationUtil.labs.components.noProjectsinSolutionMessage', { - defaultMessage: 'No labs currently in {solutionName}.', - values: { - solutionName, - }, - }), - }, - ListItem: { - getOverrideLegend: () => - i18n.translate('presentationUtil.labs.components.overrideFlagsLabel', { - defaultMessage: 'Overrides', - }), - getOverriddenIconTipLabel: () => - i18n.translate('presentationUtil.labs.components.overridenIconTipLabel', { - defaultMessage: 'Default overridden', - }), - getEnabledStatusMessage: () => ( - Enabled, - }} - description="Displays the enabled status of a lab project" - /> - ), - getDisabledStatusMessage: () => ( - Disabled, - }} - description="Displays the disabled status of a lab project" - /> - ), - }, - Flyout: { - getTitleLabel: () => - i18n.translate('presentationUtil.labs.components.titleLabel', { - defaultMessage: 'Labs', - }), - getDescriptionMessage: () => - i18n.translate('presentationUtil.labs.components.descriptionMessage', { - defaultMessage: 'Try out features that are in progress or in technical preview.', - }), - getResetToDefaultLabel: () => - i18n.translate('presentationUtil.labs.components.resetToDefaultLabel', { - defaultMessage: 'Reset to defaults', - }), - getLabFlagsLabel: () => - i18n.translate('presentationUtil.labs.components.labFlagsLabel', { - defaultMessage: 'Lab flags', - }), - getRefreshLabel: () => - i18n.translate('presentationUtil.labs.components.calloutHelp', { - defaultMessage: 'Refresh to apply changes', - }), - getCloseButtonLabel: () => - i18n.translate('presentationUtil.labs.components.closeButtonLabel', { - defaultMessage: 'Close', - }), - }, - }, -}; diff --git a/src/platform/plugins/shared/presentation_util/public/index.ts b/src/platform/plugins/shared/presentation_util/public/index.ts index fede6c4e35258..bbb0a8c993f8d 100644 --- a/src/platform/plugins/shared/presentation_util/public/index.ts +++ b/src/platform/plugins/shared/presentation_util/public/index.ts @@ -10,15 +10,11 @@ import type { ExpressionFunction } from '@kbn/expressions-plugin/common'; import { PresentationUtilPlugin } from './plugin'; -export type { PresentationLabsService } from './services/presentation_labs_service'; - export type { PresentationUtilPluginSetup, PresentationUtilPluginStart } from './types'; export type { SaveModalDashboardProps } from './components/types'; export { LazyExpressionInput, - LazyLabsBeakerButton, - LazyLabsFlyout, LazyDashboardPicker, LazySavedObjectSaveModalDashboard, LazySavedObjectSaveModalDashboardWithSaveResult, diff --git a/src/platform/plugins/shared/presentation_util/public/mocks.ts b/src/platform/plugins/shared/presentation_util/public/mocks.ts index 33d8f6a780135..e20d2eb48e428 100644 --- a/src/platform/plugins/shared/presentation_util/public/mocks.ts +++ b/src/platform/plugins/shared/presentation_util/public/mocks.ts @@ -13,13 +13,6 @@ import { setStubKibanaServices } from './services/mocks'; const createStartContract = (): PresentationUtilPluginStart => { const startContract: PresentationUtilPluginStart = { - labsService: { - getProjects: jest.fn(), - getProject: jest.fn(), - isProjectEnabled: jest.fn(), - reset: jest.fn(), - setProjectStatus: jest.fn(), - }, registerExpressionsLanguage, }; return startContract; diff --git a/src/platform/plugins/shared/presentation_util/public/plugin.ts b/src/platform/plugins/shared/presentation_util/public/plugin.ts index 275c24c9c2f52..6d5765f80457b 100644 --- a/src/platform/plugins/shared/presentation_util/public/plugin.ts +++ b/src/platform/plugins/shared/presentation_util/public/plugin.ts @@ -17,7 +17,6 @@ import type { import { registerExpressionsLanguage } from '.'; import { setKibanaServices } from './services/kibana_services'; -import { getPresentationLabsService } from './services/presentation_labs_service'; export class PresentationUtilPlugin implements @@ -42,7 +41,6 @@ export class PresentationUtilPlugin setKibanaServices(coreStart, startPlugins); return { - labsService: getPresentationLabsService(), registerExpressionsLanguage, }; } diff --git a/src/platform/plugins/shared/presentation_util/public/services/presentation_labs_service.ts b/src/platform/plugins/shared/presentation_util/public/services/presentation_labs_service.ts deleted file mode 100644 index ffb3a6d56c9bf..0000000000000 --- a/src/platform/plugins/shared/presentation_util/public/services/presentation_labs_service.ts +++ /dev/null @@ -1,156 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; -import type { - EnvironmentName, - EnvironmentStatus, - Project, - ProjectConfig, - ProjectID, - SolutionName, -} from '../../common'; -import { LABS_PROJECT_PREFIX, isProjectEnabledByStatus, projectIDs, projects } from '../../common'; -import { coreServices } from './kibana_services'; - -export interface PresentationLabsService { - isProjectEnabled: (id: ProjectID) => boolean; - getProject: (id: ProjectID) => Project; - getProjects: (solutions?: SolutionName[]) => Record; - setProjectStatus: (id: ProjectID, env: EnvironmentName, status: boolean) => void; - reset: () => void; -} - -export const getPresentationLabsService = (): PresentationLabsService => { - const { uiSettings } = coreServices; - const localStorage = window.localStorage; - const sessionStorage = window.sessionStorage; - - const getProjects = (solutions: SolutionName[] = []) => - projectIDs.reduce((acc, id) => { - const project = getProject(id); - if ( - solutions.length === 0 || - solutions.some((solution) => project.solutions.includes(solution)) - ) { - acc[id] = project; - } - return acc; - }, {} as { [id in ProjectID]: Project }); - - const getProject = (id: ProjectID) => { - const project = projects[id]; - - const status = { - session: isEnabledByStorageValue(project, 'session', sessionStorage.getItem(id)), - browser: isEnabledByStorageValue(project, 'browser', localStorage.getItem(id)), - kibana: isEnabledByStorageValue(project, 'kibana', uiSettings.get(id, project.isActive)), - }; - - return applyProjectStatus(project, status); - }; - - const setProjectStatus = (name: ProjectID, env: EnvironmentName, status: boolean) => { - switch (env) { - case 'session': - setStorageStatus(sessionStorage, name, status); - break; - case 'browser': - setStorageStatus(localStorage, name, status); - break; - case 'kibana': - setUISettingsStatus(uiSettings, name, status); - break; - } - }; - - const reset = () => { - clearLabsFromStorage(localStorage); - clearLabsFromStorage(sessionStorage); - projectIDs.forEach((id) => setProjectStatus(id, 'kibana', projects[id].isActive)); - }; - - const isProjectEnabled = (id: ProjectID) => getProject(id).status.isEnabled; - - return { - getProjects, - getProject, - isProjectEnabled, - reset, - setProjectStatus, - }; -}; - -/** - * Helpers - */ -const isEnabledByStorageValue = ( - project: ProjectConfig, - environment: EnvironmentName, - value: string | boolean | null -): boolean => { - const defaultValue = project.isActive; - - if (!project.environments.includes(environment)) { - return defaultValue; - } - - if (value === true || value === false) { - return value; - } - - if (value === 'enabled') { - return true; - } - - if (value === 'disabled') { - return false; - } - - return defaultValue; -}; - -const setStorageStatus = (storage: Storage, id: ProjectID, enabled: boolean) => - storage.setItem(id, enabled ? 'enabled' : 'disabled'); - -export const applyProjectStatus = (project: ProjectConfig, status: EnvironmentStatus): Project => { - const { isActive, environments } = project; - - environments.forEach((name) => { - if (!environments.includes(name)) { - delete status[name]; - } - }); - - const isEnabled = isProjectEnabledByStatus(isActive, status); - const isOverride = isEnabled !== isActive; - - return { - ...project, - status: { - ...status, - defaultValue: isActive, - isEnabled, - isOverride, - }, - }; -}; - -const setUISettingsStatus = (client: IUiSettingsClient, id: ProjectID, enabled: boolean) => - client.set(id, enabled); - -const clearLabsFromStorage = (storage: Storage) => { - projectIDs.forEach((projectID) => storage.removeItem(projectID)); - - // This is a redundancy, to catch any labs that may have been removed above. - // We could consider gathering telemetry to see how often this happens, or this may be unnecessary. - Object.keys(storage) - .filter((key) => key.startsWith(LABS_PROJECT_PREFIX)) - .forEach((key) => storage.removeItem(key)); -}; diff --git a/src/platform/plugins/shared/presentation_util/public/types.ts b/src/platform/plugins/shared/presentation_util/public/types.ts index d002f97cd9c4b..261fde45ac278 100644 --- a/src/platform/plugins/shared/presentation_util/public/types.ts +++ b/src/platform/plugins/shared/presentation_util/public/types.ts @@ -11,13 +11,11 @@ import type { ContentManagementPublicStart } from '@kbn/content-management-plugi import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { UiActionsStart } from '@kbn/ui-actions-plugin/public'; import type { registerExpressionsLanguage } from '.'; -import { type PresentationLabsService } from '.'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PresentationUtilPluginSetup {} export interface PresentationUtilPluginStart { - labsService: PresentationLabsService; registerExpressionsLanguage: typeof registerExpressionsLanguage; } // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/src/platform/plugins/shared/presentation_util/server/plugin.ts b/src/platform/plugins/shared/presentation_util/server/plugin.ts deleted file mode 100644 index 3a3bcbf102ba3..0000000000000 --- a/src/platform/plugins/shared/presentation_util/server/plugin.ts +++ /dev/null @@ -1,24 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import type { CoreSetup, Plugin } from '@kbn/core/server'; -import { getUISettings } from './ui_settings'; - -export class PresentationUtilPlugin implements Plugin { - public setup(core: CoreSetup) { - core.uiSettings.register(getUISettings()); - return {}; - } - - public start() { - return {}; - } - - public stop() {} -} diff --git a/src/platform/plugins/shared/presentation_util/server/ui_settings.ts b/src/platform/plugins/shared/presentation_util/server/ui_settings.ts deleted file mode 100644 index 077869c344771..0000000000000 --- a/src/platform/plugins/shared/presentation_util/server/ui_settings.ts +++ /dev/null @@ -1,42 +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", the "GNU Affero General Public License v3.0 only", 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", the "GNU Affero General Public - * License v3.0 only", or the "Server Side Public License, v 1". - */ - -import { schema } from '@kbn/config-schema'; -import type { UiSettingsParams } from '@kbn/core/types'; -import type { ProjectID } from '../common'; -import { projects, projectIDs } from '../common'; - -export const SETTING_CATEGORY = 'Presentation Labs'; - -const labsProjectSettings: Record> = projectIDs.reduce( - (acc, id) => { - const project = projects[id]; - const { name, description, isActive: value } = project; - acc[id] = { - name, - value, - type: 'boolean', - description, - schema: schema.boolean(), - requiresPageReload: true, - category: [SETTING_CATEGORY], - }; - return acc; - }, - {} as { - [id in ProjectID]: UiSettingsParams; - } -); - -/** - * uiSettings definitions for Presentation Util. - */ -export const getUISettings = (): Record> => ({ - ...labsProjectSettings, -}); diff --git a/src/platform/plugins/shared/presentation_util/tsconfig.json b/src/platform/plugins/shared/presentation_util/tsconfig.json index 2be8e5c7ad207..66f3ac4c1958b 100644 --- a/src/platform/plugins/shared/presentation_util/tsconfig.json +++ b/src/platform/plugins/shared/presentation_util/tsconfig.json @@ -7,7 +7,6 @@ "common/**/*", "public/**/*", "public/**/*.json", - "server/**/*", "test_helpers/**/*", "storybook/**/*", "../../../../../typings/**/*" @@ -24,7 +23,6 @@ "@kbn/field-formats-plugin", "@kbn/interpreter", "@kbn/react-field", - "@kbn/config-schema", "@kbn/storybook", "@kbn/ui-actions-plugin", "@kbn/saved-objects-finder-plugin", @@ -32,8 +30,7 @@ "@kbn/shared-ux-button-toolbar", "@kbn/code-editor", "@kbn/calculate-width-from-char-count", - "@kbn/field-utils", - "@kbn/core-ui-settings-browser", + "@kbn/field-utils" ], "exclude": ["target/**/*"] } diff --git a/src/platform/plugins/shared/telemetry/schema/oss_platform.json b/src/platform/plugins/shared/telemetry/schema/oss_platform.json index e9e58bc8aeb8a..40aac81830cb9 100644 --- a/src/platform/plugins/shared/telemetry/schema/oss_platform.json +++ b/src/platform/plugins/shared/telemetry/schema/oss_platform.json @@ -10829,36 +10829,6 @@ "description": "Non-default value of setting." } }, - "labs:presentation:timeToPresent": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, - "labs:canvas:enable_ui": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, - "labs:canvas:byValueEmbeddable": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, - "labs:canvas:useDataService": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, - "labs:dashboard:enable_ui": { - "type": "boolean", - "_meta": { - "description": "Non-default value of setting." - } - }, "labs:dashboard:deferBelowFold": { "type": "boolean", "_meta": { diff --git a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts index ca4a6a1a7fe74..e2d822c7f6c6a 100644 --- a/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts +++ b/src/platform/test/api_integration/apis/dashboards/create_dashboard/main.ts @@ -39,6 +39,7 @@ export default function ({ getService }: FtrProviderContext) { syncColors: true, syncTooltips: true, syncCursor: true, + fetchOnlyVisible: false, }); }); diff --git a/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.component.tsx index 634f44391541f..feab10b4490fb 100644 --- a/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -33,19 +33,12 @@ const strings = { export interface Props { onClose: () => void; - onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void; + onSelect: (id: string, embeddableType: string) => void; availableEmbeddables: string[]; - isByValueEnabled?: boolean; } -export const AddEmbeddableFlyout: FC = ({ - onSelect, - availableEmbeddables, - onClose, - isByValueEnabled, -}) => { +export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { const modalTitleId = useGeneratedHtmlId(); - const libraryTypes = useAddFromLibraryTypes(); const canvasOnlyLibraryTypes = useMemo(() => { @@ -55,9 +48,9 @@ export const AddEmbeddableFlyout: FC = ({ const onAddPanel = useCallback( (id: string, savedObjectType: string) => { - onSelect(id, savedObjectType, isByValueEnabled); + onSelect(id, savedObjectType); }, - [isByValueEnabled, onSelect] + [onSelect] ); return ( diff --git a/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.tsx index f18ca94af7e3a..d6493019b68a0 100644 --- a/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/platform/plugins/private/canvas/public/components/embeddable_flyout/flyout.tsx @@ -16,7 +16,6 @@ import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { embeddableInputToExpression } from '../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression'; import type { State } from '../../../types'; -import { presentationUtilService } from '../../services/kibana_services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -68,10 +67,6 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { - const isByValueEnabled = presentationUtilService.labsService.isProjectEnabled( - 'labs:canvas:byValueEmbeddable' - ); - const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); @@ -86,24 +81,17 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ expression: `markdown "Could not find embeddable for type ${type}" | render`, }; - // If by-value is enabled, we'll handle both by-reference and by-value embeddables - // with the new generic `embeddable` function. - // Otherwise we fallback to the embeddable type specific expressions. - if (isByValueEnabled) { - partialElement.expression = embeddableInputToExpression( - { savedObjectId: id }, - type, - undefined, - true - ); - } else if (allowedEmbeddables[type]) { - partialElement.expression = allowedEmbeddables[type](id); - } + partialElement.expression = embeddableInputToExpression( + { savedObjectId: id }, + type, + undefined, + true + ); addEmbeddable(pageId, partialElement); restProps.onClose(); }, - [addEmbeddable, pageId, restProps, isByValueEnabled] + [addEmbeddable, pageId, restProps] ); return ( @@ -111,7 +99,6 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ {...restProps} availableEmbeddables={availableEmbeddables || []} onSelect={onSelect} - isByValueEnabled={isByValueEnabled} /> ); }; diff --git a/x-pack/platform/plugins/private/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts b/x-pack/platform/plugins/private/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts index 8a78c4162d9fd..06341d8cdeaac 100644 --- a/x-pack/platform/plugins/private/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts +++ b/x-pack/platform/plugins/private/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -24,22 +24,20 @@ import { } from '../../../state/actions/embeddable'; import { clearValue } from '../../../state/actions/resolved_args'; import { embeddableInputToExpression } from '../../../../canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression'; -import { embeddableService, presentationUtilService } from '../../../services/kibana_services'; +import { embeddableService } from '../../../services/kibana_services'; const { actionsElements: strings } = ErrorStrings; export const useIncomingEmbeddable = (selectedPage: CanvasPage) => { - const labsService = presentationUtilService.labsService; const dispatch = useDispatch(); const notifyService = useNotifyService(); - const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); const stateTransferService = embeddableService.getStateTransfer(); // fetch incoming embeddable from state transfer service. const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); useEffect(() => { - if (isByValueEnabled && incomingEmbeddable) { + if (incomingEmbeddable) { const { embeddableId, serializedState: incomingState, type } = incomingEmbeddable; // retrieve existing element @@ -107,5 +105,5 @@ export const useIncomingEmbeddable = (selectedPage: CanvasPage) => { dispatch(addElement(selectedPage.id, { expression })); } } - }, [dispatch, notifyService, selectedPage, incomingEmbeddable, isByValueEnabled]); + }, [dispatch, notifyService, selectedPage, incomingEmbeddable]); }; diff --git a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/index.ts b/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/index.ts deleted file mode 100644 index fde077e88f86f..0000000000000 --- a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/index.ts +++ /dev/null @@ -1,8 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export { LabsControl } from './labs_control'; diff --git a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/labs_control.tsx b/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/labs_control.tsx deleted file mode 100644 index 6e6ca58f0dd72..0000000000000 --- a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/labs_control/labs_control.tsx +++ /dev/null @@ -1,51 +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; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonEmpty, EuiNotificationBadge } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { useState } from 'react'; - -import { LazyLabsFlyout, withSuspense } from '@kbn/presentation-util-plugin/public'; - -import { UI_SETTINGS } from '../../../../common'; -import { coreServices, presentationUtilService } from '../../../services/kibana_services'; - -const strings = { - getLabsButtonLabel: () => - i18n.translate('xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel', { - defaultMessage: 'Labs', - }), -}; - -const Flyout = withSuspense(LazyLabsFlyout, null); - -export const LabsControl = () => { - const [isShown, setIsShown] = useState(false); - - if (!coreServices.uiSettings.get(UI_SETTINGS.ENABLE_LABS_UI)) { - return null; - } - - const projects = presentationUtilService.labsService.getProjects(['canvas']); - const overrideCount = Object.values(projects).filter( - (project) => project.status.isOverride - ).length; - - return ( - <> - setIsShown(!isShown)} size="xs"> - {strings.getLabsButtonLabel()} - {overrideCount > 0 ? ( - - {overrideCount} - - ) : null} - - {isShown ? setIsShown(false)} /> : null} - - ); -}; diff --git a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/platform/plugins/private/canvas/public/components/workpad_header/workpad_header.component.tsx index dc6a96023f506..168ac91786e78 100644 --- a/x-pack/platform/plugins/private/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/platform/plugins/private/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -25,7 +25,6 @@ import { EditMenu } from './edit_menu'; import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; -import { LabsControl } from './labs_control'; import { EditorMenu } from './editor_menu'; const strings = { @@ -197,9 +196,6 @@ export const WorkpadHeader: FC = ({ - - - diff --git a/x-pack/platform/plugins/private/canvas/server/plugin.ts b/x-pack/platform/plugins/private/canvas/server/plugin.ts index 1ed92d31a41c5..22da385b353b9 100644 --- a/x-pack/platform/plugins/private/canvas/server/plugin.ts +++ b/x-pack/platform/plugins/private/canvas/server/plugin.ts @@ -29,9 +29,7 @@ import { setupInterpreter } from './setup_interpreter'; import { customElementType, workpadTypeFactory, workpadTemplateType } from './saved_objects'; import type { CanvasSavedObjectTypeMigrationsDeps } from './saved_objects/migrations'; import { initializeTemplates } from './templates'; -import { getUISettings } from './ui_settings'; -import type { CanvasRouteHandlerContext } from './workpad_route_context'; -import { createWorkpadRouteContext } from './workpad_route_context'; +import { type CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_route_context'; interface PluginsSetup { expressions: ExpressionsServerSetup; @@ -66,7 +64,6 @@ export class CanvasPlugin implements Plugin> => ({ - [UI_SETTINGS.ENABLE_LABS_UI]: { - name: i18n.translate('xpack.canvas.labs.enableUI', { - defaultMessage: 'Enable labs button in Canvas', - }), - description: i18n.translate('xpack.canvas.labs.enableLabsDescription', { - defaultMessage: - 'This flag determines if the viewer has access to the Labs button, a quick way to enable and disable technical preview features in Canvas.', - }), - value: false, - type: 'boolean', - schema: schema.boolean(), - category: [SETTING_CATEGORY], - requiresPageReload: true, - }, -}); diff --git a/x-pack/platform/plugins/private/translations/translations/de-DE.json b/x-pack/platform/plugins/private/translations/translations/de-DE.json index ba507560f2a92..48287bd8405b1 100644 --- a/x-pack/platform/plugins/private/translations/translations/de-DE.json +++ b/x-pack/platform/plugins/private/translations/translations/de-DE.json @@ -1497,8 +1497,6 @@ "dashboard.featureCatalogue.dashboardDescription": "Eine Sammlung an Visualisierungen und Suchergebnissen anzeigen und freigeben.", "dashboard.featureCatalogue.dashboardSubtitle": "Analysieren Sie Daten in Dashboards.", "dashboard.featureCatalogue.dashboardTitle": "Dashboard", - "dashboard.labs.enableLabsDescription": "Diese Flag legt fest, ob der Betrachter Zugriff auf die Schaltfläche „Labs“ hat, eine schnelle Möglichkeit, technische Vorschau-Features im Dashboard zu aktivieren und zu deaktivieren.", - "dashboard.labs.enableUI": "Schaltfläche „Labs aktivieren“ im Dashboard aktivieren", "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "Analysieren Sie alle Ihre Elastic-Daten an einem Ort, indem Sie ein Dashboard erstellen und Visualisierungen hinzufügen.", "dashboard.listing.createNewDashboard.createButtonLabel": "Dashboard erstellen", "dashboard.listing.createNewDashboard.inProgressTitle": "Dashboard in Arbeit", @@ -6026,28 +6024,6 @@ "presentationUtil.fieldPicker.selectableAriaLabel": "Feld auswählen", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "Nach Typ filtern", "presentationUtil.fieldSearch.searchPlaceHolder": "Suchfeldnamen", - "presentationUtil.labs.components.browserSwitchHelp": "Aktiviert das Labor für diesen Browser und bleibt auch nach dem Schließen bestehen.", - "presentationUtil.labs.components.browserSwitchName": "Browser", - "presentationUtil.labs.components.calloutHelp": "Aktualisieren Sie, um Änderungen anzuwenden.", - "presentationUtil.labs.components.closeButtonLabel": "Schließen", - "presentationUtil.labs.components.descriptionMessage": "Probieren Sie Features aus, die sich in der Entwicklung oder in der technischen Vorschau befinden.", - "presentationUtil.labs.components.disabledStatusMessage": "Standard: {status}", - "presentationUtil.labs.components.enabledStatusMessage": "Standard: {status}", - "presentationUtil.labs.components.kibanaSwitchHelp": "Aktiviert dieses Lab für alle Kibana-Nutzer:innen.", - "presentationUtil.labs.components.kibanaSwitchName": "Kibana", - "presentationUtil.labs.components.labFlagsLabel": "Lab-Flags", - "presentationUtil.labs.components.noProjectsinSolutionMessage": "Derzeit gibt es keine Labs in {solutionName}.", - "presentationUtil.labs.components.noProjectsMessage": "Derzeit sind keine Labs verfügbar.", - "presentationUtil.labs.components.overrideFlagsLabel": "Überschreibungen", - "presentationUtil.labs.components.overridenIconTipLabel": "Standard überschrieben", - "presentationUtil.labs.components.resetToDefaultLabel": "Auf Standardeinstellungen zurücksetzen", - "presentationUtil.labs.components.sessionSwitchHelp": "Aktiviert das Labor für diese Browsersitzung, sodass es beim Schließen zurückgesetzt wird.", - "presentationUtil.labs.components.sessionSwitchName": "Sitzung", - "presentationUtil.labs.components.titleLabel": "Labs", - "presentationUtil.labs.enableByValueEmbeddableDescription": "Aktiviert die Unterstützung für by-value-Einbettungen in Canvas", - "presentationUtil.labs.enableByValueEmbeddableName": "Einbettbare By-Value-Elemente", - "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "Alle Panele unter „the fold“ – der Bereich, der hinter dem unteren Rand des Fensters verborgen ist und auf den man durch Scrollen zugreifen kann – werden nicht sofort geladen, sondern erst, wenn sie das Ansichtsfenster betreten.", - "presentationUtil.labs.enableDeferBelowFoldProjectName": "Verschieben Sie das Laden der Panels unterhalb des \"fold\"", "presentationUtil.saveModalDashboard.addToDashboardLabel": "Zum Dashboard hinzufügen", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Zur visualisieren Bibliothek hinzugefügte Elemente sind für alle Dashboards verfügbar. Änderungen an einem Bibliothekselement erscheinen überall dort, wo es verwendet wird.", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "Vorhanden", @@ -12826,8 +12802,6 @@ "xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel": "Schließt die Tastaturkürzel-Referenz", "xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle": "Tastaturkürzel", "xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator": "oder", - "xpack.canvas.labs.enableLabsDescription": "Diese Flag legt fest, ob der Betrachter Zugriff auf die Schaltfläche 'Labs' hat, eine schnelle Möglichkeit, technische Vorschau-Features in Canvas zu aktivieren und zu deaktivieren.", - "xpack.canvas.labs.enableUI": "Schaltfläche „Labs aktivieren“ in Canvas", "xpack.canvas.lib.palettes.canvasLabel": "{CANVAS}", "xpack.canvas.lib.palettes.colorBlindLabel": "Farbenblind", "xpack.canvas.lib.palettes.earthTonesLabel": "Erdtöne", @@ -13504,7 +13478,6 @@ "xpack.canvas.workpadHeaderKioskControl.controlTitle": "Wechseln von Seiten im Vollbildmodus", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "Zyklusintervall ändern", "xpack.canvas.workpadHeaderKioskControl.disableTooltip": "Automatische Wiedergabe deaktivieren", - "xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel": "Labs", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "Elemente aktualisieren", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "Daten aktualisieren", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "Freigabe-Markup in die Zwischenablage kopiert", diff --git a/x-pack/platform/plugins/private/translations/translations/fr-FR.json b/x-pack/platform/plugins/private/translations/translations/fr-FR.json index a09feeddcb034..69cbf59a7708d 100644 --- a/x-pack/platform/plugins/private/translations/translations/fr-FR.json +++ b/x-pack/platform/plugins/private/translations/translations/fr-FR.json @@ -1514,8 +1514,6 @@ "dashboard.featureCatalogue.dashboardDescription": "Affichez et partagez une collection de visualisations et de résultats de recherche.", "dashboard.featureCatalogue.dashboardSubtitle": "Analysez des données à l’aide de tableaux de bord.", "dashboard.featureCatalogue.dashboardTitle": "Dashboard", - "dashboard.labs.enableLabsDescription": "Cet indicateur détermine si l'utilisateur a accès au bouton Ateliers, moyen rapide d'activer et de désactiver les fonctionnalités de la version d'évaluation technique dans le tableau de bord.", - "dashboard.labs.enableUI": "Activer le bouton Ateliers dans le tableau de bord", "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "Analysez toutes vos données Elastic en un seul endroit, en créant un tableau de bord et en ajoutant des visualisations.", "dashboard.listing.createNewDashboard.createButtonLabel": "Créer un tableau de bord", "dashboard.listing.createNewDashboard.inProgressTitle": "Tableau de bord en cours", @@ -6048,28 +6046,6 @@ "presentationUtil.fieldPicker.selectableAriaLabel": "Choisir un champ", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "Filtrer par type", "presentationUtil.fieldSearch.searchPlaceHolder": "Rechercher les noms de champs", - "presentationUtil.labs.components.browserSwitchHelp": "Active l'atelier pour ce navigateur et persiste après sa fermeture.", - "presentationUtil.labs.components.browserSwitchName": "Navigateur", - "presentationUtil.labs.components.calloutHelp": "Actualiser pour appliquer les modifications", - "presentationUtil.labs.components.closeButtonLabel": "Fermer", - "presentationUtil.labs.components.descriptionMessage": "Essayez des fonctionnalités en cours ou en version d'évaluation technique.", - "presentationUtil.labs.components.disabledStatusMessage": "Valeur par défaut : {status}", - "presentationUtil.labs.components.enabledStatusMessage": "Valeur par défaut : {status}", - "presentationUtil.labs.components.kibanaSwitchHelp": "Active cet atelier pour tous les utilisateurs Kibana.", - "presentationUtil.labs.components.kibanaSwitchName": "Kibana", - "presentationUtil.labs.components.labFlagsLabel": "Indicateurs d'atelier", - "presentationUtil.labs.components.noProjectsinSolutionMessage": "Aucun atelier actuellement dans {solutionName}.", - "presentationUtil.labs.components.noProjectsMessage": "Aucun atelier actuellement disponible.", - "presentationUtil.labs.components.overrideFlagsLabel": "Remplacements", - "presentationUtil.labs.components.overridenIconTipLabel": "Valeur par défaut remplacée", - "presentationUtil.labs.components.resetToDefaultLabel": "Réinitialiser aux valeurs par défaut", - "presentationUtil.labs.components.sessionSwitchHelp": "Active l’atelier pour cette session de navigateur afin de le réinitialiser lors de sa fermeture.", - "presentationUtil.labs.components.sessionSwitchName": "Session", - "presentationUtil.labs.components.titleLabel": "Ateliers", - "presentationUtil.labs.enableByValueEmbeddableDescription": "Active la prise en charge pour les éléments d'incorporation by-value dans Canvas", - "presentationUtil.labs.enableByValueEmbeddableName": "Éléments d'incorporation By-Value", - "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "Les panneaux sous \"le pli\" (la zone masquée en dessous de la fenêtre accessible en faisant défiler), ne se chargeront pas immédiatement, mais seulement lorsqu'ils entreront dans la fenêtre d'affichage.", - "presentationUtil.labs.enableDeferBelowFoldProjectName": "Différer le chargement des panneaux sous \"le pli\"", "presentationUtil.saveModalDashboard.addToDashboardLabel": "Ajouter au tableau de bord", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Les éléments ajoutés à la bibliothèque Visualize sont disponibles pour tous les tableaux de bord. Les modifications apportées à un élément de bibliothèque sont répercutées partout où il est utilisé.", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "Existant", @@ -12852,8 +12828,6 @@ "xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel": "Ferme la référence des raccourcis clavier", "xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle": "Raccourcis clavier", "xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator": "ou", - "xpack.canvas.labs.enableLabsDescription": "Cet indicateur détermine si l'utilisateur a accès au bouton Ateliers, moyen rapide d'activer et de désactiver les fonctionnalités de la version d'évaluation technique dans Canvas.", - "xpack.canvas.labs.enableUI": "Activer le bouton Ateliers dans Canvas", "xpack.canvas.lib.palettes.canvasLabel": "{CANVAS}", "xpack.canvas.lib.palettes.colorBlindLabel": "Daltonien", "xpack.canvas.lib.palettes.earthTonesLabel": "Tons terre", @@ -13533,7 +13507,6 @@ "xpack.canvas.workpadHeaderKioskControl.controlTitle": "Faire défiler les pages en plein écran", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "Modifier l'intervalle de défilement", "xpack.canvas.workpadHeaderKioskControl.disableTooltip": "Désactiver la lecture automatique", - "xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel": "Ateliers", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "Actualiser les éléments", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "Actualiser les données", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "Balisage de partage copié dans le presse-papiers", diff --git a/x-pack/platform/plugins/private/translations/translations/ja-JP.json b/x-pack/platform/plugins/private/translations/translations/ja-JP.json index 26fea2d764e88..c1cef950d3184 100644 --- a/x-pack/platform/plugins/private/translations/translations/ja-JP.json +++ b/x-pack/platform/plugins/private/translations/translations/ja-JP.json @@ -1517,8 +1517,6 @@ "dashboard.featureCatalogue.dashboardDescription": "ビジュアライゼーションと検索結果のコレクションの表示と共有を行います。", "dashboard.featureCatalogue.dashboardSubtitle": "ダッシュボードでデータを分析します。", "dashboard.featureCatalogue.dashboardTitle": "ダッシュボード", - "dashboard.labs.enableLabsDescription": "このフラグはビューアーで[ラボ]ボタンを使用できるかどうかを決定します。ダッシュボードで実験的機能を有効および無効にするための簡単な方法です。", - "dashboard.labs.enableUI": "ダッシュボードで[ラボ]ボタンを有効にする", "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "ダッシュボードを作成し、ビジュアライゼーションを追加して、すべてのElasticデータを1つの場所で分析します。", "dashboard.listing.createNewDashboard.createButtonLabel": "ダッシュボードの作成", "dashboard.listing.createNewDashboard.inProgressTitle": "実行中のダッシュボード", @@ -6051,28 +6049,6 @@ "presentationUtil.fieldPicker.selectableAriaLabel": "フィールドの選択", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "タイプでフィルタリング", "presentationUtil.fieldSearch.searchPlaceHolder": "検索フィールド名", - "presentationUtil.labs.components.browserSwitchHelp": "このブラウザーでラボを有効にします。ブラウザーを閉じた後も永続します。", - "presentationUtil.labs.components.browserSwitchName": "ブラウザー", - "presentationUtil.labs.components.calloutHelp": "変更を適用するには更新します", - "presentationUtil.labs.components.closeButtonLabel": "閉じる", - "presentationUtil.labs.components.descriptionMessage": "開発中の機能やテクニカルプレビュー中の機能を試します。", - "presentationUtil.labs.components.disabledStatusMessage": "デフォルト:{status}", - "presentationUtil.labs.components.enabledStatusMessage": "デフォルト:{status}", - "presentationUtil.labs.components.kibanaSwitchHelp": "すべてのKibanaユーザーでこのラボを有効にします。", - "presentationUtil.labs.components.kibanaSwitchName": "Kibana", - "presentationUtil.labs.components.labFlagsLabel": "ラボフラグ", - "presentationUtil.labs.components.noProjectsinSolutionMessage": "現在{solutionName}にはラボがありません。", - "presentationUtil.labs.components.noProjectsMessage": "現在ラボはありません。", - "presentationUtil.labs.components.overrideFlagsLabel": "上書き", - "presentationUtil.labs.components.overridenIconTipLabel": "デフォルトの上書き", - "presentationUtil.labs.components.resetToDefaultLabel": "デフォルトにリセット", - "presentationUtil.labs.components.sessionSwitchHelp": "このブラウザーセッションのラボを有効にします。ブラウザーを閉じるとリセットされます。", - "presentationUtil.labs.components.sessionSwitchName": "セッション", - "presentationUtil.labs.components.titleLabel": "ラボ", - "presentationUtil.labs.enableByValueEmbeddableDescription": "キャンバスでby-value埋め込み可能オブジェクトのサポートを有効にします", - "presentationUtil.labs.enableByValueEmbeddableName": "By-Value埋め込み可能オブジェクト", - "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "「区切り」の下のすべてのパネル(ウィンドウ下部の下にある非表示の領域)はすぐに読み込まれません。ビューポートを入力するときにのみ読み込まれます", - "presentationUtil.labs.enableDeferBelowFoldProjectName": "「区切り」の下のパネルの読み込みを延期", "presentationUtil.saveModalDashboard.addToDashboardLabel": "ダッシュボードに追加", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "Visualizeライブラリに追加された項目はすべてのダッシュボードで使用できます。ライブラリ項目の編集は、使用されるすべての場所に表示されます。", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "既存", @@ -12870,8 +12846,6 @@ "xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel": "キーボードショートカットリファレンスを作成", "xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle": "キーボードショートカット", "xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator": "または", - "xpack.canvas.labs.enableLabsDescription": "このフラグはビューアーで[ラボ]ボタンを使用できるかどうかを決定します。キャンバスでテクニカルプレビュー中の機能を有効および無効にするための簡単な方法です。", - "xpack.canvas.labs.enableUI": "キャンバスで[ラボ]ボタンを有効にする", "xpack.canvas.lib.palettes.canvasLabel": "{CANVAS}", "xpack.canvas.lib.palettes.colorBlindLabel": "Color Blind", "xpack.canvas.lib.palettes.earthTonesLabel": "Earth Tones", @@ -13551,7 +13525,6 @@ "xpack.canvas.workpadHeaderKioskControl.controlTitle": "全画面ページのサイクル", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "サイクル間隔を変更", "xpack.canvas.workpadHeaderKioskControl.disableTooltip": "自動再生を無効にする", - "xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel": "ラボ", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "エレメントを更新", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "データを更新", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "共有マークアップがクリップボードにコピーされました", diff --git a/x-pack/platform/plugins/private/translations/translations/zh-CN.json b/x-pack/platform/plugins/private/translations/translations/zh-CN.json index b076820f8b80d..1ddbac61c194b 100644 --- a/x-pack/platform/plugins/private/translations/translations/zh-CN.json +++ b/x-pack/platform/plugins/private/translations/translations/zh-CN.json @@ -1511,8 +1511,6 @@ "dashboard.featureCatalogue.dashboardDescription": "显示和共享可视化和搜索结果的集合。", "dashboard.featureCatalogue.dashboardSubtitle": "在仪表板中分析数据。", "dashboard.featureCatalogue.dashboardTitle": "仪表板", - "dashboard.labs.enableLabsDescription": "此标志决定查看者是否有权访问用于在仪表板中快速启用和禁用技术预览功能的“实验”按钮。", - "dashboard.labs.enableUI": "在仪表板中启用实验按钮", "dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription": "通过创建仪表板并添加可视化,在一个位置分析所有 Elastic 数据。", "dashboard.listing.createNewDashboard.createButtonLabel": "创建仪表板", "dashboard.listing.createNewDashboard.inProgressTitle": "仪表板在创建中", @@ -6044,28 +6042,6 @@ "presentationUtil.fieldPicker.selectableAriaLabel": "选择字段", "presentationUtil.fieldSearch.fieldFilterButtonLabel": "按类型筛选", "presentationUtil.fieldSearch.searchPlaceHolder": "搜索字段名称", - "presentationUtil.labs.components.browserSwitchHelp": "启用此浏览器的实验并在其关闭后继续保持。", - "presentationUtil.labs.components.browserSwitchName": "浏览器", - "presentationUtil.labs.components.calloutHelp": "刷新以应用更改", - "presentationUtil.labs.components.closeButtonLabel": "关闭", - "presentationUtil.labs.components.descriptionMessage": "试用正在开发或处于技术预览状态的功能。", - "presentationUtil.labs.components.disabledStatusMessage": "默认值:{status}", - "presentationUtil.labs.components.enabledStatusMessage": "默认值:{status}", - "presentationUtil.labs.components.kibanaSwitchHelp": "为所有 Kibana 用户启用此实验。", - "presentationUtil.labs.components.kibanaSwitchName": "Kibana", - "presentationUtil.labs.components.labFlagsLabel": "实验室标志", - "presentationUtil.labs.components.noProjectsinSolutionMessage": "{solutionName} 中当前没有实验。", - "presentationUtil.labs.components.noProjectsMessage": "当前没有可用实验。", - "presentationUtil.labs.components.overrideFlagsLabel": "覆盖", - "presentationUtil.labs.components.overridenIconTipLabel": "默认值已覆盖", - "presentationUtil.labs.components.resetToDefaultLabel": "重置为默认值", - "presentationUtil.labs.components.sessionSwitchHelp": "为此浏览器会话启用实验,以便其关闭时重置。", - "presentationUtil.labs.components.sessionSwitchName": "会话", - "presentationUtil.labs.components.titleLabel": "实验", - "presentationUtil.labs.enableByValueEmbeddableDescription": "在 Canvas 中启用按值嵌入的支持", - "presentationUtil.labs.enableByValueEmbeddableName": "按值嵌入", - "presentationUtil.labs.enableDeferBelowFoldProjectDescription": "“折叠”下的任何面板即可通过滚动访问的窗口底部隐藏的区域,将不会立即加载,而仅在进入视区时加载", - "presentationUtil.labs.enableDeferBelowFoldProjectName": "推迟加载“折叠”下的面板", "presentationUtil.saveModalDashboard.addToDashboardLabel": "添加到仪表板", "presentationUtil.saveModalDashboard.dashboardInfoTooltip": "添加到 Visualize 库的项目可用于所有仪表板。对库项目的编辑将显示在使用位置。", "presentationUtil.saveModalDashboard.existingDashboardOptionLabel": "现有", @@ -12864,8 +12840,6 @@ "xpack.canvas.keyboardShortcutsDoc.flyout.closeButtonAriaLabel": "关闭快捷键参考", "xpack.canvas.keyboardShortcutsDoc.flyoutHeaderTitle": "快捷键", "xpack.canvas.keyboardShortcutsDoc.shortcutListSeparator": "或", - "xpack.canvas.labs.enableLabsDescription": "此标志决定查看者是否有权访问用于在 Canvas 中快速启用和禁用技术预览功能的“实验”按钮。", - "xpack.canvas.labs.enableUI": "在 Canvas 中启用实验按钮", "xpack.canvas.lib.palettes.canvasLabel": "{CANVAS}", "xpack.canvas.lib.palettes.colorBlindLabel": "色盲", "xpack.canvas.lib.palettes.earthTonesLabel": "泥土色调", @@ -13545,7 +13519,6 @@ "xpack.canvas.workpadHeaderKioskControl.controlTitle": "循环播放全屏页面", "xpack.canvas.workpadHeaderKioskControl.cycleFormLabel": "更改循环播放时间间隔", "xpack.canvas.workpadHeaderKioskControl.disableTooltip": "禁用自动播放", - "xpack.canvas.workpadHeaderLabsControlSettings.labsButtonLabel": "实验", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshAriaLabel": "刷新元素", "xpack.canvas.workpadHeaderRefreshControlSettings.refreshTooltip": "刷新数据", "xpack.canvas.workpadHeaderShareMenu.copyShareConfigMessage": "已将共享标记复制到剪贴板", diff --git a/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts b/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts index 23651348527fb..c783b7c0dd796 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/data_request_actions.ts @@ -193,7 +193,11 @@ export function syncDataForLayerDueToDrawing(layer: ILayer) { true, false ); - if (!layer.isVisible() || !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom)) { + if ( + getState().map.__pauseSyncData || + !layer.isVisible() || + !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom) + ) { return; } await layer.syncData(dataRequestContext); @@ -209,7 +213,12 @@ export function syncDataForLayer(layer: ILayer, isForceRefresh: boolean) { false, isForceRefresh ); - if (!layer.isVisible() || !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom)) { + + if ( + getState().map.__pauseSyncData || + !layer.isVisible() || + !layer.showAtZoomLevel(dataRequestContext.dataFilters.zoom) + ) { return; } await layer.syncData(dataRequestContext); diff --git a/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts b/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts index a1471209f73b7..7f290be925865 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/map_action_constants.ts @@ -22,6 +22,7 @@ export const LAYER_DATA_LOAD_ERROR = 'LAYER_DATA_LOAD_ERROR'; export const UPDATE_SOURCE_DATA_REQUEST = 'UPDATE_SOURCE_DATA_REQUEST'; export const SET_JOINS = 'SET_JOINS'; export const SET_QUERY = 'SET_QUERY'; +export const SET_PAUSE_SYNC_DATA = 'SET_PAUSE_SYNC_DATA'; export const UPDATE_LAYER = 'UPDATE_LAYER'; export const UPDATE_LAYER_PROP = 'UPDATE_LAYER_PROP'; export const UPDATE_LAYER_STYLE = 'UPDATE_LAYER_STYLE'; diff --git a/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts b/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts index be38c1b450840..236ed936e3025 100644 --- a/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts +++ b/x-pack/platform/plugins/shared/maps/public/actions/map_actions.ts @@ -52,6 +52,7 @@ import { SET_MOUSE_COORDINATES, SET_OPEN_TOOLTIPS, SET_QUERY, + SET_PAUSE_SYNC_DATA, TRACK_MAP_SETTINGS, UPDATE_DRAW_STATE, UPDATE_MAP_SETTING, @@ -79,6 +80,26 @@ import { expandToTileBoundaries, getTilesForExtent } from '../classes/util/geo_t import { getToasts } from '../kibana_services'; import { getDeletedFeatureIds } from '../selectors/ui_selectors'; +export function setPauseSyncData(pauseSyncData: boolean) { + return ( + dispatch: ThunkDispatch, + getState: () => MapStoreState + ) => { + dispatch({ + type: SET_PAUSE_SYNC_DATA, + pauseSyncData, + }); + + if (!pauseSyncData) { + if (getMapSettings(getState()).autoFitToDataBounds) { + dispatch(autoFitToBounds()); + } else { + dispatch(syncDataForAllLayers(false)); + } + } + }; +} + export function setMapInitError(errorMessage: string) { return { type: SET_MAP_INIT_ERROR, diff --git a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts index 202975cad319b..9c4f96d64e464 100644 --- a/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts +++ b/x-pack/platform/plugins/shared/maps/public/react_embeddable/initialize_fetch.ts @@ -5,14 +5,15 @@ * 2.0. */ +import type { Subscription } from 'rxjs'; import type { FetchContext } from '@kbn/presentation-publishing'; -import { fetch$ } from '@kbn/presentation-publishing'; +import { apiPublishesPauseFetch, fetch$ } from '@kbn/presentation-publishing'; import type { Query } from '@kbn/es-query'; import type { MapExtent } from '../../common/descriptor_types'; import { getSearchService } from '../kibana_services'; import type { MapStore } from '../reducers/store'; import type { MapApi } from './types'; -import { setMapSettings, setQuery } from '../actions'; +import { setMapSettings, setQuery, setPauseSyncData } from '../actions'; function getIsRestore(searchSessionId?: string) { if (!searchSessionId) { @@ -36,6 +37,7 @@ export function initializeFetch({ store: MapStore; }) { let prevIsRestore: boolean | undefined; + const fetchSubscription = fetch$(api).subscribe((fetchContext: FetchContext) => { // New search session id causes all layers from elasticsearch to refetch data. // Dashboard provides a new search session id anytime filters change. @@ -77,7 +79,16 @@ export function initializeFetch({ }) ); }); + + let isPausedSubscription: Subscription | undefined; + if (apiPublishesPauseFetch(api)) { + isPausedSubscription = api.isFetchPaused$.subscribe((isFetchPaused) => { + store.dispatch(setPauseSyncData(isFetchPaused)); + }); + } + return () => { fetchSubscription.unsubscribe(); + isPausedSubscription?.unsubscribe(); }; } diff --git a/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts b/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts index bb19a67275cf0..1650485fdaf77 100644 --- a/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts +++ b/x-pack/platform/plugins/shared/maps/public/reducers/map/map.ts @@ -47,6 +47,7 @@ import { UPDATE_MAP_SETTING, UPDATE_EDIT_STATE, SET_EXECUTION_CONTEXT, + SET_PAUSE_SYNC_DATA, } from '../../actions/map_action_constants'; import { getDefaultMapSettings } from './default_map_settings'; @@ -86,6 +87,7 @@ export const DEFAULT_MAP_STATE: MapState = { waitingForMapReadyLayerList: [], settings: getDefaultMapSettings(), __rollbackSettings: null, + __pauseSyncData: false, }; export function map(state: MapState = DEFAULT_MAP_STATE, action: Record) { @@ -225,6 +227,11 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: Record layer.id === action.selectedLayerId); return { ...state, selectedLayerId: selectedMatch ? action.selectedLayerId : null }; diff --git a/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts b/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts index 129c80fc79c65..942ef7baf4e3c 100644 --- a/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts +++ b/x-pack/platform/plugins/shared/maps/public/reducers/map/types.ts @@ -64,4 +64,5 @@ export type MapState = { waitingForMapReadyLayerList: LayerDescriptor[]; settings: MapSettings; __rollbackSettings: MapSettings | null; + __pauseSyncData: boolean; };