Flame Graph: Fix for dashboard scrolling (#56555)

* Flamegraph dash scrolling

* Separate scroll for top table and for flame graph

* Custom scroll behavior for explore vs vs dash etc and sticky flame graph header

* Final UI tweaks

* Update tests
This commit is contained in:
Joey Tawadrous 2022-10-20 14:20:48 +01:00 committed by GitHub
parent f184f9211c
commit 5e27a6f276
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 69 additions and 22 deletions

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { DataFrame, GrafanaTheme2 } from '@grafana/data'; import { DataFrame, GrafanaTheme2, CoreApp } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import FlameGraphContainer from '../../plugins/panel/flamegraph/components/FlameGraphContainer'; import FlameGraphContainer from '../../plugins/panel/flamegraph/components/FlameGraphContainer';
@ -15,7 +15,7 @@ export const FlameGraphExploreContainer = (props: Props) => {
return ( return (
<div className={styles.container}> <div className={styles.container}>
<FlameGraphContainer data={props.dataFrames[0]} /> <FlameGraphContainer data={props.dataFrames[0]} app={CoreApp.Explore} />
</div> </div>
); );
}; };
@ -24,7 +24,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
container: css` container: css`
background: ${theme.colors.background.primary}; background: ${theme.colors.background.primary};
display: flow-root; display: flow-root;
padding: ${theme.spacing(1)}; padding: 0 ${theme.spacing(1)} ${theme.spacing(1)} ${theme.spacing(1)};
border: 1px solid ${theme.components.panel.borderColor}; border: 1px solid ${theme.components.panel.borderColor};
border-radius: ${theme.shape.borderRadius(1)}; border-radius: ${theme.shape.borderRadius(1)};
`, `,

View File

@ -2,7 +2,7 @@ import { screen } from '@testing-library/dom';
import { render } from '@testing-library/react'; import { render } from '@testing-library/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { DataFrameView, MutableDataFrame } from '@grafana/data'; import { CoreApp, DataFrameView, MutableDataFrame } from '@grafana/data';
import { SelectedView } from '../types'; import { SelectedView } from '../types';
@ -33,6 +33,7 @@ describe('FlameGraph', () => {
return ( return (
<FlameGraph <FlameGraph
data={flameGraphData} data={flameGraphData}
app={CoreApp.Explore}
levels={levels} levels={levels}
topLevelIndex={topLevelIndex} topLevelIndex={topLevelIndex}
rangeMin={rangeMin} rangeMin={rangeMin}

View File

@ -20,7 +20,7 @@ import { css } from '@emotion/css';
import React, { useCallback, useEffect, useRef, useState } from 'react'; import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { DataFrame, FieldType } from '@grafana/data'; import { CoreApp, DataFrame, FieldType } from '@grafana/data';
import { PIXELS_PER_LEVEL } from '../../constants'; import { PIXELS_PER_LEVEL } from '../../constants';
import { TooltipData, SelectedView } from '../types'; import { TooltipData, SelectedView } from '../types';
@ -31,6 +31,8 @@ import { getBarX, getRectDimensionsForLevel, renderRect } from './rendering';
type Props = { type Props = {
data: DataFrame; data: DataFrame;
app: CoreApp;
flameGraphHeight?: number;
levels: ItemWithStart[][]; levels: ItemWithStart[][];
topLevelIndex: number; topLevelIndex: number;
rangeMin: number; rangeMin: number;
@ -45,6 +47,8 @@ type Props = {
const FlameGraph = ({ const FlameGraph = ({
data, data,
app,
flameGraphHeight,
levels, levels,
topLevelIndex, topLevelIndex,
rangeMin, rangeMin,
@ -55,7 +59,7 @@ const FlameGraph = ({
setRangeMax, setRangeMax,
selectedView, selectedView,
}: Props) => { }: Props) => {
const styles = getStyles(selectedView); const styles = getStyles(selectedView, app, flameGraphHeight);
const totalTicks = data.fields[1].values.get(0); const totalTicks = data.fields[1].values.get(0);
const valueField = const valueField =
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number); data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
@ -173,11 +177,15 @@ const FlameGraph = ({
); );
}; };
const getStyles = (selectedView: SelectedView) => ({ const getStyles = (selectedView: SelectedView, app: CoreApp, flameGraphHeight: number | undefined) => ({
graph: css` graph: css`
cursor: pointer; cursor: pointer;
float: left; float: left;
overflow: scroll;
width: ${selectedView === SelectedView.FlameGraph ? '100%' : '50%'}; width: ${selectedView === SelectedView.FlameGraph ? '100%' : '50%'};
${app !== CoreApp.Explore
? `height: calc(${flameGraphHeight}px - 44px)`
: ''}; // 44px to adjust for header pushing content down
`, `,
}); });

View File

@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React from 'react'; import React from 'react';
import { MutableDataFrame } from '@grafana/data'; import { CoreApp, MutableDataFrame } from '@grafana/data';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
@ -29,7 +29,7 @@ describe('FlameGraphContainer', () => {
}, },
}; };
return <FlameGraphContainer data={flameGraphData} />; return <FlameGraphContainer data={flameGraphData} app={CoreApp.Explore} />;
}; };
it('should render without error', async () => { it('should render without error', async () => {

View File

@ -1,9 +1,11 @@
import { css } from '@emotion/css';
import React, { useEffect, useMemo, useState } from 'react'; import React, { useEffect, useMemo, useState } from 'react';
import { useMeasure } from 'react-use'; import { useMeasure } from 'react-use';
import { DataFrame, DataFrameView } from '@grafana/data'; import { DataFrame, DataFrameView, CoreApp } from '@grafana/data';
import { useStyles2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH, PIXELS_PER_LEVEL } from '../constants';
import FlameGraph from './FlameGraph/FlameGraph'; import FlameGraph from './FlameGraph/FlameGraph';
import { Item, nestedSetToLevels } from './FlameGraph/dataTransform'; import { Item, nestedSetToLevels } from './FlameGraph/dataTransform';
@ -13,6 +15,11 @@ import { SelectedView } from './types';
type Props = { type Props = {
data: DataFrame; data: DataFrame;
app: CoreApp;
// Height for flame graph when not used in explore.
// This needs to be different to explore flame graph height as we
// use panels with user adjustable heights in dashboards etc.
flameGraphHeight?: number;
}; };
const FlameGraphContainer = (props: Props) => { const FlameGraphContainer = (props: Props) => {
@ -34,6 +41,8 @@ const FlameGraphContainer = (props: Props) => {
return nestedSetToLevels(dataView); return nestedSetToLevels(dataView);
}, [props.data]); }, [props.data]);
const styles = useStyles2(() => getStyles(props.app, PIXELS_PER_LEVEL * levels.length));
// If user resizes window with both as the selected view // If user resizes window with both as the selected view
useEffect(() => { useEffect(() => {
if ( if (
@ -46,8 +55,9 @@ const FlameGraphContainer = (props: Props) => {
}, [selectedView, setSelectedView, containerWidth]); }, [selectedView, setSelectedView, containerWidth]);
return ( return (
<div ref={sizeRef}> <div ref={sizeRef} className={styles.container}>
<FlameGraphHeader <FlameGraphHeader
app={props.app}
setTopLevelIndex={setTopLevelIndex} setTopLevelIndex={setTopLevelIndex}
setRangeMin={setRangeMin} setRangeMin={setRangeMin}
setRangeMax={setRangeMax} setRangeMax={setRangeMax}
@ -61,6 +71,7 @@ const FlameGraphContainer = (props: Props) => {
{selectedView !== SelectedView.FlameGraph && ( {selectedView !== SelectedView.FlameGraph && (
<FlameGraphTopTableContainer <FlameGraphTopTableContainer
data={props.data} data={props.data}
app={props.app}
totalLevels={levels.length} totalLevels={levels.length}
selectedView={selectedView} selectedView={selectedView}
search={search} search={search}
@ -74,6 +85,8 @@ const FlameGraphContainer = (props: Props) => {
{selectedView !== SelectedView.TopTable && ( {selectedView !== SelectedView.TopTable && (
<FlameGraph <FlameGraph
data={props.data} data={props.data}
app={props.app}
flameGraphHeight={props.flameGraphHeight}
levels={levels} levels={levels}
topLevelIndex={topLevelIndex} topLevelIndex={topLevelIndex}
rangeMin={rangeMin} rangeMin={rangeMin}
@ -89,4 +102,10 @@ const FlameGraphContainer = (props: Props) => {
); );
}; };
const getStyles = (app: CoreApp, height: number) => ({
container: css`
height: ${app === CoreApp.Explore ? height + 'px' : '100%'};
`,
});
export default FlameGraphContainer; export default FlameGraphContainer;

View File

@ -3,6 +3,8 @@ import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { CoreApp } from '@grafana/data';
import FlameGraphHeader from './FlameGraphHeader'; import FlameGraphHeader from './FlameGraphHeader';
import { SelectedView } from './types'; import { SelectedView } from './types';
@ -13,6 +15,7 @@ describe('FlameGraphHeader', () => {
return ( return (
<FlameGraphHeader <FlameGraphHeader
app={CoreApp.Explore}
search={search} search={search}
setSearch={setSearch} setSearch={setSearch}
setTopLevelIndex={jest.fn()} setTopLevelIndex={jest.fn()}

View File

@ -1,13 +1,15 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { Button, Input, useStyles, RadioButtonGroup } from '@grafana/ui'; import { GrafanaTheme2, CoreApp } from '@grafana/data';
import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants'; import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
import { SelectedView } from './types'; import { SelectedView } from './types';
type Props = { type Props = {
app: CoreApp;
search: string; search: string;
setTopLevelIndex: (level: number) => void; setTopLevelIndex: (level: number) => void;
setRangeMin: (range: number) => void; setRangeMin: (range: number) => void;
@ -19,6 +21,7 @@ type Props = {
}; };
const FlameGraphHeader = ({ const FlameGraphHeader = ({
app,
search, search,
setTopLevelIndex, setTopLevelIndex,
setRangeMin, setRangeMin,
@ -28,7 +31,7 @@ const FlameGraphHeader = ({
setSelectedView, setSelectedView,
containerWidth, containerWidth,
}: Props) => { }: Props) => {
const styles = useStyles(getStyles); const styles = useStyles2((theme) => getStyles(theme, app));
let viewOptions: Array<{ value: string; label: string; description: string }> = [ let viewOptions: Array<{ value: string; label: string; description: string }> = [
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' }, { value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
@ -83,11 +86,15 @@ const FlameGraphHeader = ({
); );
}; };
const getStyles = () => ({ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
header: css` header: css`
display: flow-root; display: flow-root;
padding: 0 0 20px 0;
width: 100%; width: 100%;
background: ${theme.colors.background.primary};
top: 0;
height: 50px;
z-index: ${theme.zIndex.navbarFixed};
${app === CoreApp.Explore ? 'position: sticky; margin-bottom: 8px; padding-top: 9px' : ''};
`, `,
inputContainer: css` inputContainer: css`
float: left; float: left;

View File

@ -203,6 +203,11 @@ const getStyles = (theme: GrafanaTheme2) => ({
& > :nth-child(3) { & > :nth-child(3) {
text-align: right; text-align: right;
} }
// needed to keep header row height fixed so header row does not resize with browser
& > :nth-child(3) {
position: relative !important;
}
`, `,
headerCell: css` headerCell: css`
background-color: ${theme.colors.background.secondary}; background-color: ${theme.colors.background.secondary};

View File

@ -1,7 +1,7 @@
import { render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import React, { useState } from 'react'; import React, { useState } from 'react';
import { DataFrameView, MutableDataFrame } from '@grafana/data'; import { CoreApp, DataFrameView, MutableDataFrame } from '@grafana/data';
import { Item, nestedSetToLevels } from '../FlameGraph/dataTransform'; import { Item, nestedSetToLevels } from '../FlameGraph/dataTransform';
import { data } from '../FlameGraph/testData/dataNestedSet'; import { data } from '../FlameGraph/testData/dataNestedSet';
@ -21,6 +21,7 @@ describe('FlameGraphTopTableContainer', () => {
return ( return (
<FlameGraphTopTableContainer <FlameGraphTopTableContainer
data={flameGraphData} data={flameGraphData}
app={CoreApp.Explore}
totalLevels={levels.length} totalLevels={levels.length}
selectedView={selectedView} selectedView={selectedView}
search={search} search={search}

View File

@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import React, { useCallback, useEffect, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import { createTheme, DataFrame, Field, FieldType, getDisplayProcessor } from '@grafana/data'; import { CoreApp, createTheme, DataFrame, Field, FieldType, getDisplayProcessor } from '@grafana/data';
import { useStyles2 } from '@grafana/ui'; import { useStyles2 } from '@grafana/ui';
import { PIXELS_PER_LEVEL } from '../../constants'; import { PIXELS_PER_LEVEL } from '../../constants';
@ -12,6 +12,7 @@ import FlameGraphTopTable from './FlameGraphTopTable';
type Props = { type Props = {
data: DataFrame; data: DataFrame;
app: CoreApp;
totalLevels: number; totalLevels: number;
selectedView: SelectedView; selectedView: SelectedView;
search: string; search: string;
@ -23,6 +24,7 @@ type Props = {
const FlameGraphTopTableContainer = ({ const FlameGraphTopTableContainer = ({
data, data,
app,
totalLevels, totalLevels,
selectedView, selectedView,
search, search,
@ -31,7 +33,7 @@ const FlameGraphTopTableContainer = ({
setRangeMin, setRangeMin,
setRangeMax, setRangeMax,
}: Props) => { }: Props) => {
const styles = useStyles2(() => getStyles(selectedView)); const styles = useStyles2(() => getStyles(selectedView, app));
const [topTable, setTopTable] = useState<TopTableData[]>(); const [topTable, setTopTable] = useState<TopTableData[]>();
const valueField = const valueField =
data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number); data.fields.find((f) => f.name === 'value') ?? data.fields.find((f) => f.type === FieldType.number);
@ -122,7 +124,7 @@ const FlameGraphTopTableContainer = ({
); );
}; };
const getStyles = (selectedView: SelectedView) => { const getStyles = (selectedView: SelectedView, app: CoreApp) => {
const marginRight = '20px'; const marginRight = '20px';
return { return {
@ -131,6 +133,7 @@ const getStyles = (selectedView: SelectedView) => {
float: left; float: left;
margin-right: ${marginRight}; margin-right: ${marginRight};
width: ${selectedView === SelectedView.TopTable ? '100%' : `calc(50% - ${marginRight})`}; width: ${selectedView === SelectedView.TopTable ? '100%' : `calc(50% - ${marginRight})`};
${app !== CoreApp.Explore ? 'height: calc(100% - 44px)' : ''}; // 44px to adjust for header pushing content down
`, `,
}; };
}; };

View File

@ -1,11 +1,11 @@
import React from 'react'; import React from 'react';
import { PanelPlugin, PanelProps } from '@grafana/data'; import { CoreApp, PanelPlugin, PanelProps } from '@grafana/data';
import FlameGraphContainer from './components/FlameGraphContainer'; import FlameGraphContainer from './components/FlameGraphContainer';
export const FlameGraphPanel: React.FunctionComponent<PanelProps> = (props) => { export const FlameGraphPanel: React.FunctionComponent<PanelProps> = (props) => {
return <FlameGraphContainer data={props.data.series[0]} />; return <FlameGraphContainer data={props.data.series[0]} app={CoreApp.Unknown} flameGraphHeight={props.height} />;
}; };
export const plugin = new PanelPlugin(FlameGraphPanel); export const plugin = new PanelPlugin(FlameGraphPanel);