mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: Add enable pagination option (#45732)
* Table: Add page size option / pagination * Update docs/sources/visualizations/table/_index.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> * FIx packages build * Move pagination below footer * Move styles to tableStyles * Fix typecheck in jaeger-ui * Set footer to hide onChange * Styling tweaks * Center paging * Tweaks * Change pageSize to enablePagination * Move header and footer options to a separate category * Fix performance and styling issue for the pagination * Some more styling and tweaking * Fix tests * Update docs/sources/visualizations/table/_index.md Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> * Update docs/sources/visualizations/table/_index.md Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com> Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>
This commit is contained in:
parent
502cf8b37f
commit
15b48fc188
@ -105,3 +105,7 @@ Enables value inspection from table cell. The raw value is presented in a modal
|
|||||||
## Column filter
|
## Column filter
|
||||||
|
|
||||||
You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}).
|
You can temporarily change how column data is displayed. For example, you can order values from highest to lowest or hide specific values. For more information, refer to [Filter table columns]({{< relref "./filter-table-columns.md" >}}).
|
||||||
|
|
||||||
|
## Pagination
|
||||||
|
|
||||||
|
Use this option to enable or disable pagination. It is a front-end option that does not affect queries. When enabled, the page size automatically adjusts to the height of the table.
|
||||||
|
@ -102,7 +102,7 @@ e2e.scenario({
|
|||||||
e2e.components.PanelEditor.DataPane.content().should('be.visible');
|
e2e.components.PanelEditor.DataPane.content().should('be.visible');
|
||||||
|
|
||||||
// Field & Overrides tabs (need to switch to React based vis, i.e. Table)
|
// Field & Overrides tabs (need to switch to React based vis, i.e. Table)
|
||||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Show header').should('be.visible');
|
e2e.components.PanelEditor.OptionsPane.fieldLabel('Header and footer Show header').should('be.visible');
|
||||||
e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible');
|
e2e.components.PanelEditor.OptionsPane.fieldLabel('Table Column width').should('be.visible');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -9,5 +9,10 @@
|
|||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules"],
|
"exclude": ["dist", "node_modules"],
|
||||||
"extends": "@grafana/tsconfig",
|
"extends": "@grafana/tsconfig",
|
||||||
"include": ["src/**/*.ts*", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts*",
|
||||||
|
"../../public/app/types/jquery/*.ts",
|
||||||
|
"../../public/app/types/*.d.ts",
|
||||||
|
"../grafana-ui/src/types/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { render, screen } from '@testing-library/react';
|
||||||
|
import { Pagination } from './Pagination';
|
||||||
|
|
||||||
|
describe('Pagination component', () => {
|
||||||
|
it('should render only 10 buttons when number of pages is higher than 8', () => {
|
||||||
|
render(<Pagination currentPage={1} numberOfPages={90} onNavigate={() => {}} />);
|
||||||
|
expect(screen.getAllByRole('button')).toHaveLength(10);
|
||||||
|
});
|
||||||
|
it('should only show 3 buttons when showSmallVersion is true', () => {
|
||||||
|
render(<Pagination currentPage={1} numberOfPages={90} onNavigate={() => {}} showSmallVersion />);
|
||||||
|
expect(screen.getAllByRole('button')).toHaveLength(4);
|
||||||
|
});
|
||||||
|
});
|
@ -1,27 +1,36 @@
|
|||||||
import React from 'react';
|
import React, { useMemo } from 'react';
|
||||||
import { css } from '@emotion/css';
|
import { css } from '@emotion/css';
|
||||||
import { stylesFactory } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
import { Button, ButtonVariant } from '../Button';
|
import { Button, ButtonVariant } from '../Button';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
|
|
||||||
const PAGE_LENGTH_TO_CONDENSE = 8;
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
/** The current page index being shown. */
|
/** The current page index being shown. */
|
||||||
currentPage: number;
|
currentPage: number;
|
||||||
/** Number of total pages. */
|
/** Number of total pages. */
|
||||||
numberOfPages: number;
|
numberOfPages: number;
|
||||||
/** Callback function for fetching the selected page */
|
/** Callback function for fetching the selected page. */
|
||||||
onNavigate: (toPage: number) => void;
|
onNavigate: (toPage: number) => void;
|
||||||
/** When set to true and the pagination result is only one page it will not render the pagination at all */
|
/** When set to true and the pagination result is only one page it will not render the pagination at all. */
|
||||||
hideWhenSinglePage?: boolean;
|
hideWhenSinglePage?: boolean;
|
||||||
|
/** Small version only shows the current page and the navigation buttons. */
|
||||||
|
showSmallVersion?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavigate, hideWhenSinglePage }) => {
|
export const Pagination: React.FC<Props> = ({
|
||||||
const styles = getStyles();
|
currentPage,
|
||||||
|
numberOfPages,
|
||||||
|
onNavigate,
|
||||||
|
hideWhenSinglePage,
|
||||||
|
showSmallVersion,
|
||||||
|
}) => {
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
const pageLengthToCondense = showSmallVersion ? 1 : 8;
|
||||||
|
|
||||||
|
const pageButtons = useMemo(() => {
|
||||||
const pages = [...new Array(numberOfPages).keys()];
|
const pages = [...new Array(numberOfPages).keys()];
|
||||||
|
|
||||||
const condensePages = numberOfPages > PAGE_LENGTH_TO_CONDENSE;
|
const condensePages = numberOfPages > pageLengthToCondense;
|
||||||
const getListItem = (page: number, variant: 'primary' | 'secondary') => (
|
const getListItem = (page: number, variant: 'primary' | 'secondary') => (
|
||||||
<li key={page} className={styles.item}>
|
<li key={page} className={styles.item}>
|
||||||
<Button size="sm" variant={variant} onClick={() => onNavigate(page)}>
|
<Button size="sm" variant={variant} onClick={() => onNavigate(page)}>
|
||||||
@ -30,13 +39,13 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
|
|||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageButtons = pages.reduce<JSX.Element[]>((pagesToRender, pageIndex) => {
|
return pages.reduce<JSX.Element[]>((pagesToRender, pageIndex) => {
|
||||||
const page = pageIndex + 1;
|
const page = pageIndex + 1;
|
||||||
const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary';
|
const variant: ButtonVariant = page === currentPage ? 'primary' : 'secondary';
|
||||||
|
|
||||||
// The indexes at which to start and stop condensing pages
|
// The indexes at which to start and stop condensing pages
|
||||||
const lowerBoundIndex = PAGE_LENGTH_TO_CONDENSE;
|
const lowerBoundIndex = pageLengthToCondense;
|
||||||
const upperBoundIndex = numberOfPages - PAGE_LENGTH_TO_CONDENSE + 1;
|
const upperBoundIndex = numberOfPages - pageLengthToCondense + 1;
|
||||||
// When the indexes overlap one another this number is negative
|
// When the indexes overlap one another this number is negative
|
||||||
const differenceOfBounds = upperBoundIndex - lowerBoundIndex;
|
const differenceOfBounds = upperBoundIndex - lowerBoundIndex;
|
||||||
|
|
||||||
@ -45,20 +54,27 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
|
|||||||
const currentPageIsBetweenBounds =
|
const currentPageIsBetweenBounds =
|
||||||
differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex;
|
differenceOfBounds > -1 && currentPage >= lowerBoundIndex && currentPage <= upperBoundIndex;
|
||||||
|
|
||||||
|
// Show ellipsis after that many pages
|
||||||
|
const ellipsisOffset = showSmallVersion ? 1 : 3;
|
||||||
|
|
||||||
|
// The offset to show more pages when currentPageIsBetweenBounds
|
||||||
|
const pageOffset = showSmallVersion ? 0 : 2;
|
||||||
|
|
||||||
if (condensePages) {
|
if (condensePages) {
|
||||||
if (
|
if (
|
||||||
isFirstOrLastPage ||
|
isFirstOrLastPage ||
|
||||||
(currentPage < lowerBoundIndex && page < lowerBoundIndex) ||
|
(currentPage < lowerBoundIndex && page < lowerBoundIndex) ||
|
||||||
(differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) ||
|
(differenceOfBounds >= 0 && currentPage > upperBoundIndex && page > upperBoundIndex) ||
|
||||||
(differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) ||
|
(differenceOfBounds < 0 && currentPage >= lowerBoundIndex && page > upperBoundIndex) ||
|
||||||
(currentPageIsBetweenBounds && page >= currentPage - 2 && page <= currentPage + 2)
|
(currentPageIsBetweenBounds && page >= currentPage - pageOffset && page <= currentPage + pageOffset)
|
||||||
) {
|
) {
|
||||||
// Renders a button for the page
|
// Renders a button for the page
|
||||||
pagesToRender.push(getListItem(page, variant));
|
pagesToRender.push(getListItem(page, variant));
|
||||||
} else if (
|
} else if (
|
||||||
(page === lowerBoundIndex && currentPage < lowerBoundIndex) ||
|
(page === lowerBoundIndex && currentPage < lowerBoundIndex) ||
|
||||||
(page === upperBoundIndex && currentPage > upperBoundIndex) ||
|
(page === upperBoundIndex && currentPage > upperBoundIndex) ||
|
||||||
(currentPageIsBetweenBounds && (page === currentPage - 3 || page === currentPage + 3))
|
(currentPageIsBetweenBounds &&
|
||||||
|
(page === currentPage - ellipsisOffset || page === currentPage + ellipsisOffset))
|
||||||
) {
|
) {
|
||||||
// Renders and ellipsis to represent condensed pages
|
// Renders and ellipsis to represent condensed pages
|
||||||
pagesToRender.push(
|
pagesToRender.push(
|
||||||
@ -72,6 +88,7 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
|
|||||||
}
|
}
|
||||||
return pagesToRender;
|
return pagesToRender;
|
||||||
}, []);
|
}, []);
|
||||||
|
}, [currentPage, numberOfPages, onNavigate, pageLengthToCondense, showSmallVersion, styles.ellipsis, styles.item]);
|
||||||
|
|
||||||
if (hideWhenSinglePage && numberOfPages <= 1) {
|
if (hideWhenSinglePage && numberOfPages <= 1) {
|
||||||
return null;
|
return null;
|
||||||
@ -108,7 +125,7 @@ export const Pagination: React.FC<Props> = ({ currentPage, numberOfPages, onNavi
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => {
|
const getStyles = () => {
|
||||||
return {
|
return {
|
||||||
container: css`
|
container: css`
|
||||||
float: right;
|
float: right;
|
||||||
@ -122,4 +139,4 @@ const getStyles = stylesFactory(() => {
|
|||||||
transform: rotate(90deg);
|
transform: rotate(90deg);
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
};
|
||||||
|
@ -10,18 +10,19 @@ export interface FooterRowProps {
|
|||||||
totalColumnsWidth: number;
|
totalColumnsWidth: number;
|
||||||
footerGroups: HeaderGroup[];
|
footerGroups: HeaderGroup[];
|
||||||
footerValues: FooterItem[];
|
footerValues: FooterItem[];
|
||||||
|
isPaginationVisible: boolean;
|
||||||
height: number;
|
height: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const FooterRow = (props: FooterRowProps) => {
|
export const FooterRow = (props: FooterRowProps) => {
|
||||||
const { totalColumnsWidth, footerGroups, height } = props;
|
const { totalColumnsWidth, footerGroups, height, isPaginationVisible } = props;
|
||||||
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
|
const e2eSelectorsTable = selectors.components.Panels.Visualization.Table;
|
||||||
const tableStyles = useStyles2(getTableStyles);
|
const tableStyles = useStyles2(getTableStyles);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table
|
<table
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: isPaginationVisible ? 'relative' : 'absolute',
|
||||||
width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%',
|
width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%',
|
||||||
bottom: '0px',
|
bottom: '0px',
|
||||||
}}
|
}}
|
||||||
|
@ -186,3 +186,8 @@ export const Footer: Story = (args) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Pagination: Story = (args) => <Basic {...args} />;
|
||||||
|
Pagination.args = {
|
||||||
|
pageSize: 10,
|
||||||
|
};
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import React, { FC, memo, useCallback, useMemo } from 'react';
|
import React, { FC, memo, useCallback, useEffect, useMemo } from 'react';
|
||||||
import { DataFrame, getFieldDisplayName } from '@grafana/data';
|
import { DataFrame, getFieldDisplayName } from '@grafana/data';
|
||||||
import {
|
import {
|
||||||
Cell,
|
Cell,
|
||||||
Column,
|
Column,
|
||||||
|
TableState,
|
||||||
useAbsoluteLayout,
|
useAbsoluteLayout,
|
||||||
useFilters,
|
useFilters,
|
||||||
UseFiltersState,
|
usePagination,
|
||||||
useResizeColumns,
|
useResizeColumns,
|
||||||
UseResizeColumnsState,
|
|
||||||
useSortBy,
|
useSortBy,
|
||||||
UseSortByState,
|
|
||||||
useTable,
|
useTable,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
@ -27,6 +26,7 @@ import { TableCell } from './TableCell';
|
|||||||
import { useStyles2 } from '../../themes';
|
import { useStyles2 } from '../../themes';
|
||||||
import { FooterRow } from './FooterRow';
|
import { FooterRow } from './FooterRow';
|
||||||
import { HeaderRow } from './HeaderRow';
|
import { HeaderRow } from './HeaderRow';
|
||||||
|
import { Pagination } from '../Pagination/Pagination';
|
||||||
|
|
||||||
const COLUMN_MIN_WIDTH = 150;
|
const COLUMN_MIN_WIDTH = 150;
|
||||||
|
|
||||||
@ -45,13 +45,12 @@ export interface Props {
|
|||||||
onSortByChange?: TableSortByActionCallback;
|
onSortByChange?: TableSortByActionCallback;
|
||||||
onCellFilterAdded?: TableFilterActionCallback;
|
onCellFilterAdded?: TableFilterActionCallback;
|
||||||
footerValues?: FooterItem[];
|
footerValues?: FooterItem[];
|
||||||
|
enablePagination?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}>, UseFiltersState<{}> {}
|
|
||||||
|
|
||||||
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
|
function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
|
||||||
return useCallback(
|
return useCallback(
|
||||||
(newState: ReactTableInternalState, action: any) => {
|
(newState: TableState, action: any) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'columnDoneResizing':
|
case 'columnDoneResizing':
|
||||||
if (onColumnResize) {
|
if (onColumnResize) {
|
||||||
@ -95,8 +94,8 @@ function useTableStateReducer({ onColumnResize, onSortByChange, data }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial<ReactTableInternalState> {
|
function getInitialState(initialSortBy: Props['initialSortBy'], columns: Column[]): Partial<TableState> {
|
||||||
const state: Partial<ReactTableInternalState> = {};
|
const state: Partial<TableState> = {};
|
||||||
|
|
||||||
if (initialSortBy) {
|
if (initialSortBy) {
|
||||||
state.sortBy = [];
|
state.sortBy = [];
|
||||||
@ -126,6 +125,7 @@ export const Table: FC<Props> = memo((props: Props) => {
|
|||||||
initialSortBy,
|
initialSortBy,
|
||||||
footerValues,
|
footerValues,
|
||||||
showTypeIcons,
|
showTypeIcons,
|
||||||
|
enablePagination,
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
const tableStyles = useStyles2(getTableStyles);
|
const tableStyles = useStyles2(getTableStyles);
|
||||||
@ -188,17 +188,40 @@ export const Table: FC<Props> = memo((props: Props) => {
|
|||||||
[initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer]
|
[initialSortBy, memoizedColumns, memoizedData, resizable, stateReducer]
|
||||||
);
|
);
|
||||||
|
|
||||||
const { getTableProps, headerGroups, rows, prepareRow, totalColumnsWidth, footerGroups } = useTable(
|
const {
|
||||||
options,
|
getTableProps,
|
||||||
useFilters,
|
headerGroups,
|
||||||
useSortBy,
|
rows,
|
||||||
useAbsoluteLayout,
|
prepareRow,
|
||||||
useResizeColumns
|
totalColumnsWidth,
|
||||||
);
|
footerGroups,
|
||||||
|
page,
|
||||||
|
state,
|
||||||
|
gotoPage,
|
||||||
|
setPageSize,
|
||||||
|
pageOptions,
|
||||||
|
} = useTable(options, useFilters, useSortBy, usePagination, useAbsoluteLayout, useResizeColumns);
|
||||||
|
|
||||||
|
let listHeight = height - (headerHeight + footerHeight);
|
||||||
|
if (enablePagination) {
|
||||||
|
listHeight -= tableStyles.cellHeight;
|
||||||
|
}
|
||||||
|
const pageSize = Math.round(listHeight / tableStyles.cellHeight) - 1;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Don't update the page size if it is less than 1
|
||||||
|
if (pageSize <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPageSize(pageSize);
|
||||||
|
}, [pageSize, setPageSize]);
|
||||||
|
|
||||||
const RenderRow = React.useCallback(
|
const RenderRow = React.useCallback(
|
||||||
({ index: rowIndex, style }) => {
|
({ index: rowIndex, style }) => {
|
||||||
const row = rows[rowIndex];
|
let row = rows[rowIndex];
|
||||||
|
if (enablePagination) {
|
||||||
|
row = page[rowIndex];
|
||||||
|
}
|
||||||
prepareRow(row);
|
prepareRow(row);
|
||||||
return (
|
return (
|
||||||
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
||||||
@ -215,20 +238,52 @@ export const Table: FC<Props> = memo((props: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
[onCellFilterAdded, prepareRow, rows, tableStyles]
|
[onCellFilterAdded, page, enablePagination, prepareRow, rows, tableStyles]
|
||||||
);
|
);
|
||||||
|
|
||||||
const listHeight = height - (headerHeight + footerHeight);
|
const onNavigate = useCallback(
|
||||||
|
(toPage: number) => {
|
||||||
|
gotoPage(toPage - 1);
|
||||||
|
},
|
||||||
|
[gotoPage]
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemCount = enablePagination ? page.length : data.length;
|
||||||
|
let paginationEl = null;
|
||||||
|
if (enablePagination) {
|
||||||
|
const itemsRangeStart = state.pageIndex * state.pageSize + 1;
|
||||||
|
let itemsRangeEnd = itemsRangeStart + state.pageSize - 1;
|
||||||
|
const isSmall = width < 500;
|
||||||
|
if (itemsRangeEnd > data.length) {
|
||||||
|
itemsRangeEnd = data.length;
|
||||||
|
}
|
||||||
|
paginationEl = (
|
||||||
|
<div className={tableStyles.paginationWrapper}>
|
||||||
|
<div>
|
||||||
|
<Pagination
|
||||||
|
currentPage={state.pageIndex + 1}
|
||||||
|
numberOfPages={pageOptions.length}
|
||||||
|
showSmallVersion={isSmall}
|
||||||
|
onNavigate={onNavigate}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{isSmall ? null : (
|
||||||
|
<div className={tableStyles.paginationSummary}>
|
||||||
|
{itemsRangeStart} - {itemsRangeEnd} of {data.length} rows
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<div {...getTableProps()} className={tableStyles.table} aria-label={ariaLabel} role="table">
|
<div {...getTableProps()} className={tableStyles.table} aria-label={ariaLabel} role="table">
|
||||||
<CustomScrollbar hideVerticalTrack={true}>
|
<CustomScrollbar hideVerticalTrack={true}>
|
||||||
<div style={{ width: totalColumnsWidth ? `${totalColumnsWidth}px` : '100%' }}>
|
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
|
||||||
{!noHeader && <HeaderRow data={data} headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
|
{!noHeader && <HeaderRow data={data} headerGroups={headerGroups} showTypeIcons={showTypeIcons} />}
|
||||||
{rows.length > 0 ? (
|
{itemCount > 0 ? (
|
||||||
<FixedSizeList
|
<FixedSizeList
|
||||||
height={listHeight}
|
height={listHeight}
|
||||||
itemCount={rows.length}
|
itemCount={itemCount}
|
||||||
itemSize={tableStyles.rowHeight}
|
itemSize={tableStyles.rowHeight}
|
||||||
width={'100%'}
|
width={'100%'}
|
||||||
style={{ overflow: 'hidden auto' }}
|
style={{ overflow: 'hidden auto' }}
|
||||||
@ -243,11 +298,13 @@ export const Table: FC<Props> = memo((props: Props) => {
|
|||||||
{footerValues && (
|
{footerValues && (
|
||||||
<FooterRow
|
<FooterRow
|
||||||
height={footerHeight}
|
height={footerHeight}
|
||||||
|
isPaginationVisible={Boolean(enablePagination)}
|
||||||
footerValues={footerValues}
|
footerValues={footerValues}
|
||||||
footerGroups={footerGroups}
|
footerGroups={footerGroups}
|
||||||
totalColumnsWidth={totalColumnsWidth}
|
totalColumnsWidth={totalColumnsWidth}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{paginationEl}
|
||||||
</div>
|
</div>
|
||||||
</CustomScrollbar>
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
|
@ -170,6 +170,32 @@ export const getTableStyles = (theme: GrafanaTheme2) => {
|
|||||||
label: headerFilter;
|
label: headerFilter;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
`,
|
`,
|
||||||
|
paginationWrapper: css`
|
||||||
|
display: flex;
|
||||||
|
background: ${headerBg};
|
||||||
|
height: ${cellHeight}px;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
border-top: 1px solid ${theme.colors.border.weak};
|
||||||
|
li {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
div:not(:only-child):first-child {
|
||||||
|
flex-grow: 0.6;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
paginationSummary: css`
|
||||||
|
color: ${theme.colors.text.secondary};
|
||||||
|
font-size: ${theme.typography.bodySmall.fontSize};
|
||||||
|
margin-left: auto;
|
||||||
|
`,
|
||||||
|
|
||||||
|
tableContentWrapper: (totalColumnsWidth: number) => css`
|
||||||
|
width: ${totalColumnsWidth ?? '100%'};
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`,
|
||||||
row: css`
|
row: css`
|
||||||
label: row;
|
label: row;
|
||||||
border-bottom: 1px solid ${borderColor};
|
border-bottom: 1px solid ${borderColor};
|
||||||
|
111
packages/grafana-ui/src/types/react-table-config.d.ts
vendored
Normal file
111
packages/grafana-ui/src/types/react-table-config.d.ts
vendored
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
import type {
|
||||||
|
UseColumnOrderInstanceProps,
|
||||||
|
UseColumnOrderState,
|
||||||
|
UseExpandedHooks,
|
||||||
|
UseExpandedInstanceProps,
|
||||||
|
UseExpandedOptions,
|
||||||
|
UseExpandedRowProps,
|
||||||
|
UseExpandedState,
|
||||||
|
UseFiltersColumnOptions,
|
||||||
|
UseFiltersColumnProps,
|
||||||
|
UseFiltersInstanceProps,
|
||||||
|
UseFiltersOptions,
|
||||||
|
UseFiltersState,
|
||||||
|
UseGlobalFiltersColumnOptions,
|
||||||
|
UseGlobalFiltersInstanceProps,
|
||||||
|
UseGlobalFiltersOptions,
|
||||||
|
UseGlobalFiltersState,
|
||||||
|
UseGroupByCellProps,
|
||||||
|
UseGroupByColumnOptions,
|
||||||
|
UseGroupByColumnProps,
|
||||||
|
UseGroupByHooks,
|
||||||
|
UseGroupByInstanceProps,
|
||||||
|
UseGroupByOptions,
|
||||||
|
UseGroupByRowProps,
|
||||||
|
UseGroupByState,
|
||||||
|
UsePaginationInstanceProps,
|
||||||
|
UsePaginationOptions,
|
||||||
|
UsePaginationState,
|
||||||
|
UseResizeColumnsColumnOptions,
|
||||||
|
UseResizeColumnsColumnProps,
|
||||||
|
UseResizeColumnsOptions,
|
||||||
|
UseResizeColumnsState,
|
||||||
|
UseRowSelectHooks,
|
||||||
|
UseRowSelectInstanceProps,
|
||||||
|
UseRowSelectOptions,
|
||||||
|
UseRowSelectRowProps,
|
||||||
|
UseRowSelectState,
|
||||||
|
UseRowStateCellProps,
|
||||||
|
UseRowStateInstanceProps,
|
||||||
|
UseRowStateOptions,
|
||||||
|
UseRowStateRowProps,
|
||||||
|
UseRowStateState,
|
||||||
|
UseSortByColumnOptions,
|
||||||
|
UseSortByColumnProps,
|
||||||
|
UseSortByHooks,
|
||||||
|
UseSortByInstanceProps,
|
||||||
|
UseSortByOptions,
|
||||||
|
UseSortByState,
|
||||||
|
} from 'react-table';
|
||||||
|
|
||||||
|
declare module 'react-table' {
|
||||||
|
export interface TableOptions<D extends Record<string, unknown>>
|
||||||
|
extends UseExpandedOptions<D>,
|
||||||
|
UseFiltersOptions<D>,
|
||||||
|
UseGlobalFiltersOptions<D>,
|
||||||
|
UseGroupByOptions<D>,
|
||||||
|
UsePaginationOptions<D>,
|
||||||
|
UseResizeColumnsOptions<D>,
|
||||||
|
UseRowSelectOptions<D>,
|
||||||
|
UseRowStateOptions<D>,
|
||||||
|
UseSortByOptions<D>,
|
||||||
|
// note that having Record here allows you to add anything to the options, this matches the spirit of the
|
||||||
|
// underlying js library, but might be cleaner if it's replaced by a more specific type that matches your
|
||||||
|
// feature set, this is a safe default.
|
||||||
|
Record<string, any> {}
|
||||||
|
|
||||||
|
export interface Hooks<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
extends UseExpandedHooks<D>,
|
||||||
|
UseGroupByHooks<D>,
|
||||||
|
UseRowSelectHooks<D>,
|
||||||
|
UseSortByHooks<D> {}
|
||||||
|
|
||||||
|
export interface TableInstance<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
extends UseColumnOrderInstanceProps<D>,
|
||||||
|
UseExpandedInstanceProps<D>,
|
||||||
|
UseFiltersInstanceProps<D>,
|
||||||
|
UseGlobalFiltersInstanceProps<D>,
|
||||||
|
UseGroupByInstanceProps<D>,
|
||||||
|
UsePaginationInstanceProps<D>,
|
||||||
|
UseRowSelectInstanceProps<D>,
|
||||||
|
UseRowStateInstanceProps<D>,
|
||||||
|
UseSortByInstanceProps<D> {}
|
||||||
|
|
||||||
|
export interface TableState<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
extends UseColumnOrderState<D>,
|
||||||
|
UseExpandedState<D>,
|
||||||
|
UseFiltersState<D>,
|
||||||
|
UseGlobalFiltersState<D>,
|
||||||
|
UseGroupByState<D>,
|
||||||
|
UsePaginationState<D>,
|
||||||
|
UseResizeColumnsState<D>,
|
||||||
|
UseRowSelectState<D>,
|
||||||
|
UseRowStateState<D>,
|
||||||
|
UseSortByState<D> {}
|
||||||
|
|
||||||
|
export interface ColumnInterface<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
extends UseGlobalFiltersColumnOptions<D>,
|
||||||
|
UseGroupByColumnOptions<D>,
|
||||||
|
UseResizeColumnsColumnOptions<D>,
|
||||||
|
UseSortByColumnOptions<D> {}
|
||||||
|
|
||||||
|
export interface ColumnInstance<D extends Record<string, unknown> = Record<string, unknown>>
|
||||||
|
extends UseFiltersColumnProps<D>,
|
||||||
|
UseGroupByColumnProps<D>,
|
||||||
|
UseResizeColumnsColumnProps<D>,
|
||||||
|
UseSortByColumnProps<D> {}
|
||||||
|
|
||||||
|
export interface Cell<D extends Record<string, unknown> = Record<string, unknown>, V = any>
|
||||||
|
extends UseGroupByCellProps<D>,
|
||||||
|
UseRowStateCellProps<D> {}
|
||||||
|
}
|
@ -6,5 +6,11 @@
|
|||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules"],
|
"exclude": ["dist", "node_modules"],
|
||||||
"extends": "@grafana/tsconfig",
|
"extends": "@grafana/tsconfig",
|
||||||
"include": ["src/**/*.ts*", "typings", "../../public/app/types/jquery/*.ts", "../../public/app/types/*.d.ts"]
|
"include": [
|
||||||
|
"src/**/*.ts*",
|
||||||
|
"typings",
|
||||||
|
"../../public/app/types/jquery/*.ts",
|
||||||
|
"../../public/app/types/*.d.ts",
|
||||||
|
"../grafana-ui/src/types/*.d.ts"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
14
public/app/plugins/panel/table/PaginationEditor.tsx
Normal file
14
public/app/plugins/panel/table/PaginationEditor.tsx
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
import { StandardEditorProps } from '@grafana/data';
|
||||||
|
import { Switch } from '@grafana/ui';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export function PaginationEditor({ onChange, value, context }: StandardEditorProps<boolean>) {
|
||||||
|
const changeValue = (event: React.FormEvent<HTMLInputElement> | undefined) => {
|
||||||
|
if (event?.currentTarget.checked) {
|
||||||
|
context.options.footer.show = false;
|
||||||
|
}
|
||||||
|
onChange(event?.currentTarget.checked);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <Switch value={Boolean(value)} onChange={changeValue} />;
|
||||||
|
}
|
@ -108,6 +108,7 @@ export class TablePanel extends Component<Props> {
|
|||||||
onColumnResize={this.onColumnResize}
|
onColumnResize={this.onColumnResize}
|
||||||
onCellFilterAdded={this.onCellFilterAdded}
|
onCellFilterAdded={this.onCellFilterAdded}
|
||||||
footerValues={footerValues}
|
footerValues={footerValues}
|
||||||
|
enablePagination={options.footer?.enablePagination}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -24,6 +24,7 @@ export interface TableFooterCalc {
|
|||||||
show: boolean;
|
show: boolean;
|
||||||
reducer: string[]; // actually 1 value
|
reducer: string[]; // actually 1 value
|
||||||
fields?: string[];
|
fields?: string[];
|
||||||
|
enablePagination?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultPanelOptions: PanelOptions = {
|
export const defaultPanelOptions: PanelOptions = {
|
||||||
|
@ -12,6 +12,7 @@ import { TableFieldOptions } from '@grafana/schema';
|
|||||||
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
|
import { tableMigrationHandler, tablePanelChangedHandler } from './migrations';
|
||||||
import { TableCellDisplayMode } from '@grafana/ui';
|
import { TableCellDisplayMode } from '@grafana/ui';
|
||||||
import { TableSuggestionsSupplier } from './suggestions';
|
import { TableSuggestionsSupplier } from './suggestions';
|
||||||
|
import { PaginationEditor } from './PaginationEditor';
|
||||||
|
|
||||||
export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePanel)
|
export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePanel)
|
||||||
.setPanelChangeHandler(tablePanelChangedHandler)
|
.setPanelChangeHandler(tablePanelChangedHandler)
|
||||||
@ -108,18 +109,21 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
|
|||||||
builder
|
builder
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'showHeader',
|
path: 'showHeader',
|
||||||
|
category: ['Header and footer'],
|
||||||
name: 'Show header',
|
name: 'Show header',
|
||||||
description: "To display table's header or not to display",
|
description: "To display table's header or not to display",
|
||||||
defaultValue: defaultPanelOptions.showHeader,
|
defaultValue: defaultPanelOptions.showHeader,
|
||||||
})
|
})
|
||||||
.addBooleanSwitch({
|
.addBooleanSwitch({
|
||||||
path: 'footer.show',
|
path: 'footer.show',
|
||||||
|
category: ['Header and footer'],
|
||||||
name: 'Show Footer',
|
name: 'Show Footer',
|
||||||
description: "To display table's footer or not to display",
|
description: "To display table's footer or not to display",
|
||||||
defaultValue: defaultPanelOptions.footer?.show,
|
defaultValue: defaultPanelOptions.footer?.show,
|
||||||
})
|
})
|
||||||
.addCustomEditor({
|
.addCustomEditor({
|
||||||
id: 'footer.reducer',
|
id: 'footer.reducer',
|
||||||
|
category: ['Header and footer'],
|
||||||
path: 'footer.reducer',
|
path: 'footer.reducer',
|
||||||
name: 'Calculation',
|
name: 'Calculation',
|
||||||
description: 'Choose a reducer function / calculation',
|
description: 'Choose a reducer function / calculation',
|
||||||
@ -129,6 +133,7 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
|
|||||||
})
|
})
|
||||||
.addMultiSelect({
|
.addMultiSelect({
|
||||||
path: 'footer.fields',
|
path: 'footer.fields',
|
||||||
|
category: ['Header and footer'],
|
||||||
name: 'Fields',
|
name: 'Fields',
|
||||||
description: 'Select the fields that should be calculated',
|
description: 'Select the fields that should be calculated',
|
||||||
settings: {
|
settings: {
|
||||||
@ -152,6 +157,13 @@ export const plugin = new PanelPlugin<PanelOptions, TableFieldOptions>(TablePane
|
|||||||
},
|
},
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
showIf: (cfg) => cfg.footer?.show,
|
showIf: (cfg) => cfg.footer?.show,
|
||||||
|
})
|
||||||
|
.addCustomEditor({
|
||||||
|
id: 'footer.enablePagination',
|
||||||
|
category: ['Header and footer'],
|
||||||
|
path: 'footer.enablePagination',
|
||||||
|
name: 'Enable pagination',
|
||||||
|
editor: PaginationEditor,
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.setSuggestionsSupplier(new TableSuggestionsSupplier());
|
.setSuggestionsSupplier(new TableSuggestionsSupplier());
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
"public/test/**/*.ts",
|
"public/test/**/*.ts",
|
||||||
"public/vendor/**/*.ts",
|
"public/vendor/**/*.ts",
|
||||||
"packages/jaeger-ui-components/typings",
|
"packages/jaeger-ui-components/typings",
|
||||||
"packages/grafana-data/typings"
|
"packages/grafana-data/typings",
|
||||||
|
"packages/grafana-ui/src/types"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user