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>
pull/98710/head^2
Kristina 5 months ago committed by GitHub
parent af663dadc7
commit d409853683
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      docs/sources/panels-visualizations/visualizations/status-history/index.md
  2. 5
      packages/grafana-schema/src/raw/composable/statushistory/panelcfg/x/StatusHistoryPanelCfg_types.gen.ts
  3. 74
      public/app/plugins/panel/state-timeline/StateTimelinePanel.tsx
  4. 75
      public/app/plugins/panel/state-timeline/utils.tsx
  5. 182
      public/app/plugins/panel/status-history/StatusHistoryPanel.tsx
  6. 9
      public/app/plugins/panel/status-history/module.tsx
  7. 2
      public/app/plugins/panel/status-history/panelcfg.cue
  8. 5
      public/app/plugins/panel/status-history/panelcfg.gen.ts

@ -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.
### 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
Controls the width of boxes. 1 = maximum space and 0 = minimum space.

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

@ -1,12 +1,9 @@
import { css } from '@emotion/css';
import { useMemo, useState } from 'react';
import { useMeasure } from 'react-use';
import { DashboardCursorSync, DataFrame, PanelProps } from '@grafana/data';
import { DashboardCursorSync, PanelProps } from '@grafana/data';
import {
AxisPlacement,
EventBusPlugin,
Pagination,
TooltipDisplayMode,
TooltipPlugin2,
usePanelContext,
@ -15,7 +12,6 @@ import {
import { TimeRange2, TooltipHoverMode } from '@grafana/ui/src/components/uPlot/plugins/TooltipPlugin2';
import { TimelineChart } from 'app/core/components/TimelineChart/TimelineChart';
import {
makeFramePerSeries,
prepareTimelineFields,
prepareTimelineLegendItems,
TimelineMode,
@ -26,73 +22,11 @@ import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
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> {}
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
*/
@ -141,7 +75,7 @@ export const StateTimelinePanel = ({
const enableAnnotationCreation = Boolean(canAddAnnotations && canAddAnnotations());
return (
<div className={styles.container}>
<div className={containerStyles.container}>
<TimelineChart
theme={theme}
frames={paginatedFrames}

@ -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 };
}

@ -18,6 +18,7 @@ import {
} from 'app/core/components/TimelineChart/utils';
import { StateTimelineTooltip2 } from '../state-timeline/StateTimelineTooltip2';
import { containerStyles, usePagination } from '../state-timeline/utils';
import { AnnotationsPlugin2 } from '../timeseries/plugins/AnnotationsPlugin2';
import { OutsideRangePlugin } from '../timeseries/plugins/OutsideRangePlugin';
import { getTimezones } from '../timeseries/utils';
@ -53,14 +54,19 @@ export const StatusHistoryPanel = ({
[data.series, timeRange, theme]
);
const { paginatedFrames, paginationRev, paginationElement, paginationHeight } = usePagination(
frames,
options.perPage
);
const legendItems = useMemo(
() => prepareTimelineLegendItems(frames, options.legend, theme),
[frames, options.legend, theme]
() => prepareTimelineLegendItems(paginatedFrames, options.legend, theme),
[paginatedFrames, options.legend, theme]
);
const timezones = useMemo(() => getTimezones(options.timezone, timeZone), [options.timezone, timeZone]);
if (!frames || warn) {
if (!paginatedFrames || warn) {
return (
<div className="panel-empty">
<p>{warn ?? 'No data found in response'}</p>
@ -69,99 +75,103 @@ export const StatusHistoryPanel = ({
}
// Status grid requires some space between values
if (frames[0].length > width / 2) {
if (paginatedFrames[0].length > width / 2) {
return (
<div className="panel-empty">
<p>
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>
</div>
);
}
return (
<TimelineChart
theme={theme}
frames={frames}
structureRev={data.structureRev}
timeRange={timeRange}
timeZone={timezones}
width={width}
height={height}
legendItems={legendItems}
{...options}
mode={TimelineMode.Samples}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
cursorSync={cursorSync}
>
{(builder, alignedFrame) => {
return (
<>
{cursorSync !== DashboardCursorSync.Off && (
<EventBusPlugin config={builder} eventBus={eventBus} frame={alignedFrame} />
)}
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Multi ? TooltipHoverMode.xAll : TooltipHoverMode.xOne
}
queryZoom={onChangeTimeRange}
syncMode={cursorSync}
syncScope={eventsScope}
getDataLinks={(seriesIdx: number, dataIdx: number) =>
alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? []
}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => {
if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
<div className={containerStyles.container}>
<TimelineChart
theme={theme}
frames={paginatedFrames}
structureRev={data.structureRev}
paginationRev={paginationRev}
timeRange={timeRange}
timeZone={timezones}
width={width}
height={height - paginationHeight}
legendItems={legendItems}
{...options}
mode={TimelineMode.Samples}
replaceVariables={replaceVariables}
dataLinkPostProcessor={dataLinkPostProcessor}
cursorSync={cursorSync}
>
{(builder, alignedFrame) => {
return (
<>
{cursorSync !== DashboardCursorSync.Off && (
<EventBusPlugin config={builder} eventBus={eventBus} frame={alignedFrame} />
)}
{options.tooltip.mode !== TooltipDisplayMode.None && (
<TooltipPlugin2
config={builder}
hoverMode={
options.tooltip.mode === TooltipDisplayMode.Multi ? TooltipHoverMode.xAll : TooltipHoverMode.xOne
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<StateTimelineTooltip2
series={alignedFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
timeRange={timeRange}
annotate={enableAnnotationCreation ? annotate : undefined}
withDuration={false}
maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables}
dataLinks={dataLinks}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
/>
)}
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={builder}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
canvasRegionRendering={false}
/>
)}
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
</>
);
}}
</TimelineChart>
queryZoom={onChangeTimeRange}
syncMode={cursorSync}
syncScope={eventsScope}
getDataLinks={(seriesIdx: number, dataIdx: number) =>
alignedFrame.fields[seriesIdx]!.getLinks?.({ valueRowIndex: dataIdx }) ?? []
}
render={(u, dataIdxs, seriesIdx, isPinned, dismiss, timeRange2, viaSync, dataLinks) => {
if (enableAnnotationCreation && timeRange2 != null) {
setNewAnnotationRange(timeRange2);
dismiss();
return;
}
const annotate = () => {
let xVal = u.posToVal(u.cursor.left!, 'x');
setNewAnnotationRange({ from: xVal, to: xVal });
dismiss();
};
return (
<StateTimelineTooltip2
series={alignedFrame}
dataIdxs={dataIdxs}
seriesIdx={seriesIdx}
mode={viaSync ? TooltipDisplayMode.Multi : options.tooltip.mode}
sortOrder={options.tooltip.sort}
isPinned={isPinned}
timeRange={timeRange}
annotate={enableAnnotationCreation ? annotate : undefined}
withDuration={false}
maxHeight={options.tooltip.maxHeight}
replaceVariables={replaceVariables}
dataLinks={dataLinks}
/>
);
}}
maxWidth={options.tooltip.maxWidth}
/>
)}
{alignedFrame.fields[0].config.custom?.axisPlacement !== AxisPlacement.Hidden && (
<AnnotationsPlugin2
annotations={data.annotations ?? []}
config={builder}
timeZone={timeZone}
newRange={newAnnotationRange}
setNewRange={setNewAnnotationRange}
canvasRegionRendering={false}
/>
)}
<OutsideRangePlugin config={builder} onChangeTimeRange={onChangeTimeRange} />
</>
);
}}
</TimelineChart>
{paginationElement}
</div>
);
};

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

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

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

Loading…
Cancel
Save