Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[dashboard] Migrate Dashboard internal components to DashboardApi #193220

Merged
merged 13 commits into from
Sep 23, 2024
33 changes: 30 additions & 3 deletions src/plugins/dashboard/public/dashboard_api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,55 +9,82 @@

import {
CanExpandPanels,
HasRuntimeChildState,
HasSerializedChildState,
PresentationContainer,
SerializedPanelState,
TracksOverlays,
} from '@kbn/presentation-containers';
import {
HasAppContext,
HasType,
HasUniqueId,
PublishesDataViews,
PublishesPanelDescription,
PublishesPanelTitle,
PublishesSavedObjectId,
PublishesUnifiedSearch,
PublishesViewMode,
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'> &
HasUniqueId &
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 remove types below this line - from legacy embeddable system
untilEmbeddableLoaded: (id: string) => Promise<IEmbeddable | ErrorEmbeddable>;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -84,7 +85,7 @@ export const useDashboardMenuItems = ({
anchorElement,
savedObjectId: lastSavedId,
isDirty: Boolean(hasUnsavedChanges),
getPanelsState: dashboardApi.getPanelsState,
getPanelsState: () => dashboardApi.panels$.value,
});
},
[dashboardTitle, hasUnsavedChanges, lastSavedId, dashboardApi]
Expand Down Expand Up @@ -227,7 +228,7 @@ export const useDashboardMenuItems = ({
id: 'settings',
testId: 'dashboardSettingsButton',
disableButton: disableTopNav,
run: () => dashboardApi.openSettingsFlyout(),
run: () => openSettingsFlyout(dashboardApi),
},
};
}, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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>
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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]);
Comment on lines +55 to +61
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice 👍


const goToLens = useCallback(() => {
if (!lensAlias || !lensAlias.alias) return;
Expand Down Expand Up @@ -128,7 +135,7 @@ export function DashboardEmptyScreen() {
<EuiButtonEmpty
flush="left"
iconType="folderOpen"
onClick={() => dashboardContainer.addFromLibrary()}
onClick={() => dashboardApi.addFromLibrary()}
>
{emptyScreenStrings.getAddFromLibraryButtonTitle()}
</EuiButtonEmpty>
Expand All @@ -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>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -45,67 +47,71 @@ 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);

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);

Expand All @@ -114,16 +120,18 @@ 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);

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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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({
Expand All @@ -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) {
Expand Down
Loading