mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Panel inspect: Horizontal scrolling in Data table (#22245)
* First try at horizontal scrolling * move width logic to table * Update packages/grafana-ui/src/components/Table/Table.tsx Co-Authored-By: Dominik Prokop <dominik.prokop@grafana.com> * wrap table with memo * fix typo * re add field * WIP: Table scrolling troubles * Annother approach * Think it's working * Removed unnessary change * Table: Added custom scrollbar for horizontal scrolling * Removed console log and fixed test Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com> Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
This commit is contained in:
parent
066d5cf4da
commit
3f7b058bea
@ -1,4 +1,4 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { FC, memo, useMemo } from 'react';
|
||||||
import { DataFrame, Field } from '@grafana/data';
|
import { DataFrame, Field } from '@grafana/data';
|
||||||
import { useSortBy, useTable, useBlockLayout, Cell } from 'react-table';
|
import { useSortBy, useTable, useBlockLayout, Cell } from 'react-table';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
@ -10,22 +10,25 @@ import { getTableStyles } from './styles';
|
|||||||
import { TableCell } from './TableCell';
|
import { TableCell } from './TableCell';
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { getTextAlign } from './utils';
|
import { getTextAlign } from './utils';
|
||||||
|
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
data: DataFrame;
|
data: DataFrame;
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
|
/** Minimal column width specified in pixels */
|
||||||
|
columnMinWidth?: number;
|
||||||
onCellClick?: TableFilterActionCallback;
|
onCellClick?: TableFilterActionCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Table = ({ data, height, onCellClick, width }: Props) => {
|
export const Table: FC<Props> = memo(({ data, height, onCellClick, width, columnMinWidth }) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const [ref, headerRowMeasurements] = useMeasure();
|
const [ref, headerRowMeasurements] = useMeasure();
|
||||||
const tableStyles = getTableStyles(theme);
|
const tableStyles = getTableStyles(theme);
|
||||||
|
|
||||||
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
|
const { getTableProps, headerGroups, rows, prepareRow } = useTable(
|
||||||
{
|
{
|
||||||
columns: useMemo(() => getColumns(data, width), [data]),
|
columns: useMemo(() => getColumns(data, width, columnMinWidth ?? 150), [data, width, columnMinWidth]),
|
||||||
data: useMemo(() => getTableRows(data), [data]),
|
data: useMemo(() => getTableRows(data), [data]),
|
||||||
},
|
},
|
||||||
useSortBy,
|
useSortBy,
|
||||||
@ -53,28 +56,39 @@ export const Table = ({ data, height, onCellClick, width }: Props) => {
|
|||||||
[prepareRow, rows]
|
[prepareRow, rows]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let totalWidth = 0;
|
||||||
|
|
||||||
|
for (const headerGroup of headerGroups) {
|
||||||
|
for (const header of headerGroup.headers) {
|
||||||
|
totalWidth += header.width as number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...getTableProps()} className={tableStyles.table}>
|
<div {...getTableProps()} className={tableStyles.table}>
|
||||||
<div>
|
<CustomScrollbar>
|
||||||
{headerGroups.map((headerGroup: any) => (
|
<div>
|
||||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
{headerGroups.map((headerGroup: any) => (
|
||||||
{headerGroup.headers.map((column: any) =>
|
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
||||||
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
{headerGroup.headers.map((column: any) =>
|
||||||
)}
|
renderHeaderCell(column, tableStyles.headerCell, data.fields[column.index])
|
||||||
</div>
|
)}
|
||||||
))}
|
</div>
|
||||||
</div>
|
))}
|
||||||
<FixedSizeList
|
</div>
|
||||||
height={height - headerRowMeasurements.height}
|
<FixedSizeList
|
||||||
itemCount={rows.length}
|
height={height - headerRowMeasurements.height}
|
||||||
itemSize={tableStyles.rowHeight}
|
itemCount={rows.length}
|
||||||
width={width}
|
itemSize={tableStyles.rowHeight}
|
||||||
>
|
width={totalWidth ?? width}
|
||||||
{RenderRow}
|
style={{ overflow: 'hidden auto' }}
|
||||||
</FixedSizeList>
|
>
|
||||||
|
{RenderRow}
|
||||||
|
</FixedSizeList>
|
||||||
|
</CustomScrollbar>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
function renderHeaderCell(column: any, className: string, field: Field) {
|
function renderHeaderCell(column: any, className: string, field: Field) {
|
||||||
const headerProps = column.getHeaderProps(column.getSortByToggleProps());
|
const headerProps = column.getHeaderProps(column.getSortByToggleProps());
|
||||||
|
@ -31,8 +31,10 @@ export const getTableStyles = stylesFactory(
|
|||||||
cellHeightInner: bodyFontSize * lineHeight,
|
cellHeightInner: bodyFontSize * lineHeight,
|
||||||
rowHeight: cellHeight + 2,
|
rowHeight: cellHeight + 2,
|
||||||
table: css`
|
table: css`
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
border-spacing: 0;
|
display: flex;
|
||||||
`,
|
`,
|
||||||
thead: css`
|
thead: css`
|
||||||
label: thead;
|
label: thead;
|
||||||
|
@ -40,12 +40,13 @@ describe('Table utils', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('Should distribute width and use field config width', () => {
|
it('Should distribute width and use field config width', () => {
|
||||||
const columns = getColumns(getData(), 1000);
|
const columns = getColumns(getData(), 1000, 120);
|
||||||
|
|
||||||
expect(columns[0].width).toBe(450);
|
expect(columns[0].width).toBe(450);
|
||||||
expect(columns[1].width).toBe(100);
|
expect(columns[1].width).toBe(100);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getTextAlign', () => {
|
describe('getTextAlign', () => {
|
||||||
it('Should use textAlign from custom', () => {
|
it('Should use textAlign from custom', () => {
|
||||||
const data = getData();
|
const data = getData();
|
||||||
|
@ -42,7 +42,7 @@ export function getTextAlign(field: Field): TextAlignProperty {
|
|||||||
return 'left';
|
return 'left';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColumns(data: DataFrame, availableWidth: number): Column[] {
|
export function getColumns(data: DataFrame, availableWidth: number, columnMinWidth: number): Column[] {
|
||||||
const columns: Column[] = [];
|
const columns: Column[] = [];
|
||||||
let fieldCountWithoutWidth = data.fields.length;
|
let fieldCountWithoutWidth = data.fields.length;
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ export function getColumns(data: DataFrame, availableWidth: number): Column[] {
|
|||||||
const sharedWidth = availableWidth / fieldCountWithoutWidth;
|
const sharedWidth = availableWidth / fieldCountWithoutWidth;
|
||||||
for (const column of columns) {
|
for (const column of columns) {
|
||||||
if (!column.width) {
|
if (!column.width) {
|
||||||
column.width = sharedWidth;
|
column.width = Math.max(sharedWidth, columnMinWidth);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -219,6 +219,7 @@ export class PanelInspector extends PureComponent<Props, State> {
|
|||||||
if (width === 0) {
|
if (width === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{ width, height }}>
|
<div style={{ width, height }}>
|
||||||
<Table width={width} height={height} data={processed[selected]} />
|
<Table width={width} height={height} data={processed[selected]} />
|
||||||
|
Loading…
Reference in New Issue
Block a user