mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[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:
parent
90b0c39c7c
commit
cd68d39377
16 changed files with 264 additions and 281 deletions
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
.dashboardTopNav {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
position: sticky;
|
||||
z-index: $euiZLevel2;
|
||||
background: $euiPageBackgroundColor;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
top: $kbnHeaderOffset;
|
||||
&.dashboardTopNav-fullscreenMode {
|
||||
top: 0;
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -5,9 +5,8 @@
|
|||
@import './component/viewport/index';
|
||||
|
||||
.dashboardContainer, .dashboardViewport {
|
||||
flex: auto;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.dashboardViewport--loading {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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 };
|
||||
};
|
|
@ -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 };
|
||||
};
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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==
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue