[Dashboard] Fix positioning of sticky top nav (#153610)

Closes https://github.com/elastic/kibana/issues/153582

## Summary

This PR adjusts the styling of the floating navigation bar in Dashboard
to make it **sticky** rather than using fixed positioning - this makes
it so that banners and/or callouts at the top of Kibana no longer cause
the nav bar to behave unexpectedly.


**Before**



https://user-images.githubusercontent.com/8698078/227572446-08963382-e16e-42ac-a83a-348da6b18b93.mov

<br>

**After**


https://user-images.githubusercontent.com/8698078/227573218-03c06ff5-be9a-47ea-a198-364fa89da65b.mov

<br>

While working on this, https://github.com/elastic/kibana/pull/153693 was
opened, which had the potential to cause a lot of conflicts - to resolve
this, I worked with @ThomThomson to merge his changes into this PR as
well. However, due to my lack of a separate graphics card (I have an M1
chip, which has an integrated GPU), the performance improvements that
were noted on his original PR were not nearly as dramatic on my machine.

Specifically, after some research, we found that CSS transforms are
calculated on the graphics card - so, by switching to using CSS
transforms for React Grid Layout, any machines with a graphics card
should see a large performance improvement - however, since I don't have
one, the performance before and after these changes was more-or-less the
same (only `~50ms` were saved in the "performance improved" version).


### Checklist

- [x] Any UI touched in this PR is usable by keyboard only (learn more
about [keyboard accessibility](https://webaim.org/techniques/keyboard/))
- [x] Any UI touched in this PR does not create any new axe failures
(run axe in browser:
[FF](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/),
[Chrome](https://chrome.google.com/webstore/detail/axe-web-accessibility-tes/lhdoppojpmngadmnindnejefpokejbdd?hl=en-US))
- [x] This renders correctly on smaller devices using a responsive
layout. (You can test this [in your
browser](https://www.browserstack.com/guide/responsive-testing-on-local-server))
- [x] This was checked for [cross-browser
compatibility](https://www.elastic.co/support/matrix#matrix_browsers)

### For maintainers

- [ ] This was checked for breaking API changes and was [labeled
appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process)
This commit is contained in:
Hannah Mudge 2023-03-29 14:29:26 -06:00 committed by GitHub
parent 90b0c39c7c
commit cd68d39377
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 264 additions and 281 deletions

View file

@ -878,7 +878,6 @@
"react-router-config": "^5.1.1",
"react-router-dom": "^5.2.0",
"react-shortcuts": "^2.1.0",
"react-sizeme": "^3.0.2",
"react-syntax-highlighter": "^15.3.1",
"react-tiny-virtual-list": "^2.2.0",
"react-use": "^15.3.8",

View file

@ -1,13 +1,6 @@
@import 'src/core/public/mixins';
.dshAppWrapper {
@include kibanaFullBodyHeight();
display: flex;
flex-direction: column;
}
.dashboardViewportWrapper {
display: flex;
flex: 1;
flex-direction: column;

View file

@ -14,7 +14,6 @@ import { ViewMode } from '@kbn/embeddable-plugin/public';
import { useExecutionContext } from '@kbn/kibana-react-plugin/public';
import { createKbnUrlStateStorage, withNotifyOnErrors } from '@kbn/kibana-utils-plugin/public';
import { css } from '@emotion/react';
import {
DashboardAppNoDataPage,
isDashboardAppInNoDataState,
@ -54,11 +53,6 @@ export function DashboardApp({
history,
}: DashboardAppProps) {
const [showNoDataPage, setShowNoDataPage] = useState<boolean>(false);
/**
* This state keeps track of the height of the top navigation bar so that padding at the
* top of the viewport can be adjusted dynamically.
*/
const [topNavHeight, setTopNavHeight] = useState(0);
useMount(() => {
(async () => setShowNoDataPage(await isDashboardAppInNoDataState()))();
@ -191,7 +185,7 @@ export function DashboardApp({
}, [dashboardContainer, kbnUrlStateStorage]);
return (
<div className={'dshAppWrapper'}>
<div className="dshAppWrapper">
{showNoDataPage && (
<DashboardAppNoDataPage onDataViewCreated={() => setShowNoDataPage(false)} />
)}
@ -199,27 +193,17 @@ export function DashboardApp({
<>
{DashboardReduxWrapper && (
<DashboardReduxWrapper>
<DashboardTopNav
onHeightChange={setTopNavHeight}
redirectTo={redirectTo}
embedSettings={embedSettings}
/>
<DashboardTopNav redirectTo={redirectTo} embedSettings={embedSettings} />
</DashboardReduxWrapper>
)}
{getLegacyConflictWarning?.()}
<div
className="dashboardViewportWrapper"
css={css`
padding-top: ${topNavHeight}px;
`}
>
<DashboardContainerRenderer
savedObjectId={savedDashboardId}
getCreationOptions={getCreationOptions}
onDashboardContainerLoaded={(container) => setDashboardContainer(container)}
/>
</div>
<DashboardContainerRenderer
savedObjectId={savedDashboardId}
getCreationOptions={getCreationOptions}
onDashboardContainerLoaded={(container) => setDashboardContainer(container)}
/>
</>
)}
</div>

View file

@ -1,6 +1,11 @@
.dashboardTopNav {
position: fixed;
width: 100%;
position: sticky;
z-index: $euiZLevel2;
background: $euiPageBackgroundColor;
width: 100%;
}
top: $kbnHeaderOffset;
&.dashboardTopNav-fullscreenMode {
top: 0;
}
}

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import classNames from 'classnames';
import UseUnmount from 'react-use/lib/useUnmount';
import React, { useEffect, useMemo, useRef, useState } from 'react';
@ -17,7 +18,7 @@ import {
import { ViewMode } from '@kbn/embeddable-plugin/public';
import type { DataView } from '@kbn/data-views-plugin/public';
import { EuiHorizontalRule, useResizeObserver } from '@elastic/eui';
import { EuiHorizontalRule } from '@elastic/eui';
import {
getDashboardTitle,
leaveConfirmStrings,
@ -37,16 +38,11 @@ import './_dashboard_top_nav.scss';
export interface DashboardTopNavProps {
embedSettings?: DashboardEmbedSettings;
redirectTo: DashboardRedirect;
onHeightChange: (height: number) => void;
}
const LabsFlyout = withSuspense(LazyLabsFlyout, null);
export function DashboardTopNav({
embedSettings,
redirectTo,
onHeightChange,
}: DashboardTopNavProps) {
export function DashboardTopNav({ embedSettings, redirectTo }: DashboardTopNavProps) {
const [isChromeVisible, setIsChromeVisible] = useState(false);
const [isLabsShown, setIsLabsShown] = useState(false);
@ -122,16 +118,6 @@ export function DashboardTopNav({
if (!embedSettings) setChromeVisibility(viewMode !== ViewMode.PRINT);
}, [embedSettings, setChromeVisibility, viewMode]);
/**
* Keep track of the height of the top nav bar as it changes so that the padding at the top of the
* dashboard viewport can be adjusted dynamically as it changes
*/
const resizeRef = useRef<HTMLDivElement>(null);
const dimensions = useResizeObserver(resizeRef.current);
useEffect(() => {
onHeightChange(dimensions.height);
}, [dimensions, onHeightChange]);
/**
* populate recently accessed, and set is chrome visible.
*/
@ -230,7 +216,11 @@ export function DashboardTopNav({
});
return (
<div ref={resizeRef} className={'dashboardTopNav'}>
<div
className={classNames('dashboardTopNav', {
'dashboardTopNav-fullscreenMode': fullScreenMode,
})}
>
<h1
id="dashboardTitle"
className="euiScreenReaderOnly"

View file

@ -59,6 +59,7 @@ export const DASHBOARD_SAVED_OBJECT_TYPE = 'dashboard';
// Grid
// ------------------------------------------------------------------
export const DEFAULT_PANEL_HEIGHT = 15;
export const DASHBOARD_MARGIN_SIZE = 8;
export const DASHBOARD_GRID_HEIGHT = 20;
export const DASHBOARD_GRID_COLUMN_COUNT = 48;
export const DEFAULT_PANEL_WIDTH = DASHBOARD_GRID_COLUMN_COUNT / 2;

View file

@ -5,9 +5,8 @@
@import './component/viewport/index';
.dashboardContainer, .dashboardViewport {
flex: auto;
display: flex;
flex: 1;
flex-direction: column;
}
.dashboardViewport--loading {

View file

@ -35,10 +35,6 @@
height: 100% !important; /* 1. */
}
/**
* .dshLayout-withoutMargins only affects the panel styles themselves, see ../panel
*/
/**
* When a single panel is expanded, all the other panels are hidden in the grid.
*/
@ -46,6 +42,13 @@
display: none;
}
/**
* turn off panel transforms initially so that the dashboard panels don't swoop in on first load.
*/
.dshLayout--noAnimation .react-grid-item.cssTransforms {
transition-property: none !important;
}
/**
* 1. We need to mark this as important because react grid layout sets the width and height of the panels inline.
*/
@ -54,6 +57,7 @@
width: 100% !important; /* 1 */
top: 0 !important; /* 1 */
left: 0 !important; /* 1 */
transform: translate(0, 0) !important; /* 1 */
padding: $euiSizeS;
// Altered panel styles can be found in ../panel
@ -69,10 +73,6 @@
// REACT-GRID
.react-grid-item {
/**
* Disable transitions from the library on each grid element.
*/
transition: none;
/**
* Copy over and overwrite the fill color with EUI color mixin (for theming)
*/
@ -112,22 +112,3 @@
background: $euiColorWarning;
}
}
// When in view-mode only, and on tiny mobile screens, just stack each of the grid-items
@include euiBreakpoint('xs', 's') {
.dshLayout--viewing {
.react-grid-item {
position: static !important;
width: calc(100% - #{$euiSize}) !important;
margin: $euiSizeS;
}
&.dshLayout-withoutMargins {
.react-grid-item {
width: 100% !important;
margin: 0;
}
}
}
}

View file

@ -7,7 +7,6 @@
*/
// @ts-ignore
import sizeMe from 'react-sizeme';
import React from 'react';
import { CONTACT_CARD_EMBEDDABLE } from '@kbn/embeddable-plugin/public/lib/test_samples/embeddables';
@ -62,16 +61,6 @@ async function getDashboardContainer() {
return dashboardContainer;
}
beforeAll(() => {
// sizeme detects the width to be 0 in our test environment. noPlaceholder will mean that the grid contents will
// get rendered even when width is 0, which will improve our tests.
sizeMe.noPlaceholders = true;
});
afterAll(() => {
sizeMe.noPlaceholders = false;
});
test('renders DashboardGrid', async () => {
const dashboardContainer = await getDashboardContainer();
const { Wrapper: DashboardReduxWrapper } = dashboardContainer.getReduxEmbeddableTools();
@ -79,7 +68,7 @@ test('renders DashboardGrid', async () => {
const component = mountWithIntl(
<DashboardServicesProvider>
<DashboardReduxWrapper>
<DashboardGrid />
<DashboardGrid viewportWidth={1000} />
</DashboardReduxWrapper>
</DashboardServicesProvider>
);
@ -94,7 +83,7 @@ test('renders DashboardGrid with no visualizations', async () => {
const component = mountWithIntl(
<DashboardServicesProvider>
<DashboardReduxWrapper>
<DashboardGrid />
<DashboardGrid viewportWidth={1000} />
</DashboardReduxWrapper>
</DashboardServicesProvider>
);
@ -111,7 +100,7 @@ test('DashboardGrid removes panel when removed from container', async () => {
const component = mountWithIntl(
<DashboardServicesProvider>
<DashboardReduxWrapper>
<DashboardGrid />
<DashboardGrid viewportWidth={1000} />
</DashboardReduxWrapper>
</DashboardServicesProvider>
);
@ -132,7 +121,7 @@ test('DashboardGrid renders expanded panel', async () => {
const component = mountWithIntl(
<DashboardServicesProvider>
<DashboardReduxWrapper>
<DashboardGrid />
<DashboardGrid viewportWidth={1000} />
</DashboardReduxWrapper>
</DashboardServicesProvider>
);

View file

@ -6,178 +6,92 @@
* Side Public License, v 1.
*/
import _ from 'lodash';
import sizeMe from 'react-sizeme';
import classNames from 'classnames';
import 'react-resizable/css/styles.css';
import 'react-grid-layout/css/styles.css';
import React, { useCallback, useMemo, useRef } from 'react';
import ReactGridLayout, { Layout, ReactGridLayoutProps } from 'react-grid-layout';
import { ViewMode, EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
import { pick } from 'lodash';
import classNames from 'classnames';
import { useEffectOnce } from 'react-use/lib';
import React, { useState, useMemo, useCallback } from 'react';
import { Layout, Responsive as ResponsiveReactGridLayout } from 'react-grid-layout';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DashboardPanelState } from '../../../../common';
import { DashboardGridItem } from './dashboard_grid_item';
import { DASHBOARD_GRID_HEIGHT, DASHBOARD_MARGIN_SIZE } from '../../../dashboard_constants';
import { useDashboardGridSettings } from './use_dashboard_grid_settings';
import { useDashboardContainerContext } from '../../dashboard_container_context';
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
import { DASHBOARD_GRID_COLUMN_COUNT, DASHBOARD_GRID_HEIGHT } from '../../../dashboard_constants';
import { useDashboardPerformanceTracker } from './use_dashboard_performance_tracker';
import { getPanelLayoutsAreEqual } from '../../embeddable/integrations/diff_state/dashboard_diffing_utils';
let lastValidGridSize = 0;
/**
* This is a fix for a bug that stopped the browser window from automatically scrolling down when panels were made
* taller than the current grid.
* see https://github.com/elastic/kibana/issues/14710.
*/
function ensureWindowScrollsToBottom(event: { clientY: number; pageY: number }) {
// The buffer is to handle the case where the browser is maximized and it's impossible for the mouse to move below
// the screen, out of the window. see https://github.com/elastic/kibana/issues/14737
const WINDOW_BUFFER = 10;
if (event.clientY > window.innerHeight - WINDOW_BUFFER) {
window.scrollTo(0, event.pageY + WINDOW_BUFFER - window.innerHeight);
}
}
function ResponsiveGrid({
size,
isViewMode,
layout,
onLayoutChange,
children,
maximizedPanelId,
useMargins,
}: {
size: { width: number };
isViewMode: boolean;
layout: Layout[];
onLayoutChange: ReactGridLayoutProps['onLayoutChange'];
children: JSX.Element[];
maximizedPanelId?: string;
useMargins: boolean;
}) {
// This is to prevent a bug where view mode changes when the panel is expanded. View mode changes will trigger
// the grid to re-render, but when a panel is expanded, the size will be 0. Minimizing the panel won't cause the
// grid to re-render so it'll show a grid with a width of 0.
lastValidGridSize = size.width > 0 ? size.width : lastValidGridSize;
const classes = classNames({
'dshLayout--viewing': isViewMode,
'dshLayout--editing': !isViewMode,
'dshLayout-isMaximizedPanel': maximizedPanelId !== undefined,
'dshLayout-withoutMargins': !useMargins,
});
const MARGINS = useMargins ? 8 : 0;
return (
<ReactGridLayout
width={lastValidGridSize}
className={classes}
isDraggable={!maximizedPanelId}
isResizable={!maximizedPanelId}
// There is a bug with d3 + firefox + elements using transforms.
// See https://github.com/elastic/kibana/issues/16870 for more context.
useCSSTransforms={false}
margin={[MARGINS, MARGINS]}
cols={DASHBOARD_GRID_COLUMN_COUNT}
rowHeight={DASHBOARD_GRID_HEIGHT}
// Pass the named classes of what should get the dragging handle
draggableHandle={'.embPanel--dragHandle'}
layout={layout}
onLayoutChange={onLayoutChange}
onResize={({}, {}, {}, {}, event) => ensureWindowScrollsToBottom(event)}
>
{children}
</ReactGridLayout>
);
}
// Using sizeMe sets up the grid to be re-rendered automatically not only when the window size changes, but also
// when the container size changes, so it works for Full Screen mode switches.
const config = { monitorWidth: true };
const ResponsiveSizedGrid = sizeMe(config)(ResponsiveGrid);
interface PanelLayout extends Layout {
i: string;
}
type DashboardRenderPerformanceTracker = DashboardRenderPerformanceStats & {
panelIds: Record<string, Record<string, number>>;
status: DashboardLoadedEventStatus;
doneCount: number;
};
const getDefaultPerformanceTracker: () => DashboardRenderPerformanceTracker = () => ({
panelsRenderStartTime: performance.now(),
panelsRenderDoneTime: 0,
lastTimeToData: 0,
panelIds: {},
doneCount: 0,
status: 'done',
});
export const DashboardGrid = () => {
export const DashboardGrid = ({ viewportWidth }: { viewportWidth: number }) => {
const {
useEmbeddableSelector: select,
actions: { setPanels },
useEmbeddableDispatch,
useEmbeddableSelector: select,
embeddableInstance: dashboardContainer,
} = useDashboardContainerContext();
const dispatch = useEmbeddableDispatch();
const panels = select((state) => state.explicitInput.panels);
const viewMode = select((state) => state.explicitInput.viewMode);
const useMargins = select((state) => state.explicitInput.useMargins);
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
const layout = useMemo(() => Object.values(panels).map((panel) => panel.gridData), [panels]);
const panelsInOrder = useMemo(
() => Object.keys(panels).map((key: string) => panels[key]),
[panels]
);
// turn off panel transform animations for the first 500ms so that the dashboard doesn't animate on its first render.
const [animatePanelTransforms, setAnimatePanelTransforms] = useState(false);
useEffectOnce(() => {
setTimeout(() => setAnimatePanelTransforms(true), 500);
});
// reset performance tracker on each render.
const performanceRefs = useRef<DashboardRenderPerformanceTracker>(getDefaultPerformanceTracker());
performanceRefs.current = getDefaultPerformanceTracker();
const { onPanelStatusChange } = useDashboardPerformanceTracker({
panelCount: Object.keys(panels).length,
});
const onPanelStatusChange = useCallback(
(info: EmbeddablePhaseEvent) => {
if (performanceRefs.current.panelIds[info.id] === undefined || info.status === 'loading') {
performanceRefs.current.panelIds[info.id] = {};
} else if (info.status === 'error') {
performanceRefs.current.status = 'error';
} else if (info.status === 'loaded') {
performanceRefs.current.lastTimeToData = performance.now();
const panelsInOrder: string[] = useMemo(() => {
return Object.keys(panels).sort((embeddableIdA, embeddableIdB) => {
const panelA = panels[embeddableIdA];
const panelB = panels[embeddableIdB];
// need to manually sort the panels by position because we want the panels to be collapsed from the left to the
// right when switching to the single column layout, but RGL sorts by ID which can cause unexpected behaviour between
// by-reference and by-value panels + we want the HTML order to align with this in the multi-panel view
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;
} else {
return panelA.gridData.y - panelB.gridData.y;
}
});
}, [panels]);
performanceRefs.current.panelIds[info.id][info.status] = performance.now();
if (info.status === 'error' || info.status === 'rendered') {
performanceRefs.current.doneCount++;
if (performanceRefs.current.doneCount === panelsInOrder.length) {
performanceRefs.current.panelsRenderDoneTime = performance.now();
dashboardContainer.reportPerformanceMetrics(performanceRefs.current);
}
}
},
[dashboardContainer, panelsInOrder.length]
);
const panelComponents = useMemo(() => {
return panelsInOrder.map((embeddableId, index) => {
const type = panels[embeddableId].type;
return (
<DashboardGridItem
data-grid={panels[embeddableId].gridData}
key={embeddableId}
id={embeddableId}
index={index + 1}
type={type}
expandedPanelId={expandedPanelId}
onPanelStatusChange={onPanelStatusChange}
/>
);
});
}, [expandedPanelId, onPanelStatusChange, panels, panelsInOrder]);
const onLayoutChange = useCallback(
(newLayout: PanelLayout[]) => {
(newLayout: Array<Layout & { i: string }>) => {
const updatedPanels: { [key: string]: DashboardPanelState } = newLayout.reduce(
(updatedPanelsAcc, panelLayout) => {
updatedPanelsAcc[panelLayout.i] = {
...panels[panelLayout.i],
gridData: _.pick(panelLayout, ['x', 'y', 'w', 'h', 'i']),
gridData: pick(panelLayout, ['x', 'y', 'w', 'h', 'i']),
};
return updatedPanelsAcc;
},
{} as { [key: string]: DashboardPanelState }
);
// onLayoutChange gets called by react grid layout a lot more than it should, so only dispatch the updated panels if the layout has actually changed
if (!getPanelLayoutsAreEqual(panels, updatedPanels)) {
dispatch(setPanels(updatedPanels));
}
@ -185,41 +99,37 @@ export const DashboardGrid = () => {
[dispatch, panels, setPanels]
);
const dashboardPanels = useMemo(() => {
panelsInOrder.sort((panelA, panelB) => {
if (panelA.gridData.y === panelB.gridData.y) {
return panelA.gridData.x - panelB.gridData.x;
} else {
return panelA.gridData.y - panelB.gridData.y;
}
});
const classes = classNames({
'dshLayout-withoutMargins': !useMargins,
'dshLayout--viewing': viewMode === ViewMode.VIEW,
'dshLayout--editing': viewMode !== ViewMode.VIEW,
'dshLayout--noAnimation': !animatePanelTransforms,
'dshLayout-isMaximizedPanel': expandedPanelId !== undefined,
});
return panelsInOrder.map(({ explicitInput, type }, index) => (
<DashboardGridItem
key={explicitInput.id}
id={explicitInput.id}
index={index + 1}
type={type}
expandedPanelId={expandedPanelId}
onPanelStatusChange={onPanelStatusChange}
/>
));
}, [expandedPanelId, panelsInOrder, onPanelStatusChange]);
const { layouts, breakpoints, columns } = useDashboardGridSettings(panelsInOrder);
// in print mode, dashboard layout is not controlled by React Grid Layout
if (viewMode === ViewMode.PRINT) {
return <>{dashboardPanels}</>;
return <>{panelComponents}</>;
}
return (
<ResponsiveSizedGrid
layout={layout}
useMargins={useMargins}
onLayoutChange={onLayoutChange}
maximizedPanelId={expandedPanelId}
isViewMode={viewMode === ViewMode.VIEW}
<ResponsiveReactGridLayout
cols={columns}
layouts={layouts}
className={classes}
width={viewportWidth}
breakpoints={breakpoints}
onDragStop={onLayoutChange}
onResizeStop={onLayoutChange}
isResizable={!expandedPanelId}
isDraggable={!expandedPanelId}
rowHeight={DASHBOARD_GRID_HEIGHT}
margin={useMargins ? [DASHBOARD_MARGIN_SIZE, DASHBOARD_MARGIN_SIZE] : [0, 0]}
draggableHandle={'.embPanel--dragHandle'}
>
{dashboardPanels}
</ResponsiveSizedGrid>
{panelComponents}
</ResponsiveReactGridLayout>
);
};

View file

@ -0,0 +1,44 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { useMemo } from 'react';
import { useEuiTheme } from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { DASHBOARD_GRID_COLUMN_COUNT } from '../../../dashboard_constants';
import { useDashboardContainerContext } from '../../dashboard_container_context';
export const useDashboardGridSettings = (panelsInOrder: string[]) => {
const { useEmbeddableSelector: select } = useDashboardContainerContext();
const { euiTheme } = useEuiTheme();
const panels = select((state) => state.explicitInput.panels);
const viewMode = select((state) => state.explicitInput.viewMode);
const layouts = useMemo(() => {
return {
lg: panelsInOrder.map((embeddableId) => panels[embeddableId].gridData),
};
}, [panels, panelsInOrder]);
const breakpoints = useMemo(
() => ({ lg: euiTheme.breakpoint.m, ...(viewMode === ViewMode.VIEW ? { sm: 0 } : {}) }),
[viewMode, euiTheme.breakpoint.m]
);
const columns = useMemo(
() => ({
lg: DASHBOARD_GRID_COLUMN_COUNT,
...(viewMode === ViewMode.VIEW ? { sm: 1 } : {}),
}),
[viewMode]
);
return { layouts, breakpoints, columns };
};

View file

@ -0,0 +1,63 @@
/*
* 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 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 or the Server
* Side Public License, v 1.
*/
import { useCallback, useRef } from 'react';
import { EmbeddablePhaseEvent } from '@kbn/embeddable-plugin/public';
import { useDashboardContainerContext } from '../../dashboard_container_context';
import { DashboardLoadedEventStatus, DashboardRenderPerformanceStats } from '../../types';
type DashboardRenderPerformanceTracker = DashboardRenderPerformanceStats & {
panelIds: Record<string, Record<string, number>>;
status: DashboardLoadedEventStatus;
doneCount: number;
};
const getDefaultPerformanceTracker: () => DashboardRenderPerformanceTracker = () => ({
panelsRenderStartTime: performance.now(),
panelsRenderDoneTime: 0,
lastTimeToData: 0,
panelIds: {},
doneCount: 0,
status: 'done',
});
export const useDashboardPerformanceTracker = ({ panelCount }: { panelCount: number }) => {
const { embeddableInstance: dashboardContainer } = useDashboardContainerContext();
// reset performance tracker on each render.
const performanceRefs = useRef<DashboardRenderPerformanceTracker>(getDefaultPerformanceTracker());
performanceRefs.current = getDefaultPerformanceTracker();
const onPanelStatusChange = useCallback(
(info: EmbeddablePhaseEvent) => {
if (performanceRefs.current.panelIds[info.id] === undefined || info.status === 'loading') {
performanceRefs.current.panelIds[info.id] = {};
} else if (info.status === 'error') {
performanceRefs.current.status = 'error';
} else if (info.status === 'loaded') {
performanceRefs.current.lastTimeToData = performance.now();
}
performanceRefs.current.panelIds[info.id][info.status] = performance.now();
if (info.status === 'error' || info.status === 'rendered') {
performanceRefs.current.doneCount++;
if (performanceRefs.current.doneCount === panelCount) {
performanceRefs.current.panelsRenderDoneTime = performance.now();
dashboardContainer.reportPerformanceMetrics(performanceRefs.current);
}
}
},
[dashboardContainer, panelCount]
);
return { onPanelStatusChange };
};

View file

@ -1,9 +1,15 @@
.dshDashboardViewportWrapper {
flex: auto;
display: flex;
flex-direction: column;
}
.dshDashboardViewport {
width: 100%;
}
.dshDashboardViewport-withMargins {
width: 100%;
.dshDashboardViewport--panelExpanded {
flex: 1;
}
.dshDashboardViewport-controls {

View file

@ -6,18 +6,32 @@
* Side Public License, v 1.
*/
import React, { useEffect, useRef } from 'react';
import { debounce } from 'lodash';
import classNames from 'classnames';
import useResizeObserver from 'use-resize-observer/polyfilled';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import { EuiPortal } from '@elastic/eui';
import { ViewMode } from '@kbn/embeddable-plugin/public';
import { ExitFullScreenButton } from '@kbn/shared-ux-button-exit-full-screen';
import { css } from '@emotion/react';
import { EuiPortal } from '@elastic/eui';
import { DashboardGrid } from '../grid';
import { pluginServices } from '../../../services/plugin_services';
import { DashboardEmptyScreen } from '../empty_screen/dashboard_empty_screen';
import { useDashboardContainerContext } from '../../dashboard_container_context';
export const useDebouncedWidthObserver = (wait = 250) => {
const [width, setWidth] = useState<number>(0);
const onWidthCange = useMemo(() => debounce(setWidth, wait), [wait]);
const { ref } = useResizeObserver<HTMLDivElement>({
onResize: (dimensions) => {
if (width === 0) setWidth(dimensions.width);
if (dimensions.width !== width) onWidthCange(dimensions.width);
},
});
return { ref, width };
};
export const DashboardViewportComponent = () => {
const {
settings: { isProjectEnabledInLabs },
@ -42,16 +56,19 @@ export const DashboardViewportComponent = () => {
const viewMode = select((state) => state.explicitInput.viewMode);
const dashboardTitle = select((state) => state.explicitInput.title);
const useMargins = select((state) => state.explicitInput.useMargins);
const description = select((state) => state.explicitInput.description);
const expandedPanelId = select((state) => state.componentState.expandedPanelId);
const expandedPanelStyles = css`
flex: 1;
`;
const controlsEnabled = isProjectEnabledInLabs('labs:dashboard:dashboardControls');
const { ref: resizeRef, width: viewportWidth } = useDebouncedWidthObserver();
const classes = classNames({
dshDashboardViewport: true,
'dshDashboardViewport--panelExpanded': Boolean(expandedPanelId),
});
return (
<>
<div className={'dshDashboardViewportWrapper'}>
{controlsEnabled && controlGroup && viewMode !== ViewMode.PRINT ? (
<div
className={controlCount > 0 ? 'dshDashboardViewport-controls' : ''}
@ -59,21 +76,21 @@ export const DashboardViewportComponent = () => {
/>
) : null}
<div
data-shared-items-count={panelCount}
ref={resizeRef}
className={classes}
data-shared-items-container
data-title={dashboardTitle}
data-description={description}
className={useMargins ? 'dshDashboardViewport-withMargins' : 'dshDashboardViewport'}
css={expandedPanelId ? expandedPanelStyles : undefined}
data-shared-items-count={panelCount}
>
{panelCount === 0 && (
<div className="dshDashboardEmptyScreen">
<DashboardEmptyScreen isEditMode={viewMode === ViewMode.EDIT} />
</div>
)}
<DashboardGrid />
<DashboardGrid viewportWidth={viewportWidth} />
</div>
</>
</div>
);
};

View file

@ -181,7 +181,7 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
),
skip(1) // skip first filter output because it will have been applied in initialize
)
.subscribe(() => this.updateInput({ lastReloadRequestTime: Date.now() }))
.subscribe(() => this.forceRefresh())
);
subscriptions.add(
@ -193,7 +193,9 @@ function startSyncingDashboardControlGroup(this: DashboardContainer) {
)
)
.subscribe(({ timeslice }) => {
this.updateInput({ timeslice });
if (!_.isEqual(timeslice, this.getInputAsValueType().timeslice)) {
this.updateInput({ timeslice });
}
})
);

View file

@ -24324,7 +24324,7 @@ react-shortcuts@^2.1.0:
platform "^1.3.0"
prop-types "^15.5.8"
react-sizeme@^3.0.1, react-sizeme@^3.0.2:
react-sizeme@^3.0.1:
version "3.0.2"
resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-3.0.2.tgz#4a2f167905ba8f8b8d932a9e35164e459f9020e4"
integrity sha512-xOIAOqqSSmKlKFJLO3inBQBdymzDuXx4iuwkNcJmC96jeiOg5ojByvL+g3MW9LPEsojLbC6pf68zOfobK8IPlw==