From 7803cd17a16e840944e0260835d194dca66c3cef Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Thu, 2 Oct 2025 10:42:05 -0600 Subject: [PATCH] [dashboard] fixs controls cause double fetch (#237169) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes https://github.com/elastic/kibana/issues/237147 PR updates dashboard to wait until controls are ready before rendering panels. This prevents a double fetch when panels would fetch data while controls where building filters and then fetch data again once controls filters are available. This PR introduces a small performance degradation to dashboards with controls. Panels will not start rendering until controls have finished initializing. This is a better performance trade-off then the current behavior of issuing a new round of requests once controls are ready. ### Testing * install sample web logs * create new dashboard * add options list control on field `machine.os.keyword` * Select `ios` in control * add metric vis with count * save dashboard * open dashboard filter network requests to `ese`. Ensure metric chart only makes a single request Screenshot 2025-10-01 at 12 39
45 PM --------- Co-authored-by: Elastic Machine (cherry picked from commit 7f7bd4fd3b5d731ba9f2e073f4a2a82efad0cd4c) # Conflicts: # src/platform/plugins/shared/dashboard/public/dashboard_api/control_group_manager.ts # src/platform/plugins/shared/dashboard/public/dashboard_api/get_dashboard_api.ts # src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts # src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx --- .../dashboard_api/control_group_manager.ts | 19 ++++++- .../public/dashboard_api/get_dashboard_api.ts | 1 + .../dashboard/public/dashboard_api/types.ts | 1 + .../viewport/dashboard_viewport.test.tsx | 57 +++++++++++++++++-- .../viewport/dashboard_viewport.tsx | 17 ++---- 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_api/control_group_manager.ts b/src/platform/plugins/shared/dashboard/public/dashboard_api/control_group_manager.ts index c8aacc0aa1405..721758ec05fbf 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/control_group_manager.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/control_group_manager.ts @@ -9,7 +9,7 @@ import type { Reference } from '@kbn/content-management-utils'; import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public'; -import { BehaviorSubject } from 'rxjs'; +import { BehaviorSubject, first, skipWhile, switchMap } from 'rxjs'; export const CONTROL_GROUP_EMBEDDABLE_ID = 'CONTROL_GROUP_EMBEDDABLE_ID'; @@ -19,6 +19,22 @@ export function initializeControlGroupManager( ) { const controlGroupApi$ = new BehaviorSubject(undefined); + async function untilControlsInitialized(): Promise { + return new Promise((resolve) => { + controlGroupApi$ + .pipe( + skipWhile((controlGroupApi) => !controlGroupApi), + switchMap(async (controlGroupApi) => { + await controlGroupApi?.untilInitialized(); + }), + first() + ) + .subscribe(() => { + resolve(); + }); + }); + } + return { api: { controlGroupApi$, @@ -52,6 +68,7 @@ export function initializeControlGroupManager( }, setControlGroupApi: (controlGroupApi: ControlGroupApi) => controlGroupApi$.next(controlGroupApi), + untilControlsInitialized, }, }; } 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 a3575a6144538..1f999d715dea7 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 @@ -232,6 +232,7 @@ export function getDashboardApi({ ...layoutManager.internalApi, ...unifiedSearchManager.internalApi, setControlGroupApi: controlGroupManager.internalApi.setControlGroupApi, + untilControlsInitialized: controlGroupManager.internalApi.untilControlsInitialized, }; const searchSessionManager = initializeSearchSessionManager( 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 246bcc0792d59..4afc0ec2d879a 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts +++ b/src/platform/plugins/shared/dashboard/public/dashboard_api/types.ts @@ -159,4 +159,5 @@ export interface DashboardInternalApi { setControlGroupApi: (controlGroupApi: ControlGroupApi) => void; serializeLayout: () => Pick; isSectionCollapsed: (sectionId?: string) => boolean; + untilControlsInitialized: () => Promise; } diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx index 2390078274096..790358d3e184b 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.test.tsx @@ -16,8 +16,16 @@ import { DashboardContext } from '../../dashboard_api/use_dashboard_api'; import { DashboardInternalContext } from '../../dashboard_api/use_dashboard_internal_api'; import { buildMockDashboardApi, getMockPanels } from '../../mocks'; import { DashboardViewport } from './dashboard_viewport'; +import { BehaviorSubject, first, skipWhile } from 'rxjs'; +import type { DashboardInternalApi } from '../../dashboard_api/types'; -const createAndMountDashboardViewport = async () => { +jest.mock('../grid', () => { + return { + DashboardGrid: () =>
, + }; +}); + +const renderDashboardViewport = async (internalApiOverrides?: Partial) => { const panels = getMockPanels(); const { api, internalApi } = buildMockDashboardApi({ overrides: { @@ -27,7 +35,12 @@ const createAndMountDashboardViewport = async () => { const component = render( - + @@ -36,19 +49,51 @@ const createAndMountDashboardViewport = async () => { // wait for first render await waitFor(() => { - expect(component.queryAllByTestId('dashboardPanel').length).toBe(Object.keys(panels).length); + component.getByTestId('dshDashboardViewport'); }); return { dashboardApi: api, internalApi, component }; }; describe('DashboardViewport', () => { - test('renders', async () => { - await createAndMountDashboardViewport(); + test('should render DashboardGrid when dashboard has panels', async () => { + const { component } = await renderDashboardViewport(); + await waitFor(() => { + component.getByTestId('mockDashboardGrid'); + }); + }); + + test('should not render DashboardGrid until controls are ready', async () => { + const controlsReadyMock$ = new BehaviorSubject(false); + const { component } = await renderDashboardViewport({ + untilControlsInitialized: () => { + return new Promise((resolve) => { + controlsReadyMock$ + .pipe( + skipWhile((controlsReady) => !controlsReady), + first() + ) + .subscribe(() => { + resolve(); + }); + }); + }, + }); + + await waitFor(() => { + expect(component.queryByTestId('mockDashboardGrid')).toBeNull(); + }); + + // simulate controls ready + controlsReadyMock$.next(true); + + await waitFor(() => { + component.getByTestId('mockDashboardGrid'); + }); }); test('renders print mode styles', async () => { - const { component, dashboardApi } = await createAndMountDashboardViewport(); + const { component, dashboardApi } = await renderDashboardViewport(); dashboardApi.setViewMode('print'); await waitFor(() => { diff --git a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx index 5a40531ba7621..32e08b66d4c20 100644 --- a/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx +++ b/src/platform/plugins/shared/dashboard/public/dashboard_renderer/viewport/dashboard_viewport.tsx @@ -85,23 +85,18 @@ export const DashboardViewport = ({ }; }, [controlGroupApi]); - // Bug in main where panels are loaded before control filters are ready - // Want to migrate to react embeddable controls with same behavior - // TODO - do not load panels until control filters are ready - /* - const [dashboardInitialized, setDashboardInitialized] = useState(false); + const [controlsReady, setControlsReady] = useState(false); useEffect(() => { let ignore = false; - dashboard.untilContainerInitialized().then(() => { + dashboardInternalApi.untilControlsInitialized().then(() => { if (!ignore) { - setDashboardInitialized(true); + setControlsReady(true); } }); return () => { ignore = true; }; - }, [dashboard]); - */ + }, [dashboardInternalApi]); const styles = useMemoCss(dashboardViewportStyles); @@ -147,9 +142,9 @@ export const DashboardViewport = ({ > {panelCount === 0 && sectionCount === 0 ? ( - ) : ( + ) : viewMode === 'print' || controlsReady ? ( - )} + ) : null}
);