[dashboard] Migrate Dashboard internal components to DashboardApi (#193220)

PR replaces `useDashboardContainer` with `useDashboardApi`.
`useDashboardApi` returns `DashboardApi` instead of
`DashboardContainer`.

After this PR, all react context's in dashboard return `DashboardApi`
and thus all components are now prepared for the migration from
DashboardContainer to DashboardApi.

---------

Co-authored-by: Hannah Mudge <Heenawter@users.noreply.github.com>
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Nathan Reese 2024-09-23 09:21:37 -06:00 committed by GitHub
parent 3fa5bdf873
commit 92da1767f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 299 additions and 181 deletions

View file

@ -9,13 +9,17 @@
import {
CanExpandPanels,
HasRuntimeChildState,
HasSerializedChildState,
PresentationContainer,
SerializedPanelState,
TracksOverlays,
} from '@kbn/presentation-containers';
import {
HasAppContext,
HasType,
PublishesDataViews,
PublishesPanelDescription,
PublishesPanelTitle,
PublishesSavedObjectId,
PublishesUnifiedSearch,
@ -23,41 +27,64 @@ import {
PublishingSubject,
ViewMode,
} from '@kbn/presentation-publishing';
import { ControlGroupApi } from '@kbn/controls-plugin/public';
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
import { Filter, Query, TimeRange } from '@kbn/es-query';
import { DefaultEmbeddableApi, ErrorEmbeddable, IEmbeddable } from '@kbn/embeddable-plugin/public';
import { DashboardPanelMap, DashboardPanelState } from '../../common';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
import { DashboardStateFromSettingsFlyout, UnsavedPanelState } from '../dashboard_container/types';
export type DashboardApi = CanExpandPanels &
HasAppContext &
HasRuntimeChildState &
HasSerializedChildState &
HasType<'dashboard'> &
PresentationContainer &
PublishesDataViews &
PublishesPanelDescription &
Pick<PublishesPanelTitle, 'panelTitle'> &
PublishesSavedObjectId &
PublishesUnifiedSearch &
PublishesViewMode &
TracksOverlays & {
addFromLibrary: () => void;
animatePanelTransforms$: PublishingSubject<boolean | undefined>;
asyncResetToLastSavedState: () => Promise<void>;
controlGroupApi$: PublishingSubject<ControlGroupApi | undefined>;
embeddedExternally$: PublishingSubject<boolean | undefined>;
fullScreenMode$: PublishingSubject<boolean | undefined>;
focusedPanelId$: PublishingSubject<string | undefined>;
forceRefresh: () => void;
getRuntimeStateForControlGroup: () => UnsavedPanelState | undefined;
getSerializedStateForControlGroup: () => SerializedPanelState<ControlGroupSerializedState>;
getSettings: () => DashboardStateFromSettingsFlyout;
getDashboardPanelFromId: (id: string) => Promise<DashboardPanelState>;
getPanelsState: () => DashboardPanelMap;
hasOverlays$: PublishingSubject<boolean | undefined>;
hasRunMigrations$: PublishingSubject<boolean | undefined>;
hasUnsavedChanges$: PublishingSubject<boolean | undefined>;
highlightPanel: (panelRef: HTMLDivElement) => void;
highlightPanelId$: PublishingSubject<string | undefined>;
managed$: PublishingSubject<boolean | undefined>;
panels$: PublishingSubject<DashboardPanelMap>;
registerChildApi: (api: DefaultEmbeddableApi) => void;
runInteractiveSave: (interactionMode: ViewMode) => Promise<SaveDashboardReturn | undefined>;
runQuickSave: () => Promise<void>;
scrollToPanel: (panelRef: HTMLDivElement) => void;
scrollToPanelId$: PublishingSubject<string | undefined>;
scrollToTop: () => void;
setControlGroupApi: (controlGroupApi: ControlGroupApi) => void;
setSettings: (settings: DashboardStateFromSettingsFlyout) => void;
setFilters: (filters?: Filter[] | undefined) => void;
setFullScreenMode: (fullScreenMode: boolean) => void;
setPanels: (panels: DashboardPanelMap) => void;
setQuery: (query?: Query | undefined) => void;
setTags: (tags: string[]) => void;
setTimeRange: (timeRange?: TimeRange | undefined) => void;
setViewMode: (viewMode: ViewMode) => void;
openSettingsFlyout: () => void;
useMargins$: PublishingSubject<boolean | undefined>;
// TODO replace with HasUniqueId once dashboard is refactored and navigateToDashboard is removed
uuid$: PublishingSubject<string>;
// TODO remove types below this line - from legacy embeddable system
untilEmbeddableLoaded: (id: string) => Promise<IEmbeddable | ErrorEmbeddable>;
};

View file

@ -23,6 +23,7 @@ import { CHANGE_CHECK_DEBOUNCE } from '../../dashboard_constants';
import { confirmDiscardUnsavedChanges } from '../../dashboard_listing/confirm_overlays';
import { SaveDashboardReturn } from '../../services/dashboard_content_management/types';
import { useDashboardApi } from '../../dashboard_api/use_dashboard_api';
import { openSettingsFlyout } from '../../dashboard_container/embeddable/api';
export const useDashboardMenuItems = ({
isLabsShown,
@ -84,7 +85,7 @@ export const useDashboardMenuItems = ({
anchorElement,
savedObjectId: lastSavedId,
isDirty: Boolean(hasUnsavedChanges),
getPanelsState: dashboardApi.getPanelsState,
getPanelsState: () => dashboardApi.panels$.value,
});
},
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboardApi]
@ -227,7 +228,7 @@ export const useDashboardMenuItems = ({
id: 'settings',
testId: 'dashboardSettingsButton',
disableButton: disableTopNav,
run: () => dashboardApi.openSettingsFlyout(),
run: () => openSettingsFlyout(dashboardApi),
},
};
}, [

View file

@ -14,7 +14,8 @@ import { findTestSubject } from '@elastic/eui/lib/test';
import { buildMockDashboard } from '../../../mocks';
import { DashboardEmptyScreen } from './dashboard_empty_screen';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardContainerContext } from '../../embeddable/dashboard_container';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
import { ViewMode } from '@kbn/embeddable-plugin/public';
pluginServices.getServices().visualizations.getAliases = jest
@ -23,11 +24,11 @@ pluginServices.getServices().visualizations.getAliases = jest
describe('DashboardEmptyScreen', () => {
function mountComponent(viewMode: ViewMode) {
const dashboardContainer = buildMockDashboard({ overrides: { viewMode } });
const dashboardApi = buildMockDashboard({ overrides: { viewMode } }) as DashboardApi;
return mountWithIntl(
<DashboardContainerContext.Provider value={dashboardContainer}>
<DashboardContext.Provider value={dashboardApi}>
<DashboardEmptyScreen />
</DashboardContainerContext.Provider>
</DashboardContext.Provider>
);
}

View file

@ -22,9 +22,10 @@ import {
import { METRIC_TYPE } from '@kbn/analytics';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { DASHBOARD_UI_METRIC_ID } from '../../../dashboard_constants';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { emptyScreenStrings } from '../../_dashboard_container_strings';
export function DashboardEmptyScreen() {
@ -45,13 +46,19 @@ export function DashboardEmptyScreen() {
[getVisTypeAliases]
);
const dashboardContainer = useDashboardContainer();
const dashboardApi = useDashboardApi();
const isDarkTheme = useObservable(theme$)?.darkMode;
const isEditMode =
dashboardContainer.select((state) => state.explicitInput.viewMode) === ViewMode.EDIT;
const embeddableAppContext = dashboardContainer.getAppContext();
const originatingPath = embeddableAppContext?.getCurrentPath?.() ?? '';
const originatingApp = embeddableAppContext?.currentAppId;
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
const isEditMode = useMemo(() => {
return viewMode === 'edit';
}, [viewMode]);
const { originatingPath, originatingApp } = useMemo(() => {
const appContext = dashboardApi.getAppContext();
return {
originatingApp: appContext?.currentAppId,
originatingPath: appContext?.getCurrentPath?.() ?? '',
};
}, [dashboardApi]);
const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.alias) return;
@ -128,7 +135,7 @@ export function DashboardEmptyScreen() {
<EuiButtonEmpty
flush="left"
iconType="folderOpen"
onClick={() => dashboardContainer.addFromLibrary()}
onClick={() => dashboardApi.addFromLibrary()}
>
{emptyScreenStrings.getAddFromLibraryButtonTitle()}
</EuiButtonEmpty>
@ -138,10 +145,7 @@ export function DashboardEmptyScreen() {
}
if (showWriteControls) {
return (
<EuiButton
iconType="pencil"
onClick={() => dashboardContainer.dispatch.setViewMode(ViewMode.EDIT)}
>
<EuiButton iconType="pencil" onClick={() => dashboardApi.setViewMode(ViewMode.EDIT)}>
{emptyScreenStrings.getEditLinkTitle()}
</EuiButton>
);

View file

@ -15,7 +15,9 @@ import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_
import { DashboardGrid } from './dashboard_grid';
import { buildMockDashboard } from '../../../mocks';
import type { Props as DashboardGridItemProps } from './dashboard_grid_item';
import { DashboardContainerContext } from '../../embeddable/dashboard_container';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
import { DashboardPanelMap } from '../../../../common';
jest.mock('./dashboard_grid_item', () => {
return {
@ -45,59 +47,62 @@ jest.mock('./dashboard_grid_item', () => {
};
});
const createAndMountDashboardGrid = async () => {
const PANELS = {
'1': {
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
type: CONTACT_CARD_EMBEDDABLE,
explicitInput: { id: '1' },
},
'2': {
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
type: CONTACT_CARD_EMBEDDABLE,
explicitInput: { id: '2' },
},
};
const createAndMountDashboardGrid = async (panels: DashboardPanelMap = PANELS) => {
const dashboardContainer = buildMockDashboard({
overrides: {
panels: {
'1': {
gridData: { x: 0, y: 0, w: 6, h: 6, i: '1' },
type: CONTACT_CARD_EMBEDDABLE,
explicitInput: { id: '1' },
},
'2': {
gridData: { x: 6, y: 6, w: 6, h: 6, i: '2' },
type: CONTACT_CARD_EMBEDDABLE,
explicitInput: { id: '2' },
},
},
panels,
},
});
await dashboardContainer.untilContainerInitialized();
const component = mountWithIntl(
<DashboardContainerContext.Provider value={dashboardContainer}>
<DashboardContext.Provider value={dashboardContainer as DashboardApi}>
<DashboardGrid viewportWidth={1000} />
</DashboardContainerContext.Provider>
</DashboardContext.Provider>
);
return { dashboardContainer, component };
return { dashboardApi: dashboardContainer, component };
};
test('renders DashboardGrid', async () => {
const { component } = await createAndMountDashboardGrid();
const { component } = await createAndMountDashboardGrid(PANELS);
const panelElements = component.find('GridItem');
expect(panelElements.length).toBe(2);
});
test('renders DashboardGrid with no visualizations', async () => {
const { dashboardContainer, component } = await createAndMountDashboardGrid();
dashboardContainer.updateInput({ panels: {} });
component.update();
const { component } = await createAndMountDashboardGrid({});
expect(component.find('GridItem').length).toBe(0);
});
test('DashboardGrid removes panel when removed from container', async () => {
const { dashboardContainer, component } = await createAndMountDashboardGrid();
const originalPanels = dashboardContainer.getInput().panels;
const filteredPanels = { ...originalPanels };
delete filteredPanels['1'];
dashboardContainer.updateInput({ panels: filteredPanels });
const { dashboardApi, component } = await createAndMountDashboardGrid(PANELS);
expect(component.find('GridItem').length).toBe(2);
dashboardApi.setPanels({
'2': PANELS['2'],
});
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
const panelElements = component.find('GridItem');
expect(panelElements.length).toBe(1);
expect(component.find('GridItem').length).toBe(1);
});
test('DashboardGrid renders expanded panel', async () => {
const { dashboardContainer, component } = await createAndMountDashboardGrid();
dashboardContainer.setExpandedPanelId('1');
const { dashboardApi, component } = await createAndMountDashboardGrid();
dashboardApi.setExpandedPanelId('1');
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
expect(component.find('GridItem').length).toBe(2);
@ -105,7 +110,8 @@ test('DashboardGrid renders expanded panel', async () => {
expect(component.find('#mockDashboardGridItem_1').hasClass('expandedPanel')).toBe(true);
expect(component.find('#mockDashboardGridItem_2').hasClass('hiddenPanel')).toBe(true);
dashboardContainer.setExpandedPanelId();
dashboardApi.setExpandedPanelId();
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
expect(component.find('GridItem').length).toBe(2);
@ -114,8 +120,9 @@ test('DashboardGrid renders expanded panel', async () => {
});
test('DashboardGrid renders focused panel', async () => {
const { dashboardContainer, component } = await createAndMountDashboardGrid();
dashboardContainer.setFocusedPanelId('2');
const { dashboardApi, component } = await createAndMountDashboardGrid();
dashboardApi.setFocusedPanelId('2');
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
// Both panels should still exist in the dom, so nothing needs to be re-fetched once minimized.
expect(component.find('GridItem').length).toBe(2);
@ -123,7 +130,8 @@ test('DashboardGrid renders focused panel', async () => {
expect(component.find('#mockDashboardGridItem_1').hasClass('blurredPanel')).toBe(true);
expect(component.find('#mockDashboardGridItem_2').hasClass('focusedPanel')).toBe(true);
dashboardContainer.setFocusedPanelId(undefined);
dashboardApi.setFocusedPanelId(undefined);
await new Promise((resolve) => setTimeout(resolve, 1));
component.update();
expect(component.find('GridItem').length).toBe(2);

View file

@ -17,23 +17,26 @@ import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layo
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardPanelState } from '../../../../common';
import { DashboardGridItem } from './dashboard_grid_item';
import { useDashboardGridSettings } from './use_dashboard_grid_settings';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { getPanelLayoutsAreEqual } from '../../state/diffing/dashboard_diffing_utils';
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
const dashboard = useDashboardContainer();
const panels = dashboard.select((state) => state.explicitInput.panels);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
const focusedPanelId = dashboard.select((state) => state.componentState.focusedPanelId);
const animatePanelTransforms = dashboard.select(
(state) => state.componentState.animatePanelTransforms
);
const dashboardApi = useDashboardApi();
const [animatePanelTransforms, expandedPanelId, focusedPanelId, panels, useMargins, viewMode] =
useBatchedPublishingSubjects(
dashboardApi.animatePanelTransforms$,
dashboardApi.expandedPanelId,
dashboardApi.focusedPanelId$,
dashboardApi.panels$,
dashboardApi.useMargins$,
dashboardApi.viewMode
);
/**
* Track panel maximized state delayed by one tick and use it to prevent
@ -96,10 +99,10 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
{} as { [key: string]: DashboardPanelState }
);
if (!getPanelLayoutsAreEqual(panels, updatedPanels)) {
dashboard.dispatch.setPanels(updatedPanels);
dashboardApi.setPanels(updatedPanels);
}
},
[dashboard, panels, viewMode]
[dashboardApi, panels, viewMode]
);
const classes = classNames({
@ -110,7 +113,7 @@ export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
});
const { layouts, breakpoints, columns } = useDashboardGridSettings(panelsInOrder);
const { layouts, breakpoints, columns } = useDashboardGridSettings(panelsInOrder, panels);
// in print mode, dashboard layout is not controlled by React Grid Layout
if (viewMode === ViewMode.PRINT) {

View file

@ -14,7 +14,8 @@ import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_
import { buildMockDashboard } from '../../../mocks';
import { Item, Props as DashboardGridItemProps } from './dashboard_grid_item';
import { DashboardContainerContext } from '../../embeddable/dashboard_container';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
jest.mock('@kbn/embeddable-plugin/public', () => {
const original = jest.requireActual('@kbn/embeddable-plugin/public');
@ -44,14 +45,14 @@ const createAndMountDashboardGridItem = (props: DashboardGridItemProps) => {
explicitInput: { id: '2' },
},
};
const dashboardContainer = buildMockDashboard({ overrides: { panels } });
const dashboardApi = buildMockDashboard({ overrides: { panels } }) as DashboardApi;
const component = mountWithIntl(
<DashboardContainerContext.Provider value={dashboardContainer}>
<DashboardContext.Provider value={dashboardApi}>
<Item {...props} />
</DashboardContainerContext.Provider>
</DashboardContext.Provider>
);
return { dashboardContainer, component };
return { dashboardApi, component };
};
test('renders Item', async () => {

View file

@ -9,12 +9,13 @@
import { EuiLoadingChart } from '@elastic/eui';
import { css } from '@emotion/react';
import { EmbeddablePanel, ReactEmbeddableRenderer, ViewMode } from '@kbn/embeddable-plugin/public';
import { EmbeddablePanel, ReactEmbeddableRenderer } from '@kbn/embeddable-plugin/public';
import classNames from 'classnames';
import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardPanelState } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
type DivProps = Pick<React.HTMLAttributes<HTMLDivElement>, 'className' | 'style' | 'children'>;
@ -45,10 +46,13 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
},
ref
) => {
const container = useDashboardContainer();
const scrollToPanelId = container.select((state) => state.componentState.scrollToPanelId);
const highlightPanelId = container.select((state) => state.componentState.highlightPanelId);
const useMargins = container.select((state) => state.explicitInput.useMargins);
const dashboardApi = useDashboardApi();
const [highlightPanelId, scrollToPanelId, useMargins, viewMode] = useBatchedPublishingSubjects(
dashboardApi.highlightPanelId$,
dashboardApi.scrollToPanelId$,
dashboardApi.useMargins$,
dashboardApi.viewMode
);
const expandPanel = expandedPanelId !== undefined && expandedPanelId === id;
const hidePanel = expandedPanelId !== undefined && expandedPanelId !== id;
@ -60,17 +64,17 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
'dshDashboardGrid__item--focused': focusPanel,
'dshDashboardGrid__item--blurred': blurPanel,
// eslint-disable-next-line @typescript-eslint/naming-convention
printViewport__vis: container.getInput().viewMode === ViewMode.PRINT,
printViewport__vis: viewMode === 'print',
});
useLayoutEffect(() => {
if (typeof ref !== 'function' && ref?.current) {
const panelRef = ref.current;
if (scrollToPanelId === id) {
container.scrollToPanel(panelRef);
dashboardApi.scrollToPanel(panelRef);
}
if (highlightPanelId === id) {
container.highlightPanel(panelRef);
dashboardApi.highlightPanel(panelRef);
}
panelRef.querySelectorAll('*').forEach((e) => {
@ -83,7 +87,7 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
}
});
}
}, [id, container, scrollToPanelId, highlightPanelId, ref, blurPanel]);
}, [id, dashboardApi, scrollToPanelId, highlightPanelId, ref, blurPanel]);
const focusStyles = blurPanel
? css`
@ -110,10 +114,10 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
<ReactEmbeddableRenderer
type={type}
maybeId={id}
getParentApi={() => container}
getParentApi={() => dashboardApi}
key={`${type}_${id}`}
panelProps={panelProps}
onApiAvailable={(api) => container.registerChildApi(api)}
onApiAvailable={(api) => dashboardApi.registerChildApi(api)}
/>
);
}
@ -122,11 +126,11 @@ export const Item = React.forwardRef<HTMLDivElement, Props>(
<EmbeddablePanel
key={type}
index={index}
embeddable={() => container.untilEmbeddableLoaded(id)}
embeddable={() => dashboardApi.untilEmbeddableLoaded(id)}
{...panelProps}
/>
);
}, [id, container, type, index, useMargins]);
}, [id, dashboardApi, type, index, useMargins]);
return (
<div
@ -189,14 +193,14 @@ export const DashboardGridItem = React.forwardRef<HTMLDivElement, Props>((props,
const {
settings: { isProjectEnabledInLabs },
} = pluginServices.getServices();
const container = useDashboardContainer();
const focusedPanelId = container.select((state) => state.componentState.focusedPanelId);
const dashboardApi = useDashboardApi();
const [focusedPanelId, viewMode] = useBatchedPublishingSubjects(
dashboardApi.focusedPanelId$,
dashboardApi.viewMode
);
const dashboard = useDashboardContainer();
const isPrintMode = dashboard.select((state) => state.explicitInput.viewMode) === ViewMode.PRINT;
const isEnabled =
!isPrintMode &&
viewMode !== 'print' &&
isProjectEnabledInLabs('labs:dashboard:deferBelowFold') &&
(!focusedPanelId || focusedPanelId === props.id);

View file

@ -12,15 +12,16 @@ import { useMemo } from 'react';
import { useEuiTheme } from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { DashboardPanelMap } from '../../../../common';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
export const useDashboardGridSettings = (panelsInOrder: string[]) => {
const dashboard = useDashboardContainer();
export const useDashboardGridSettings = (panelsInOrder: string[], panels: DashboardPanelMap) => {
const dashboardApi = useDashboardApi();
const { euiTheme } = useEuiTheme();
const panels = dashboard.select((state) => state.explicitInput.panels);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const viewMode = useStateFromPublishingSubject(dashboardApi.viewMode);
const layouts = useMemo(() => {
return {

View file

@ -31,7 +31,7 @@ import {
import { FormattedMessage } from '@kbn/i18n-react';
import { DashboardContainerInput } from '../../../../common';
import { pluginServices } from '../../../services/plugin_services';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
interface DashboardSettingsProps {
onClose: () => void;
@ -45,19 +45,14 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
dashboardContentManagement: { checkForDuplicateDashboardTitle },
} = pluginServices.getServices();
const dashboard = useDashboardContainer();
const dashboardApi = useDashboardApi();
const [dashboardSettingsState, setDashboardSettingsState] = useState({
...dashboard.getInput(),
});
const [localSettings, setLocalSettings] = useState(dashboardApi.getSettings());
const [isTitleDuplicate, setIsTitleDuplicate] = useState(false);
const [isTitleDuplicateConfirmed, setIsTitleDuplicateConfirmed] = useState(false);
const [isApplying, setIsApplying] = useState(false);
const lastSavedId = dashboard.select((state) => state.componentState.lastSavedId);
const lastSavedTitle = dashboard.select((state) => state.explicitInput.title);
const isMounted = useMountedState();
const onTitleDuplicate = () => {
@ -69,9 +64,9 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
const onApply = async () => {
setIsApplying(true);
const validTitle = await checkForDuplicateDashboardTitle({
title: dashboardSettingsState.title,
title: localSettings.title,
copyOnSave: false,
lastSavedTitle,
lastSavedTitle: dashboardApi.panelTitle.value ?? '',
onTitleDuplicate,
isTitleDuplicateConfirmed,
});
@ -81,15 +76,15 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
setIsApplying(false);
if (validTitle) {
dashboard.dispatch.setStateFromSettingsFlyout({ lastSavedId, ...dashboardSettingsState });
dashboardApi.setSettings(localSettings);
onClose();
}
};
const updateDashboardSetting = useCallback((newSettings: Partial<DashboardContainerInput>) => {
setDashboardSettingsState((prevDashboardSettingsState) => {
setLocalSettings((prevSettings) => {
return {
...prevDashboardSettingsState,
...prevSettings,
...newSettings,
};
});
@ -117,7 +112,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
id="dashboard.embeddableApi.showSettings.flyout.form.duplicateTitleDescription"
defaultMessage="Saving ''{title}'' creates a duplicate title."
values={{
title: dashboardSettingsState.title,
title: localSettings.title,
}}
/>
</p>
@ -137,7 +132,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
}
>
<components.TagSelector
selected={dashboardSettingsState.tags}
selected={localSettings.tags}
onTagsSelected={(selectedTags) => updateDashboardSetting({ tags: selectedTags })}
/>
</EuiFormRow>
@ -173,7 +168,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
data-test-subj="dashboardTitleInput"
name="title"
type="text"
value={dashboardSettingsState.title}
value={localSettings.title}
onChange={(event) => {
setIsTitleDuplicate(false);
setIsTitleDuplicateConfirmed(false);
@ -201,7 +196,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
className="dashboardDescriptionInputText"
data-test-subj="dashboardDescriptionInput"
name="description"
value={dashboardSettingsState.description ?? ''}
value={localSettings.description ?? ''}
onChange={(event) => updateDashboardSetting({ description: event.target.value })}
aria-label={i18n.translate(
'dashboard.embeddableApi.showSettings.flyout.form.panelDescriptionAriaLabel',
@ -222,7 +217,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
>
<EuiSwitch
data-test-subj="storeTimeWithDashboard"
checked={dashboardSettingsState.timeRestore}
checked={localSettings.timeRestore}
onChange={(event) => updateDashboardSetting({ timeRestore: event.target.checked })}
label={
<FormattedMessage
@ -240,7 +235,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
defaultMessage: 'Use margins between panels',
}
)}
checked={dashboardSettingsState.useMargins}
checked={localSettings.useMargins}
onChange={(event) => updateDashboardSetting({ useMargins: event.target.checked })}
data-test-subj="dashboardMarginsCheckbox"
/>
@ -254,7 +249,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
defaultMessage: 'Show panel titles',
}
)}
checked={!dashboardSettingsState.hidePanelTitles}
checked={!localSettings.hidePanelTitles}
onChange={(event) =>
updateDashboardSetting({ hidePanelTitles: !event.target.checked })
}
@ -313,7 +308,7 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
/>
</EuiText>
}
checked={dashboardSettingsState.syncColors}
checked={localSettings.syncColors}
onChange={(event) => updateDashboardSetting({ syncColors: event.target.checked })}
data-test-subj="dashboardSyncColorsCheckbox"
/>
@ -326,10 +321,10 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
defaultMessage: 'Sync cursor across panels',
}
)}
checked={dashboardSettingsState.syncCursor}
checked={localSettings.syncCursor}
onChange={(event) => {
const syncCursor = event.target.checked;
if (!syncCursor && dashboardSettingsState.syncTooltips) {
if (!syncCursor && localSettings.syncTooltips) {
updateDashboardSetting({ syncCursor, syncTooltips: false });
} else {
updateDashboardSetting({ syncCursor });
@ -346,8 +341,8 @@ export const DashboardSettings = ({ onClose }: DashboardSettingsProps) => {
defaultMessage: 'Sync tooltips across panels',
}
)}
checked={dashboardSettingsState.syncTooltips}
disabled={!Boolean(dashboardSettingsState.syncCursor)}
checked={localSettings.syncTooltips}
disabled={!Boolean(localSettings.syncCursor)}
onChange={(event) =>
updateDashboardSetting({ syncTooltips: event.target.checked })
}

View file

@ -22,9 +22,9 @@ import {
ControlGroupSerializedState,
} from '@kbn/controls-plugin/public';
import { CONTROL_GROUP_TYPE } from '@kbn/controls-plugin/common';
import { useStateFromPublishingSubject } from '@kbn/presentation-publishing';
import { useBatchedPublishingSubjects } from '@kbn/presentation-publishing';
import { DashboardGrid } from '../grid';
import { useDashboardContainer } from '../../embeddable/dashboard_container';
import { useDashboardApi } from '../../../dashboard_api/use_dashboard_api';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
export const useDebouncedWidthObserver = (skipDebounce = false, wait = 100) => {
@ -42,17 +42,33 @@ export const useDebouncedWidthObserver = (skipDebounce = false, wait = 100) => {
};
export const DashboardViewportComponent = () => {
const dashboard = useDashboardContainer();
const controlGroupApi = useStateFromPublishingSubject(dashboard.controlGroupApi$);
const panelCount = Object.keys(dashboard.select((state) => state.explicitInput.panels)).length;
const dashboardApi = useDashboardApi();
const [hasControls, setHasControls] = useState(false);
const viewMode = dashboard.select((state) => state.explicitInput.viewMode);
const dashboardTitle = dashboard.select((state) => state.explicitInput.title);
const useMargins = dashboard.select((state) => state.explicitInput.useMargins);
const description = dashboard.select((state) => state.explicitInput.description);
const focusedPanelId = dashboard.select((state) => state.componentState.focusedPanelId);
const expandedPanelId = dashboard.select((state) => state.componentState.expandedPanelId);
const [
controlGroupApi,
dashboardTitle,
description,
expandedPanelId,
focusedPanelId,
panels,
viewMode,
useMargins,
uuid,
] = useBatchedPublishingSubjects(
dashboardApi.controlGroupApi$,
dashboardApi.panelTitle,
dashboardApi.panelDescription,
dashboardApi.expandedPanelId,
dashboardApi.focusedPanelId$,
dashboardApi.panels$,
dashboardApi.viewMode,
dashboardApi.useMargins$,
dashboardApi.uuid$
);
const panelCount = useMemo(() => {
return Object.keys(panels).length;
}, [panels]);
const { ref: resizeRef, width: viewportWidth } = useDebouncedWidthObserver(!!focusedPanelId);
@ -104,19 +120,19 @@ export const DashboardViewportComponent = () => {
ControlGroupRuntimeState,
ControlGroupApi
>
key={dashboard.getInput().id}
key={uuid}
hidePanelChrome={true}
panelProps={{ hideLoader: true }}
type={CONTROL_GROUP_TYPE}
maybeId={'control_group'}
getParentApi={() => {
return {
...dashboard,
getSerializedStateForChild: dashboard.getSerializedStateForControlGroup,
getRuntimeStateForChild: dashboard.getRuntimeStateForControlGroup,
...dashboardApi,
getSerializedStateForChild: dashboardApi.getSerializedStateForControlGroup,
getRuntimeStateForChild: dashboardApi.getRuntimeStateForControlGroup,
};
}}
onApiAvailable={(api) => dashboard.setControlGroupApi(api)}
onApiAvailable={(api) => dashboardApi.setControlGroupApi(api)}
/>
</div>
) : null}
@ -143,11 +159,11 @@ export const DashboardViewportComponent = () => {
// because ExitFullScreenButton sets isFullscreenMode to false on unmount while rerendering.
// This specifically fixed maximizing/minimizing panels without exiting fullscreen mode.
const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
const dashboard = useDashboardContainer();
const dashboardApi = useDashboardApi();
const isFullScreenMode = dashboard.select((state) => state.componentState.fullScreenMode);
const isEmbeddedExternally = dashboard.select(
(state) => state.componentState.isEmbeddedExternally
const [isFullScreenMode, isEmbeddedExternally] = useBatchedPublishingSubjects(
dashboardApi.fullScreenMode$,
dashboardApi.embeddedExternally$
);
return (
@ -156,7 +172,7 @@ const WithFullScreenButton = ({ children }: { children: JSX.Element }) => {
{isFullScreenMode && (
<EuiPortal>
<ExitFullScreenButton
onExit={() => dashboard.dispatch.setFullScreenMode(false)}
onExit={() => dashboardApi.setFullScreenMode(false)}
toggleChrome={!isEmbeddedExternally}
/>
</EuiPortal>

View file

@ -7,7 +7,7 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
export { showSettings } from './show_settings';
export { openSettingsFlyout } from './open_settings_flyout';
export { addFromLibrary } from './add_panel_from_library';
export { addOrUpdateEmbeddable } from './panel_management';
export { runQuickSave, runInteractiveSave } from './run_save_functions';

View file

@ -13,37 +13,33 @@ import { toMountPoint } from '@kbn/react-kibana-mount';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardSettings } from '../../component/settings/settings_flyout';
import { DashboardContainer, DashboardContainerContext } from '../dashboard_container';
import { DashboardContext } from '../../../dashboard_api/use_dashboard_api';
import { DashboardApi } from '../../../dashboard_api/types';
export function showSettings(this: DashboardContainer) {
export function openSettingsFlyout(dashboardApi: DashboardApi) {
const {
analytics,
settings: { i18n, theme },
overlays,
} = pluginServices.getServices();
// TODO Move this action into DashboardContainer.openOverlay
this.dispatch.setHasOverlays(true);
this.openOverlay(
dashboardApi.openOverlay(
overlays.openFlyout(
toMountPoint(
<DashboardContainerContext.Provider value={this}>
<DashboardContext.Provider value={dashboardApi}>
<DashboardSettings
onClose={() => {
this.dispatch.setHasOverlays(false);
this.clearOverlays();
dashboardApi.clearOverlays();
}}
/>
</DashboardContainerContext.Provider>,
</DashboardContext.Provider>,
{ analytics, i18n, theme }
),
{
size: 's',
'data-test-subj': 'dashboardSettingsFlyout',
onClose: (flyout) => {
this.clearOverlays();
this.dispatch.setHasOverlays(false);
dashboardApi.clearOverlays();
flyout.close();
},
}

View file

@ -50,7 +50,7 @@ import { LocatorPublic } from '@kbn/share-plugin/common';
import { ExitFullScreenButtonKibanaProvider } from '@kbn/shared-ux-button-exit-full-screen';
import deepEqual from 'fast-deep-equal';
import { omit } from 'lodash';
import React, { createContext, useContext } from 'react';
import React from 'react';
import ReactDOM from 'react-dom';
import { batch } from 'react-redux';
import { BehaviorSubject, Subject, Subscription, first, skipWhile, switchMap } from 'rxjs';
@ -59,8 +59,13 @@ import { v4 } from 'uuid';
import { PublishesSettings } from '@kbn/presentation-containers/interfaces/publishes_settings';
import { apiHasSerializableState } from '@kbn/presentation-containers/interfaces/serialized_state';
import { ControlGroupApi, ControlGroupSerializedState } from '@kbn/controls-plugin/public';
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE } from '../..';
import { DashboardAttributes, DashboardContainerInput, DashboardPanelState } from '../../../common';
import { DashboardLocatorParams, DASHBOARD_CONTAINER_TYPE, DashboardApi } from '../..';
import {
DashboardAttributes,
DashboardContainerInput,
DashboardPanelMap,
DashboardPanelState,
} from '../../../common';
import {
getReferencesForControls,
getReferencesForPanelId,
@ -81,14 +86,13 @@ import { DashboardViewport } from '../component/viewport/dashboard_viewport';
import { getDashboardPanelPlacementSetting } from '../panel_placement/panel_placement_registry';
import { dashboardContainerReducers } from '../state/dashboard_container_reducers';
import { getDiffingMiddleware } from '../state/diffing/dashboard_diffing_integration';
import { DashboardPublicState, DashboardReduxState, UnsavedPanelState } from '../types';
import {
addFromLibrary,
addOrUpdateEmbeddable,
runQuickSave,
runInteractiveSave,
showSettings,
} from './api';
DashboardPublicState,
DashboardReduxState,
DashboardStateFromSettingsFlyout,
UnsavedPanelState,
} from '../types';
import { addFromLibrary, addOrUpdateEmbeddable, runQuickSave, runInteractiveSave } from './api';
import { duplicateDashboardPanel } from './api/duplicate_dashboard_panel';
import {
combineDashboardFiltersWithControlGroupFilters,
@ -102,6 +106,7 @@ import {
} from './dashboard_container_factory';
import { getPanelAddedSuccessString } from '../../dashboard_app/_dashboard_app_strings';
import { PANELS_CONTROL_GROUP_KEY } from '../../services/dashboard_backup/dashboard_backup_service';
import { DashboardContext } from '../../dashboard_api/use_dashboard_api';
export interface InheritedChildInput {
filters: Filter[];
@ -124,15 +129,6 @@ type DashboardReduxEmbeddableTools = ReduxEmbeddableTools<
typeof dashboardContainerReducers
>;
export const DashboardContainerContext = createContext<DashboardContainer | null>(null);
export const useDashboardContainer = (): DashboardContainer => {
const dashboard = useContext<DashboardContainer | null>(DashboardContainerContext);
if (dashboard == null) {
throw new Error('useDashboardContainer must be used inside DashboardContainerContext.');
}
return dashboard!;
};
export class DashboardContainer
extends Container<InheritedChildInput, DashboardContainerInput>
implements
@ -297,6 +293,12 @@ export class DashboardContainer
this.dispatch = reduxTools.dispatch;
this.select = reduxTools.select;
this.uuid$ = embeddableInputToSubject<string>(
this.publishingSubscription,
this,
'id'
) as BehaviorSubject<string>;
this.savedObjectId = new BehaviorSubject(this.getDashboardSavedObjectId());
this.expandedPanelId = new BehaviorSubject(this.getExpandedPanelId());
this.focusedPanelId$ = new BehaviorSubject(this.getState().componentState.focusedPanelId);
@ -307,6 +309,16 @@ export class DashboardContainer
);
this.hasUnsavedChanges$ = new BehaviorSubject(this.getState().componentState.hasUnsavedChanges);
this.hasOverlays$ = new BehaviorSubject(this.getState().componentState.hasOverlays);
this.useMargins$ = new BehaviorSubject(this.getState().explicitInput.useMargins);
this.scrollToPanelId$ = new BehaviorSubject(this.getState().componentState.scrollToPanelId);
this.highlightPanelId$ = new BehaviorSubject(this.getState().componentState.highlightPanelId);
this.animatePanelTransforms$ = new BehaviorSubject(
this.getState().componentState.animatePanelTransforms
);
this.panels$ = new BehaviorSubject(this.getState().explicitInput.panels);
this.embeddedExternally$ = new BehaviorSubject(
this.getState().componentState.isEmbeddedExternally
);
this.publishingSubscription.add(
this.onStateChange(() => {
const state = this.getState();
@ -334,6 +346,24 @@ export class DashboardContainer
if (this.hasOverlays$.value !== state.componentState.hasOverlays) {
this.hasOverlays$.next(state.componentState.hasOverlays);
}
if (this.useMargins$.value !== state.explicitInput.useMargins) {
this.useMargins$.next(state.explicitInput.useMargins);
}
if (this.scrollToPanelId$.value !== state.componentState.scrollToPanelId) {
this.scrollToPanelId$.next(state.componentState.scrollToPanelId);
}
if (this.highlightPanelId$.value !== state.componentState.highlightPanelId) {
this.highlightPanelId$.next(state.componentState.highlightPanelId);
}
if (this.animatePanelTransforms$.value !== state.componentState.animatePanelTransforms) {
this.animatePanelTransforms$.next(state.componentState.animatePanelTransforms);
}
if (this.embeddedExternally$.value !== state.componentState.isEmbeddedExternally) {
this.embeddedExternally$.next(state.componentState.isEmbeddedExternally);
}
if (this.panels$.value !== state.explicitInput.panels) {
this.panels$.next(state.explicitInput.panels);
}
})
);
@ -452,9 +482,9 @@ export class DashboardContainer
<ExitFullScreenButtonKibanaProvider
coreStart={{ chrome: this.chrome, customBranding: this.customBranding }}
>
<DashboardContainerContext.Provider value={this}>
<DashboardContext.Provider value={this as DashboardApi}>
<DashboardViewport />
</DashboardContainerContext.Provider>
</DashboardContext.Provider>
</ExitFullScreenButtonKibanaProvider>
</KibanaRenderContextProvider>,
dom
@ -535,7 +565,6 @@ export class DashboardContainer
public runInteractiveSave = runInteractiveSave;
public runQuickSave = runQuickSave;
public openSettingsFlyout = showSettings;
public addFromLibrary = addFromLibrary;
public duplicatePanel(id: string) {
@ -555,6 +584,13 @@ export class DashboardContainer
public hasRunMigrations$: BehaviorSubject<boolean | undefined>;
public hasUnsavedChanges$: BehaviorSubject<boolean | undefined>;
public hasOverlays$: BehaviorSubject<boolean | undefined>;
public useMargins$: BehaviorSubject<boolean>;
public scrollToPanelId$: BehaviorSubject<string | undefined>;
public highlightPanelId$: BehaviorSubject<string | undefined>;
public animatePanelTransforms$: BehaviorSubject<boolean | undefined>;
public panels$: BehaviorSubject<DashboardPanelMap>;
public embeddedExternally$: BehaviorSubject<boolean | undefined>;
public uuid$: BehaviorSubject<string>;
public async replacePanel(idToRemove: string, { panelType, initialState }: PanelPackage) {
const newId = await this.replaceEmbeddable(
@ -812,6 +848,26 @@ export class DashboardContainer
return this.getState().explicitInput.panels;
};
public getSettings = (): DashboardStateFromSettingsFlyout => {
const state = this.getState();
return {
description: state.explicitInput.description,
hidePanelTitles: state.explicitInput.hidePanelTitles,
lastSavedId: state.componentState.lastSavedId,
syncColors: state.explicitInput.syncColors,
syncCursor: state.explicitInput.syncCursor,
syncTooltips: state.explicitInput.syncTooltips,
tags: state.explicitInput.tags,
timeRestore: state.explicitInput.timeRestore,
title: state.explicitInput.title,
useMargins: state.explicitInput.useMargins,
};
};
public setSettings = (settings: DashboardStateFromSettingsFlyout) => {
this.dispatch.setStateFromSettingsFlyout(settings);
};
public setExpandedPanelId = (newId?: string) => {
this.dispatch.setExpandedPanelId(newId);
};
@ -925,6 +981,10 @@ export class DashboardContainer
this.setScrollToPanelId(id);
};
public setPanels = (panels: DashboardPanelMap) => {
this.dispatch.setPanels(panels);
};
// ------------------------------------------------------------------------------------------------------
// React Embeddable system
// ------------------------------------------------------------------------------------------------------

View file

@ -48,6 +48,7 @@ import './_dashboard_top_nav.scss';
import { DashboardRedirect } from '../dashboard_container/types';
import { SaveDashboardReturn } from '../services/dashboard_content_management/types';
import { useDashboardApi } from '../dashboard_api/use_dashboard_api';
import { openSettingsFlyout } from '../dashboard_container/embeddable/api';
export interface InternalDashboardTopNavProps {
customLeadingBreadCrumbs?: EuiBreadcrumb[];
@ -188,7 +189,7 @@ export function InternalDashboardTopNav({
size="s"
type="pencil"
className="dshTitleBreadcrumbs__updateIcon"
onClick={() => dashboardApi.openSettingsFlyout()}
onClick={() => openSettingsFlyout(dashboardApi)}
/>
</>
) : (