mirror of
https://github.com/grafana/grafana.git
synced 2024-11-30 12:44:10 -06:00
FlameGraph: Add option to align text left or right (#68893)
This commit is contained in:
parent
25b89babac
commit
55bba165b1
@ -18,6 +18,8 @@ export const availableIconsIndex = {
|
||||
'angle-left': true,
|
||||
'angle-right': true,
|
||||
'angle-up': true,
|
||||
'align-left': true,
|
||||
'align-right': true,
|
||||
apps: true,
|
||||
arrow: true,
|
||||
'arrow-down': true,
|
||||
|
@ -46,6 +46,7 @@ describe('FlameGraph', () => {
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
textAlign={'left'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -25,7 +25,7 @@ import { CoreApp } from '@grafana/data';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { PIXELS_PER_LEVEL } from '../../constants';
|
||||
import { SelectedView, ContextMenuData } from '../types';
|
||||
import { SelectedView, ContextMenuData, TextAlign } from '../types';
|
||||
|
||||
import FlameGraphContextMenu from './FlameGraphContextMenu';
|
||||
import FlameGraphMetadata from './FlameGraphMetadata';
|
||||
@ -48,6 +48,7 @@ type Props = {
|
||||
setRangeMax: (range: number) => void;
|
||||
selectedView: SelectedView;
|
||||
style?: React.CSSProperties;
|
||||
textAlign: TextAlign;
|
||||
};
|
||||
|
||||
const FlameGraph = ({
|
||||
@ -64,6 +65,7 @@ const FlameGraph = ({
|
||||
setRangeMin,
|
||||
setRangeMax,
|
||||
selectedView,
|
||||
textAlign,
|
||||
}: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const totalTicks = data.getValue(0);
|
||||
@ -119,10 +121,21 @@ const FlameGraph = ({
|
||||
const dimensions = getRectDimensionsForLevel(data, level, levelIndex, totalTicks, rangeMin, pixelsPerTick);
|
||||
for (const rect of dimensions) {
|
||||
// Render each rectangle based on the computed dimensions
|
||||
renderRect(ctx, rect, totalTicks, rangeMin, rangeMax, search, levelIndex, topLevelIndex, foundLabels);
|
||||
renderRect(
|
||||
ctx,
|
||||
rect,
|
||||
totalTicks,
|
||||
rangeMin,
|
||||
rangeMax,
|
||||
search,
|
||||
levelIndex,
|
||||
topLevelIndex,
|
||||
foundLabels,
|
||||
textAlign
|
||||
);
|
||||
}
|
||||
}
|
||||
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels]);
|
||||
}, [data, levels, wrapperWidth, totalTicks, rangeMin, rangeMax, search, topLevelIndex, foundLabels, textAlign]);
|
||||
|
||||
useEffect(() => {
|
||||
if (graphRef.current) {
|
||||
|
@ -8,6 +8,7 @@ import {
|
||||
LABEL_THRESHOLD,
|
||||
PIXELS_PER_LEVEL,
|
||||
} from '../../constants';
|
||||
import { TextAlign } from '../types';
|
||||
|
||||
import { FlameGraphDataContainer, LevelItem } from './dataTransform';
|
||||
|
||||
@ -80,7 +81,8 @@ export function renderRect(
|
||||
query: string,
|
||||
levelIndex: number,
|
||||
topLevelIndex: number,
|
||||
foundNames: Set<string>
|
||||
foundNames: Set<string>,
|
||||
textAlign: TextAlign
|
||||
) {
|
||||
if (rect.width < HIDE_THRESHOLD) {
|
||||
return;
|
||||
@ -110,18 +112,40 @@ export function renderRect(
|
||||
ctx.fill();
|
||||
|
||||
if (!rect.collapsed && rect.width >= LABEL_THRESHOLD) {
|
||||
renderLabel(ctx, name, rect, textAlign);
|
||||
}
|
||||
}
|
||||
|
||||
// Renders a text inside the node rectangle. It allows setting alignment of the text left or right which takes effect
|
||||
// when text is too long to fit in the rectangle.
|
||||
function renderLabel(ctx: CanvasRenderingContext2D, name: string, rect: RectData, textAlign: TextAlign) {
|
||||
ctx.save();
|
||||
ctx.clip(); // so text does not overflow
|
||||
ctx.fillStyle = '#222';
|
||||
ctx.fillText(
|
||||
`${name} (${rect.unitLabel})`,
|
||||
Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT,
|
||||
rect.y + PIXELS_PER_LEVEL / 2
|
||||
);
|
||||
ctx.restore();
|
||||
|
||||
// We only measure name here instead of full label because of how we deal with the units and aligning later.
|
||||
const measure = ctx.measureText(name);
|
||||
const spaceForTextInRect = rect.width - BAR_TEXT_PADDING_LEFT;
|
||||
|
||||
let label = `${name} (${rect.unitLabel})`;
|
||||
let labelX = Math.max(rect.x, 0) + BAR_TEXT_PADDING_LEFT;
|
||||
|
||||
// We use the desired alignment only if there is not enough space for the text, otherwise we keep left alignment as
|
||||
// that will already show full text.
|
||||
if (measure.width > spaceForTextInRect) {
|
||||
ctx.textAlign = textAlign;
|
||||
// If aligned to the right we don't want to take the space with the unit label as the assumption is user wants to
|
||||
// mainly see the name. This also reflects how pyro/flamegraph works.
|
||||
if (textAlign === 'right') {
|
||||
label = name;
|
||||
labelX = rect.x + rect.width - BAR_TEXT_PADDING_LEFT;
|
||||
}
|
||||
}
|
||||
|
||||
ctx.fillText(label, labelX, rect.y + PIXELS_PER_LEVEL / 2);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the X position of the bar. totalTicks * rangeMin is to adjust for any current zoom. So if we zoom to a
|
||||
* section of the graph we align and shift the X coordinates accordingly.
|
||||
|
@ -11,7 +11,7 @@ import FlameGraph from './FlameGraph/FlameGraph';
|
||||
import { FlameGraphDataContainer, LevelItem, nestedSetToLevels } from './FlameGraph/dataTransform';
|
||||
import FlameGraphHeader from './FlameGraphHeader';
|
||||
import FlameGraphTopTableContainer from './TopTable/FlameGraphTopTableContainer';
|
||||
import { SelectedView } from './types';
|
||||
import { SelectedView, TextAlign } from './types';
|
||||
|
||||
type Props = {
|
||||
data?: DataFrame;
|
||||
@ -26,6 +26,8 @@ const FlameGraphContainer = (props: Props) => {
|
||||
const [search, setSearch] = useState('');
|
||||
const [selectedView, setSelectedView] = useState(SelectedView.Both);
|
||||
const [sizeRef, { width: containerWidth }] = useMeasure<HTMLDivElement>();
|
||||
const [textAlign, setTextAlign] = useState<TextAlign>('left');
|
||||
|
||||
const theme = useTheme2();
|
||||
|
||||
const [dataContainer, levels] = useMemo((): [FlameGraphDataContainer, LevelItem[][]] | [undefined, undefined] => {
|
||||
@ -75,6 +77,8 @@ const FlameGraphContainer = (props: Props) => {
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={containerWidth}
|
||||
textAlign={textAlign}
|
||||
onTextAlignChange={setTextAlign}
|
||||
/>
|
||||
|
||||
<div className={styles.body}>
|
||||
@ -108,6 +112,7 @@ const FlameGraphContainer = (props: Props) => {
|
||||
setRangeMin={setRangeMin}
|
||||
setRangeMax={setRangeMax}
|
||||
selectedView={selectedView}
|
||||
textAlign={textAlign}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -125,7 +130,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
flex: '1 1 0',
|
||||
flexDirection: 'column',
|
||||
minHeight: 0,
|
||||
gap: theme.spacing(2),
|
||||
gap: theme.spacing(1),
|
||||
}),
|
||||
body: css({
|
||||
display: 'flex',
|
||||
|
@ -25,6 +25,8 @@ describe('FlameGraphHeader', () => {
|
||||
selectedView={selectedView}
|
||||
setSelectedView={setSelectedView}
|
||||
containerWidth={1600}
|
||||
onTextAlignChange={jest.fn()}
|
||||
textAlign={'left'}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
@ -3,14 +3,14 @@ import React, { useEffect, useState } from 'react';
|
||||
import useDebounce from 'react-use/lib/useDebounce';
|
||||
import usePrevious from 'react-use/lib/usePrevious';
|
||||
|
||||
import { GrafanaTheme2, CoreApp } from '@grafana/data';
|
||||
import { CoreApp, GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Button, Input, RadioButtonGroup, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { config } from '../../../../core/config';
|
||||
import { MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH } from '../constants';
|
||||
|
||||
import { SelectedView } from './types';
|
||||
import { SelectedView, TextAlign } from './types';
|
||||
|
||||
type Props = {
|
||||
app: CoreApp;
|
||||
@ -23,6 +23,8 @@ type Props = {
|
||||
selectedView: SelectedView;
|
||||
setSelectedView: (view: SelectedView) => void;
|
||||
containerWidth: number;
|
||||
textAlign: TextAlign;
|
||||
onTextAlignChange: (align: TextAlign) => void;
|
||||
};
|
||||
|
||||
const FlameGraphHeader = ({
|
||||
@ -36,19 +38,15 @@ const FlameGraphHeader = ({
|
||||
selectedView,
|
||||
setSelectedView,
|
||||
containerWidth,
|
||||
textAlign,
|
||||
onTextAlignChange,
|
||||
}: Props) => {
|
||||
const styles = useStyles2((theme) => getStyles(theme, app));
|
||||
|
||||
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
||||
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
||||
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
|
||||
];
|
||||
|
||||
if (containerWidth >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
|
||||
viewOptions.push({
|
||||
value: SelectedView.Both,
|
||||
label: 'Both',
|
||||
description: 'Show both the top table and flame graph',
|
||||
function interaction(name: string, context: Record<string, string | number>) {
|
||||
reportInteraction(`grafana_flamegraph_${name}`, {
|
||||
app,
|
||||
grafana_version: config.buildInfo.version,
|
||||
...context,
|
||||
});
|
||||
}
|
||||
|
||||
@ -83,15 +81,23 @@ const FlameGraphHeader = ({
|
||||
</div>
|
||||
|
||||
<div className={styles.rightContainer}>
|
||||
<RadioButtonGroup<TextAlign>
|
||||
size="sm"
|
||||
disabled={selectedView === SelectedView.TopTable}
|
||||
options={alignOptions}
|
||||
value={textAlign}
|
||||
onChange={(val) => {
|
||||
interaction('text_align_selected', { align: val });
|
||||
onTextAlignChange(val);
|
||||
}}
|
||||
className={styles.buttonSpacing}
|
||||
/>
|
||||
<RadioButtonGroup<SelectedView>
|
||||
options={viewOptions}
|
||||
size="sm"
|
||||
options={getViewOptions(containerWidth)}
|
||||
value={selectedView}
|
||||
onChange={(view) => {
|
||||
reportInteraction('grafana_flamegraph_view_selected', {
|
||||
app,
|
||||
grafana_version: config.buildInfo.version,
|
||||
view,
|
||||
});
|
||||
interaction('view_selected', { view });
|
||||
setSelectedView(view);
|
||||
}}
|
||||
/>
|
||||
@ -100,6 +106,28 @@ const FlameGraphHeader = ({
|
||||
);
|
||||
};
|
||||
|
||||
const alignOptions: Array<SelectableValue<TextAlign>> = [
|
||||
{ value: 'left', description: 'Align text left', icon: 'align-left' },
|
||||
{ value: 'right', description: 'Align text right', icon: 'align-right' },
|
||||
];
|
||||
|
||||
function getViewOptions(width: number): Array<SelectableValue<SelectedView>> {
|
||||
let viewOptions: Array<{ value: SelectedView; label: string; description: string }> = [
|
||||
{ value: SelectedView.TopTable, label: 'Top Table', description: 'Only show top table' },
|
||||
{ value: SelectedView.FlameGraph, label: 'Flame Graph', description: 'Only show flame graph' },
|
||||
];
|
||||
|
||||
if (width >= MIN_WIDTH_TO_SHOW_BOTH_TOPTABLE_AND_FLAMEGRAPH) {
|
||||
viewOptions.push({
|
||||
value: SelectedView.Both,
|
||||
label: 'Both',
|
||||
description: 'Show both the top table and flame graph',
|
||||
});
|
||||
}
|
||||
|
||||
return viewOptions;
|
||||
}
|
||||
|
||||
function useSearchInput(
|
||||
search: string,
|
||||
setSearch: (search: string) => void
|
||||
@ -133,9 +161,14 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
|
||||
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' : ''};
|
||||
${app === CoreApp.Explore
|
||||
? css`
|
||||
position: sticky;
|
||||
padding-bottom: ${theme.spacing(1)};
|
||||
padding-top: ${theme.spacing(1)};
|
||||
`
|
||||
: ''};
|
||||
`,
|
||||
inputContainer: css`
|
||||
float: left;
|
||||
@ -147,6 +180,9 @@ const getStyles = (theme: GrafanaTheme2, app: CoreApp) => ({
|
||||
rightContainer: css`
|
||||
float: right;
|
||||
`,
|
||||
buttonSpacing: css`
|
||||
margin-right: ${theme.spacing(1)};
|
||||
`,
|
||||
});
|
||||
|
||||
export default FlameGraphHeader;
|
||||
|
@ -44,3 +44,5 @@ export type TopTableValue = {
|
||||
value: number;
|
||||
unitValue: string;
|
||||
};
|
||||
|
||||
export type TextAlign = 'left' | 'right';
|
||||
|
Loading…
Reference in New Issue
Block a user