Skip to content

Commit

Permalink
[dashboard] Migrate Dashboard internal components to DashboardApi (#1…
Browse files Browse the repository at this point in the history
…93220)

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>
(cherry picked from commit 92da176)
  • Loading branch information
nreese committed Sep 23, 2024
1 parent a460623 commit 8b0fc4c
Show file tree
Hide file tree
Showing 15 changed files with 299 additions and 181 deletions.
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,
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'> &
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>;
};
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]);

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

0 comments on commit 8b0fc4c

Please sign in to comment.