StatusHistory: Add pagination option (#99517)

* first pass

* Add to docs

* Move pagination hook and styles to a shared util

* Update docs/sources/panels-visualizations/visualizations/status-history/index.md

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>

---------

Co-authored-by: Isabel Matwawana <76437239+imatwawana@users.noreply.github.com>
This commit is contained in:
Kristina 2025-01-24 13:52:04 -06:00 committed by GitHub
parent af663dadc7
commit d409853683
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 197 additions and 153 deletions

View File

@ -111,6 +111,10 @@ Controls whether values are rendered inside the value boxes. Auto will render va
Controls the height of boxes. 1 = maximum space and 0 = minimum space. Controls the height of boxes. 1 = maximum space and 0 = minimum space.
### Page size (enable pagination)
The **Page size** option lets you paginate the status history visualization to limit how many series are visible at once. This is useful when you have many series.
### Column width ### Column width
Controls the width of boxes. 1 = maximum space and 0 = minimum space. Controls the width of boxes. 1 = maximum space and 0 = minimum space.

View File

@ -17,6 +17,10 @@ export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui
* Controls the column width * Controls the column width
*/ */
colWidth?: number; colWidth?: number;
/**
* Enables pagination when > 0
*/
perPage?: number;
/** /**
* Set the height of the rows * Set the height of the rows
*/ */
@ -29,6 +33,7 @@ export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui
export const defaultOptions: Partial<Options> = { export const defaultOptions: Partial<Options> = {
colWidth: 0.9, colWidth: 0.9,
perPage: 20,
rowHeight: 0.9, rowHeight: 0.9,
showValue: ui.VisibilityMode.Auto, showValue: ui.VisibilityMode.Auto,
}; };

View File

@ -1,12 +1,9 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react'; import { useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DashboardCursorSync, DataFrame, PanelProps } from '@grafana/data'; import { DashboardCursorSync, PanelProps } from '@grafana/data';
import { import {
AxisPlacement, AxisPlacement,
EventBusPlugin, EventBusPlugin,
Pagination,
TooltipDisplayMode, TooltipDisplayMode,
TooltipPlugin2, TooltipPlugin2,
usePanelContext, usePanelContext,
@ -15,7 +12,6 @@ import {
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2'; import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart'; import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import { import {
makeFramePerSeries,
prepareTimelineFields, prepareTimelineFields,
prepareTimelineLegendItems, prepareTimelineLegendItems,
TimelineMode, TimelineMode,
@ -26,73 +22,11 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils'; import { getTimezones } from '../timeseries/utils';
import { StateTimelineTooltip2 } from './StateTimelineTooltip2'; import { StateTimelineTooltip2 } from './StateTimelineTooltip2';
import { Options, defaultOptions } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
import { containerStyles, usePagination } from './utils';
interface TimelinePanelProps extends PanelProps<Options> {} interface TimelinePanelProps extends PanelProps<Options> {}
const styles = {
container: css({
display: 'flex',
flexDirection: 'column',
}),
paginationContainer: css({
display: 'flex',
justifyContent: 'center',
width: '100%',
}),
paginationElement: css({
marginTop: '8px',
}),
};
function usePagination(frames?: DataFrame[], perPage?: number) {
const [currentPage, setCurrentPage] = useState(1);
const [paginationWrapperRef, { height: paginationHeight, width: paginationWidth }] = useMeasure<HTMLDivElement>();
const pagedFrames = useMemo(
() => (!perPage || frames == null ? frames : makeFramePerSeries(frames)),
[frames, perPage]
);
if (!perPage || pagedFrames == null) {
return {
paginatedFrames: pagedFrames,
paginationRev: 'disabled',
paginationElement: undefined,
paginationHeight: 0,
};
}
perPage ||= defaultOptions.perPage!;
const numberOfPages = Math.ceil(pagedFrames.length / perPage);
// `perPage` changing might lead to temporarily too large values of `currentPage`.
const currentPageCapped = Math.min(currentPage, numberOfPages);
const pageOffset = (currentPageCapped - 1) * perPage;
const currentPageFrames = pagedFrames.slice(pageOffset, pageOffset + perPage);
// `paginationRev` needs to change value whenever any of the pagination settings changes.
// It's used in to trigger a reconfiguration of the underlying graphs (which is cached,
// hence an explicit nudge is required).
const paginationRev = `${currentPageCapped}/${perPage}`;
const showSmallVersion = paginationWidth < 550;
const paginationElement = (
<div className={styles.paginationContainer} ref={paginationWrapperRef}>
<Pagination
className={styles.paginationElement}
currentPage={currentPageCapped}
numberOfPages={numberOfPages}
showSmallVersion={showSmallVersion}
onNavigate={setCurrentPage}
/>
</div>
);
return { paginatedFrames: currentPageFrames, paginationRev, paginationElement, paginationHeight };
}
/** /**
* @alpha * @alpha
*/ */
@ -141,7 +75,7 @@ export const StateTimelinePanel = ({
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations()); const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return ( return (
<div className={styles.container}> <div className={containerStyles.container}>
<TimelineChart <TimelineChart
theme={theme} theme={theme}
frames={paginatedFrames} frames={paginatedFrames}

View File

@ -0,0 +1,75 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DataFrame } from '@grafana/data';
import { Pagination } from '@grafana/ui';
import { makeFramePerSeries } from 'app/core/components/TimelineChart/utils';
import { defaultOptions } from './panelcfg.gen';
export const containerStyles = {
container: css({
display: 'flex',
flexDirection: 'column',
}),
};
const styles = {
paginationContainer: css({
display: 'flex',
justifyContent: 'center',
width: '100%',
}),
paginationElement: css({
marginTop: '8px',
}),
};
export function usePagination(frames?: DataFrame[], perPage?: number) {
const [currentPage, setCurrentPage] = useState(1);
const [paginationWrapperRef, { height: paginationHeight, width: paginationWidth }] = useMeasure<HTMLDivElement>();
const pagedFrames = useMemo(
() => (!perPage || frames == null ? frames : makeFramePerSeries(frames)),
[frames, perPage]
);
if (!perPage || pagedFrames == null) {
return {
paginatedFrames: pagedFrames,
paginationRev: 'disabled',
paginationElement: undefined,
paginationHeight: 0,
};
}
perPage ||= defaultOptions.perPage!;
const numberOfPages = Math.ceil(pagedFrames.length / perPage);
// `perPage` changing might lead to temporarily too large values of `currentPage`.
const currentPageCapped = Math.min(currentPage, numberOfPages);
const pageOffset = (currentPageCapped - 1) * perPage;
const currentPageFrames = pagedFrames.slice(pageOffset, pageOffset + perPage);
// `paginationRev` needs to change value whenever any of the pagination settings changes.
// It's used in to trigger a reconfiguration of the underlying graphs (which is cached,
// hence an explicit nudge is required).
const paginationRev = `${currentPageCapped}/${perPage}`;
const showSmallVersion = paginationWidth < 550;
const paginationElement = (
<div className={styles.paginationContainer} ref={paginationWrapperRef}>
<Pagination
className={styles.paginationElement}
currentPage={currentPageCapped}
numberOfPages={numberOfPages}
showSmallVersion={showSmallVersion}
onNavigate={setCurrentPage}
/>
</div>
);
return { paginatedFrames: currentPageFrames, paginationRev, paginationElement, paginationHeight };
}

View File

@ -18,6 +18,7 @@ import {
} from 'app/core/components/TimelineChart/utils'; } from 'app/core/components/TimelineChart/utils';
import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2'; import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2';
import { containerStyles, usePagination } from '../state-timeline/utils';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2'; import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin'; import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils'; import { getTimezones } from '../timeseries/utils';
@ -53,14 +54,19 @@ export const StatusHistoryPanel = ({
[data.series, timeRange, theme] [data.series, timeRange, theme]
); );
const { paginatedFrames, paginationRev, paginationElement, paginationHeight } = usePagination(
frames,
options.perPage
);
const legendItems = useMemo( const legendItems = useMemo(
() => prepareTimelineLegendItems(frames, options.legend, theme), () => prepareTimelineLegendItems(paginatedFrames, options.legend, theme),
[frames, options.legend, theme] [paginatedFrames, options.legend, theme]
); );
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]); const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
if (!frames || warn) { if (!paginatedFrames || warn) {
return ( return (
<div className="panel-empty"> <div className="panel-empty">
<p>{warn ?? 'No data found in response'}</p> <p>{warn ?? 'No data found in response'}</p>
@ -69,26 +75,28 @@ export const StatusHistoryPanel = ({
} }
// Status grid requires some space between values // Status grid requires some space between values
if (frames[0].length > width / 2) { if (paginatedFrames[0].length > width / 2) {
return ( return (
<div className="panel-empty"> <div className="panel-empty">
<p> <p>
Too many points to visualize properly. <br /> Too many points to visualize properly. <br />
Update the query to return fewer points. <br />({frames[0].length} points received) Update the query to return fewer points. <br />({paginatedFrames[0].length} points received)
</p> </p>
</div> </div>
); );
} }
return ( return (
<div className={containerStyles.container}>
<TimelineChart <TimelineChart
theme={theme} theme={theme}
frames={frames} frames={paginatedFrames}
structureRev={data.structureRev} structureRev={data.structureRev}
paginationRev={paginationRev}
timeRange={timeRange} timeRange={timeRange}
timeZone={timezones} timeZone={timezones}
width={width} width={width}
height={height} height={height - paginationHeight}
legendItems={legendItems} legendItems={legendItems}
{...options} {...options}
mode={TimelineMode.Samples} mode={TimelineMode.Samples}
@ -163,5 +171,7 @@ export const StatusHistoryPanel = ({
); );
}} }}
</TimelineChart> </TimelineChart>
{paginationElement}
</div>
); );
}; };

View File

@ -82,6 +82,15 @@ export const plugin = new PanelPlugin<Options, FieldConfig>(StatusHistoryPanel)
max: 1, max: 1,
step: 0.01, step: 0.01,
}, },
})
.addNumberInput({
path: 'perPage',
name: 'Page size (enable pagination)',
settings: {
min: 1,
step: 1,
integer: true,
},
}); });
commonOptionsBuilder.addLegendOptions(builder, false); commonOptionsBuilder.addLegendOptions(builder, false);

View File

@ -35,6 +35,8 @@ composableKinds: PanelCfg: {
showValue: ui.VisibilityMode & (*"auto" | _) showValue: ui.VisibilityMode & (*"auto" | _)
//Controls the column width //Controls the column width
colWidth?: float & <=1 | *0.9 colWidth?: float & <=1 | *0.9
//Enables pagination when > 0
perPage?: number & >=1 | *20
} @cuetsy(kind="interface") } @cuetsy(kind="interface")
FieldConfig: { FieldConfig: {
ui.AxisConfig ui.AxisConfig

View File

@ -15,6 +15,10 @@ export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui
* Controls the column width * Controls the column width
*/ */
colWidth?: number; colWidth?: number;
/**
* Enables pagination when > 0
*/
perPage?: number;
/** /**
* Set the height of the rows * Set the height of the rows
*/ */
@ -27,6 +31,7 @@ export interface Options extends ui.OptionsWithLegend, ui.OptionsWithTooltip, ui
export const defaultOptions: Partial<Options> = { export const defaultOptions: Partial<Options> = {
colWidth: 0.9, colWidth: 0.9,
perPage: 20,
rowHeight: 0.9, rowHeight: 0.9,
showValue: ui.VisibilityMode.Auto, showValue: ui.VisibilityMode.Auto,
}; };