mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: Improvements to column resizing, style and alignment (#23663)
* Table: Fixed to column alignment * testing table state reducer * Styles starting to work * Persisting column resize now works * Trying to fix Table storybook stories * Minor updates * fixed ts issue * Table: Support duplicate field names, and use data frame directly instead of copying data and other improvements (#23681) * Poc at use data frame directly * working ok * Table improvements
This commit is contained in:
parent
3aa8eb0176
commit
56a7de562e
@ -9,9 +9,8 @@ import {
|
||||
GrafanaTheme,
|
||||
TimeZone,
|
||||
} from '../types';
|
||||
import { Registry } from '../utils/Registry';
|
||||
import { InterpolateFunction } from './panel';
|
||||
import { StandardEditorProps } from '../field';
|
||||
import { StandardEditorProps, FieldConfigOptionsRegistry } from '../field';
|
||||
import { OptionsEditorItem } from './OptionsUIRegistryBuilder';
|
||||
|
||||
export interface DynamicConfigValue {
|
||||
@ -122,7 +121,7 @@ export interface ApplyFieldOverrideOptions {
|
||||
theme: GrafanaTheme;
|
||||
timeZone?: TimeZone;
|
||||
autoMinMax?: boolean;
|
||||
fieldConfigRegistry?: Registry<FieldConfigPropertyItem>;
|
||||
fieldConfigRegistry?: FieldConfigOptionsRegistry;
|
||||
}
|
||||
|
||||
export enum FieldConfigProperty {
|
||||
|
@ -13,7 +13,7 @@
|
||||
|
||||
.track-vertical {
|
||||
border-radius: 3px;
|
||||
width: 6px !important;
|
||||
width: 8px !important;
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
top: 2px;
|
||||
@ -21,7 +21,7 @@
|
||||
|
||||
.track-horizontal {
|
||||
border-radius: 3px;
|
||||
height: 6px !important;
|
||||
height: 8px !important;
|
||||
|
||||
right: 2px;
|
||||
bottom: 2px;
|
||||
|
@ -52,6 +52,7 @@ export const BarGaugeCell: FC<TableCellProps> = props => {
|
||||
width={width}
|
||||
height={tableStyles.cellHeightInner}
|
||||
field={config}
|
||||
display={field.display}
|
||||
value={displayValue}
|
||||
orientation={VizOrientation.Horizontal}
|
||||
theme={tableStyles.theme}
|
||||
|
@ -1,4 +1,5 @@
|
||||
import React from 'react';
|
||||
import { merge } from 'lodash';
|
||||
import { Table } from './Table';
|
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
@ -6,14 +7,13 @@ import { useTheme } from '../../themes';
|
||||
import mdx from './Table.mdx';
|
||||
import {
|
||||
applyFieldOverrides,
|
||||
ConfigOverrideRule,
|
||||
DataFrame,
|
||||
FieldMatcherID,
|
||||
FieldType,
|
||||
GrafanaTheme,
|
||||
MutableDataFrame,
|
||||
ThresholdsConfig,
|
||||
ThresholdsMode,
|
||||
FieldConfig,
|
||||
} from '@grafana/data';
|
||||
|
||||
export default {
|
||||
@ -27,7 +27,7 @@ export default {
|
||||
},
|
||||
};
|
||||
|
||||
function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFrame {
|
||||
function buildData(theme: GrafanaTheme, config: Record<string, FieldConfig>): DataFrame {
|
||||
const data = new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'Time', type: FieldType.time, values: [] }, // The time field
|
||||
@ -39,6 +39,7 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
|
||||
decimals: 0,
|
||||
custom: {
|
||||
align: 'center',
|
||||
width: 80,
|
||||
},
|
||||
},
|
||||
},
|
||||
@ -57,14 +58,20 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
|
||||
values: [],
|
||||
config: {
|
||||
unit: 'percent',
|
||||
min: 0,
|
||||
max: 100,
|
||||
custom: {
|
||||
width: 100,
|
||||
width: 150,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
for (const field of data.fields) {
|
||||
field.config = merge(field.config, config[field.name]);
|
||||
}
|
||||
|
||||
for (let i = 0; i < 1000; i++) {
|
||||
data.appendRow([
|
||||
new Date().getTime(),
|
||||
@ -78,7 +85,7 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
|
||||
return applyFieldOverrides({
|
||||
data: [data],
|
||||
fieldConfig: {
|
||||
overrides,
|
||||
overrides: [],
|
||||
defaults: {},
|
||||
},
|
||||
theme,
|
||||
@ -86,40 +93,6 @@ function buildData(theme: GrafanaTheme, overrides: ConfigOverrideRule[]): DataFr
|
||||
})[0];
|
||||
}
|
||||
|
||||
export const Simple = () => {
|
||||
const theme = useTheme();
|
||||
const width = number('width', 700, {}, 'Props');
|
||||
const data = buildData(theme, []);
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto' }}>
|
||||
<Table data={data} height={500} width={width} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BarGaugeCell = () => {
|
||||
const theme = useTheme();
|
||||
const width = number('width', 700, {}, 'Props');
|
||||
const data = buildData(theme, [
|
||||
{
|
||||
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
|
||||
properties: [
|
||||
{ id: 'width', value: '200' },
|
||||
{ id: 'displayMode', value: 'gradient-gauge' },
|
||||
{ id: 'min', value: '0' },
|
||||
{ id: 'max', value: '100' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto' }}>
|
||||
<Table data={data} height={500} width={width} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const defaultThresholds: ThresholdsConfig = {
|
||||
steps: [
|
||||
{
|
||||
@ -134,21 +107,50 @@ const defaultThresholds: ThresholdsConfig = {
|
||||
mode: ThresholdsMode.Absolute,
|
||||
};
|
||||
|
||||
export const ColoredCells = () => {
|
||||
export const Simple = () => {
|
||||
const theme = useTheme();
|
||||
const width = number('width', 750, {}, 'Props');
|
||||
const data = buildData(theme, [
|
||||
{
|
||||
matcher: { id: FieldMatcherID.byName, options: 'Progress' },
|
||||
properties: [
|
||||
{ id: 'width', value: '80' },
|
||||
{ id: 'displayMode', value: 'color-background' },
|
||||
{ id: 'min', value: '0' },
|
||||
{ id: 'max', value: '100' },
|
||||
{ id: 'thresholds', value: defaultThresholds },
|
||||
],
|
||||
},
|
||||
]);
|
||||
const width = number('width', 700, {}, 'Props');
|
||||
const data = buildData(theme, {});
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto' }}>
|
||||
<Table data={data} height={500} width={width} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const BarGaugeCell = () => {
|
||||
const theme = useTheme();
|
||||
const width = number('width', 700, {}, 'Props');
|
||||
const data = buildData(theme, {
|
||||
Progress: {
|
||||
custom: {
|
||||
width: 200,
|
||||
displayMode: 'gradient-gauge',
|
||||
},
|
||||
thresholds: defaultThresholds,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto' }}>
|
||||
<Table data={data} height={500} width={width} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ColoredCells = () => {
|
||||
const theme = useTheme();
|
||||
const width = number('width', 750, {}, 'Props');
|
||||
const data = buildData(theme, {
|
||||
Progress: {
|
||||
custom: {
|
||||
width: 80,
|
||||
displayMode: 'color-background',
|
||||
},
|
||||
thresholds: defaultThresholds,
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="panel-container" style={{ width: 'auto' }}>
|
||||
|
@ -1,11 +1,20 @@
|
||||
import React, { FC, memo, useMemo } from 'react';
|
||||
import React, { FC, memo, useMemo, useCallback } from 'react';
|
||||
import { DataFrame, Field } from '@grafana/data';
|
||||
import { Cell, Column, HeaderGroup, useBlockLayout, useResizeColumns, useSortBy, useTable } from 'react-table';
|
||||
import {
|
||||
Cell,
|
||||
Column,
|
||||
HeaderGroup,
|
||||
useAbsoluteLayout,
|
||||
useResizeColumns,
|
||||
useSortBy,
|
||||
useTable,
|
||||
UseResizeColumnsState,
|
||||
UseSortByState,
|
||||
} from 'react-table';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import useMeasure from 'react-use/lib/useMeasure';
|
||||
import { getColumns, getTableRows, getTextAlign } from './utils';
|
||||
import { getColumns, getTextAlign } from './utils';
|
||||
import { useTheme } from '../../themes';
|
||||
import { ColumnResizeActionCallback, TableFilterActionCallback } from './types';
|
||||
import { TableColumnResizeActionCallback, TableFilterActionCallback, TableSortByActionCallback } from './types';
|
||||
import { getTableStyles, TableStyles } from './styles';
|
||||
import { TableCell } from './TableCell';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
@ -22,109 +31,142 @@ export interface Props {
|
||||
noHeader?: boolean;
|
||||
resizable?: boolean;
|
||||
onCellClick?: TableFilterActionCallback;
|
||||
onColumnResize?: ColumnResizeActionCallback;
|
||||
onColumnResize?: TableColumnResizeActionCallback;
|
||||
onSortBy?: TableSortByActionCallback;
|
||||
}
|
||||
|
||||
export const Table: FC<Props> = memo(
|
||||
({ data, height, onCellClick, width, columnMinWidth = COLUMN_MIN_WIDTH, noHeader, resizable = false }) => {
|
||||
const theme = useTheme();
|
||||
const [ref, headerRowMeasurements] = useMeasure();
|
||||
const tableStyles = getTableStyles(theme);
|
||||
const memoizedColumns = useMemo(() => getColumns(data, width, columnMinWidth), [data, width, columnMinWidth]);
|
||||
const memoizedData = useMemo(() => getTableRows(data), [data]);
|
||||
interface ReactTableInternalState extends UseResizeColumnsState<{}>, UseSortByState<{}> {}
|
||||
|
||||
const defaultColumn = React.useMemo(
|
||||
() => ({
|
||||
minWidth: memoizedColumns.reduce((minWidth, column) => {
|
||||
if (column.width) {
|
||||
const width = typeof column.width === 'string' ? parseInt(column.width, 10) : column.width;
|
||||
return Math.min(minWidth, width);
|
||||
function useTableStateReducer(props: Props) {
|
||||
return useCallback(
|
||||
(newState: ReactTableInternalState, action: any) => {
|
||||
console.log(action, newState);
|
||||
|
||||
switch (action.type) {
|
||||
case 'columnDoneResizing':
|
||||
if (props.onColumnResize) {
|
||||
const info = (newState.columnResizing.headerIdWidths as any)[0];
|
||||
const columnIdString = info[0];
|
||||
const fieldIndex = parseInt(columnIdString, 10);
|
||||
const width = Math.round(newState.columnResizing.columnWidths[columnIdString] as number);
|
||||
props.onColumnResize(fieldIndex, width);
|
||||
}
|
||||
return minWidth;
|
||||
}, columnMinWidth),
|
||||
}),
|
||||
[columnMinWidth, memoizedColumns]
|
||||
);
|
||||
case 'toggleSortBy':
|
||||
if (props.onSortBy) {
|
||||
// todo call callback and persist
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
const options: any = useMemo(
|
||||
() => ({
|
||||
columns: memoizedColumns,
|
||||
data: memoizedData,
|
||||
disableResizing: !resizable,
|
||||
defaultColumn,
|
||||
}),
|
||||
[memoizedColumns, memoizedData, resizable, defaultColumn]
|
||||
);
|
||||
return newState;
|
||||
},
|
||||
[props.onColumnResize]
|
||||
);
|
||||
}
|
||||
|
||||
const { getTableProps, headerGroups, rows, prepareRow, totalColumnsWidth } = useTable(
|
||||
options,
|
||||
useBlockLayout,
|
||||
useResizeColumns,
|
||||
useSortBy
|
||||
);
|
||||
export const Table: FC<Props> = memo((props: Props) => {
|
||||
const { data, height, onCellClick, width, columnMinWidth = COLUMN_MIN_WIDTH, noHeader, resizable = true } = props;
|
||||
const theme = useTheme();
|
||||
const tableStyles = getTableStyles(theme);
|
||||
|
||||
const RenderRow = React.useCallback(
|
||||
({ index, style }) => {
|
||||
const row = rows[index];
|
||||
prepareRow(row);
|
||||
return (
|
||||
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
||||
{row.cells.map((cell: Cell, index: number) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
field={data.fields[index]}
|
||||
tableStyles={tableStyles}
|
||||
cell={cell}
|
||||
onCellClick={onCellClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[prepareRow, rows]
|
||||
);
|
||||
// React table data array. This data acts just like a dummy array to let react-table know how many rows exist
|
||||
// The cells use the field to look up values
|
||||
const memoizedData = useMemo(() => {
|
||||
return data.fields.length > 0 ? data.fields[0].values.toArray() : [];
|
||||
}, [data]);
|
||||
|
||||
return (
|
||||
<div {...getTableProps()} className={tableStyles.table}>
|
||||
<CustomScrollbar hideVerticalTrack={true}>
|
||||
<div style={{ width: `${totalColumnsWidth}px` }}>
|
||||
{!noHeader && (
|
||||
<div>
|
||||
{headerGroups.map((headerGroup: HeaderGroup) => {
|
||||
return (
|
||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()} ref={ref}>
|
||||
{headerGroup.headers.map((column: Column, index: number) =>
|
||||
renderHeaderCell(column, tableStyles, data.fields[index])
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<FixedSizeList
|
||||
height={height - headerRowMeasurements.height}
|
||||
itemCount={rows.length}
|
||||
itemSize={tableStyles.rowHeight}
|
||||
width={'100%'}
|
||||
style={{ overflow: 'hidden auto' }}
|
||||
>
|
||||
{RenderRow}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
// React-table column definitions
|
||||
const memoizedColumns = useMemo(() => getColumns(data, width, columnMinWidth), [data, width, columnMinWidth]);
|
||||
|
||||
// Internal react table state reducer
|
||||
const stateReducer = useTableStateReducer(props);
|
||||
|
||||
const options: any = useMemo(
|
||||
() => ({
|
||||
columns: memoizedColumns,
|
||||
data: memoizedData,
|
||||
disableResizing: !resizable,
|
||||
stateReducer: stateReducer,
|
||||
// this is how you set initial sort by state
|
||||
// initialState: {
|
||||
// sortBy: [{ id: '2', desc: true }],
|
||||
// },
|
||||
}),
|
||||
[memoizedColumns, memoizedData, stateReducer, resizable]
|
||||
);
|
||||
|
||||
const { getTableProps, headerGroups, rows, prepareRow, totalColumnsWidth } = useTable(
|
||||
options,
|
||||
useSortBy,
|
||||
useAbsoluteLayout,
|
||||
useResizeColumns
|
||||
);
|
||||
|
||||
const RenderRow = React.useCallback(
|
||||
({ index, style }) => {
|
||||
const row = rows[index];
|
||||
prepareRow(row);
|
||||
return (
|
||||
<div {...row.getRowProps({ style })} className={tableStyles.row}>
|
||||
{row.cells.map((cell: Cell, index: number) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
field={data.fields[index]}
|
||||
tableStyles={tableStyles}
|
||||
cell={cell}
|
||||
onCellClick={onCellClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[prepareRow, rows]
|
||||
);
|
||||
|
||||
const headerHeight = noHeader ? 0 : tableStyles.cellHeight;
|
||||
|
||||
return (
|
||||
<div {...getTableProps()} className={tableStyles.table}>
|
||||
<CustomScrollbar hideVerticalTrack={true}>
|
||||
<div style={{ width: `${totalColumnsWidth}px` }}>
|
||||
{!noHeader && (
|
||||
<div>
|
||||
{headerGroups.map((headerGroup: HeaderGroup) => {
|
||||
return (
|
||||
<div className={tableStyles.thead} {...headerGroup.getHeaderGroupProps()}>
|
||||
{headerGroup.headers.map((column: Column, index: number) =>
|
||||
renderHeaderCell(column, tableStyles, data.fields[index])
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<FixedSizeList
|
||||
height={height - headerHeight}
|
||||
itemCount={rows.length}
|
||||
itemSize={tableStyles.rowHeight}
|
||||
width={'100%'}
|
||||
style={{ overflow: 'hidden auto' }}
|
||||
>
|
||||
{RenderRow}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
</CustomScrollbar>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
Table.displayName = 'Table';
|
||||
|
||||
function renderHeaderCell(column: any, tableStyles: TableStyles, field?: Field) {
|
||||
const headerProps = column.getHeaderProps();
|
||||
|
||||
if (column.canResize) {
|
||||
headerProps.style.userSelect = column.isResizing ? 'none' : 'auto'; // disables selecting text while resizing
|
||||
}
|
||||
|
||||
headerProps.style.position = 'absolute';
|
||||
headerProps.style.textAlign = getTextAlign(field);
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { css } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { stylesFactory } from '../../themes';
|
||||
import { stylesFactory, styleMixins } from '../../themes';
|
||||
|
||||
export interface TableStyles {
|
||||
cellHeight: number;
|
||||
@ -20,13 +20,14 @@ export interface TableStyles {
|
||||
export const getTableStyles = stylesFactory(
|
||||
(theme: GrafanaTheme): TableStyles => {
|
||||
const { palette, colors } = theme;
|
||||
const headerBg = theme.colors.panelBorder;
|
||||
const headerBorderColor = theme.isLight ? palette.gray70 : palette.gray05;
|
||||
const resizerColor = theme.isLight ? palette.blue77 : palette.blue95;
|
||||
const headerBg = theme.colors.bg2;
|
||||
const borderColor = theme.colors.border1;
|
||||
const resizerColor = theme.isLight ? palette.blue95 : palette.blue77;
|
||||
const padding = 6;
|
||||
const lineHeight = theme.typography.lineHeight.md;
|
||||
const bodyFontSize = 14;
|
||||
const cellHeight = padding * 2 + bodyFontSize * lineHeight;
|
||||
const rowHoverBg = styleMixins.hoverColor(theme.colors.bg1, theme);
|
||||
|
||||
return {
|
||||
theme,
|
||||
@ -42,6 +43,7 @@ export const getTableStyles = stylesFactory(
|
||||
`,
|
||||
thead: css`
|
||||
label: thead;
|
||||
height: ${cellHeight}px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: ${headerBg};
|
||||
@ -52,7 +54,7 @@ export const getTableStyles = stylesFactory(
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
color: ${colors.textBlue};
|
||||
border-right: 1px solid ${headerBorderColor};
|
||||
border-right: 1px solid ${theme.colors.panelBg};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@ -60,10 +62,14 @@ export const getTableStyles = stylesFactory(
|
||||
`,
|
||||
row: css`
|
||||
label: row;
|
||||
border-bottom: 1px solid ${headerBg};
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
|
||||
&:hover {
|
||||
background-color: ${rowHoverBg};
|
||||
}
|
||||
`,
|
||||
tableCellWrapper: css`
|
||||
border-right: 1px solid ${headerBg};
|
||||
border-right: 1px solid ${borderColor};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
@ -79,13 +85,14 @@ export const getTableStyles = stylesFactory(
|
||||
label: resizeHandle;
|
||||
cursor: col-resize !important;
|
||||
display: inline-block;
|
||||
border-right: 2px solid ${resizerColor};
|
||||
background: ${resizerColor};
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
width: 10px;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
right: -4px;
|
||||
border-radius: 3px;
|
||||
top: 0;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
touch-action: none;
|
||||
|
@ -24,7 +24,13 @@ export interface TableRow {
|
||||
}
|
||||
|
||||
export type TableFilterActionCallback = (key: string, value: string) => void;
|
||||
export type ColumnResizeActionCallback = (field: Field, width: number) => void;
|
||||
export type TableColumnResizeActionCallback = (fieldIndex: number, width: number) => void;
|
||||
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
|
||||
|
||||
export interface TableSortByFieldState {
|
||||
fieldIndex: number;
|
||||
desc?: boolean;
|
||||
}
|
||||
|
||||
export interface TableCellProps extends CellProps<any> {
|
||||
tableStyles: TableStyles;
|
||||
|
@ -3,26 +3,11 @@ import { DataFrame, Field, FieldType } from '@grafana/data';
|
||||
import { Column } from 'react-table';
|
||||
import { DefaultCell } from './DefaultCell';
|
||||
import { BarGaugeCell } from './BarGaugeCell';
|
||||
import { TableCellDisplayMode, TableCellProps, TableFieldOptions, TableRow } from './types';
|
||||
import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types';
|
||||
import { css, cx } from 'emotion';
|
||||
import { withTableStyles } from './withTableStyles';
|
||||
import tinycolor from 'tinycolor2';
|
||||
|
||||
export function getTableRows(data: DataFrame): TableRow[] {
|
||||
const tableData = [];
|
||||
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const row: { [key: string]: string | number } = {};
|
||||
for (let j = 0; j < data.fields.length; j++) {
|
||||
const prop = data.fields[j].name;
|
||||
row[prop] = data.fields[j].values.get(i);
|
||||
}
|
||||
tableData.push(row);
|
||||
}
|
||||
|
||||
return tableData;
|
||||
}
|
||||
|
||||
export function getTextAlign(field?: Field): TextAlignProperty {
|
||||
if (!field) {
|
||||
return 'left';
|
||||
@ -52,8 +37,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||
const columns: Column[] = [];
|
||||
let fieldCountWithoutWidth = data.fields.length;
|
||||
|
||||
for (const field of data.fields) {
|
||||
for (let fieldIndex = 0; fieldIndex < data.fields.length; fieldIndex++) {
|
||||
const field = data.fields[fieldIndex];
|
||||
const fieldTableOptions = (field.config.custom || {}) as TableFieldOptions;
|
||||
|
||||
if (fieldTableOptions.width) {
|
||||
availableWidth -= fieldTableOptions.width;
|
||||
fieldCountWithoutWidth -= 1;
|
||||
@ -63,10 +50,13 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||
|
||||
columns.push({
|
||||
Cell,
|
||||
id: field.name,
|
||||
id: fieldIndex.toString(),
|
||||
Header: field.config.title ?? field.name,
|
||||
accessor: field.name,
|
||||
accessor: (row: any, i: number) => {
|
||||
return field.values.get(i);
|
||||
},
|
||||
width: fieldTableOptions.width,
|
||||
minWidth: 50,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -12,8 +12,14 @@ export function cardChrome(theme: GrafanaTheme): string {
|
||||
`;
|
||||
}
|
||||
|
||||
export function hoverColor(color: string, theme: GrafanaTheme) {
|
||||
return theme.isDark ? tinycolor(color).brighten(2) : tinycolor(color).darken(2);
|
||||
export function hoverColor(color: string, theme: GrafanaTheme): string {
|
||||
return theme.isDark
|
||||
? tinycolor(color)
|
||||
.brighten(2)
|
||||
.toString()
|
||||
: tinycolor(color)
|
||||
.darken(2)
|
||||
.toString();
|
||||
}
|
||||
|
||||
export function listItem(theme: GrafanaTheme): string {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import { Table, Select } from '@grafana/ui';
|
||||
import { Field, FieldMatcherID, PanelProps, DataFrame, SelectableValue } from '@grafana/data';
|
||||
import { FieldMatcherID, PanelProps, DataFrame, SelectableValue } from '@grafana/data';
|
||||
import { Options } from './types';
|
||||
import { css } from 'emotion';
|
||||
import { config } from 'app/core/config';
|
||||
@ -13,21 +13,44 @@ export class TablePanel extends Component<Props> {
|
||||
super(props);
|
||||
}
|
||||
|
||||
onColumnResize = (field: Field, width: number) => {
|
||||
const current = this.props.fieldConfig;
|
||||
const matcherId = FieldMatcherID.byName;
|
||||
const prop = 'width';
|
||||
const overrides = current.overrides.filter(
|
||||
o => o.matcher.id !== matcherId || o.matcher.options !== field.name || o.properties[0].id !== prop
|
||||
);
|
||||
onColumnResize = (fieldIndex: number, width: number) => {
|
||||
const { fieldConfig, data } = this.props;
|
||||
const { overrides } = fieldConfig;
|
||||
const frame = data.series[this.getCurrentFrameIndex()];
|
||||
|
||||
overrides.push({
|
||||
matcher: { id: matcherId, options: field.name },
|
||||
properties: [{ id: prop, value: width }],
|
||||
});
|
||||
if (!frame) {
|
||||
return;
|
||||
}
|
||||
|
||||
const field = frame.fields[fieldIndex];
|
||||
if (!field) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fieldName = field.name;
|
||||
const matcherId = FieldMatcherID.byName;
|
||||
const propId = 'custom.width';
|
||||
|
||||
// look for existing override
|
||||
const override = overrides.find(o => o.matcher.id === matcherId && o.matcher.options === fieldName);
|
||||
|
||||
if (override) {
|
||||
// look for existing property
|
||||
const property = override.properties.find(prop => prop.id === propId);
|
||||
if (property) {
|
||||
property.value = width;
|
||||
} else {
|
||||
override.properties.push({ id: propId, value: width });
|
||||
}
|
||||
} else {
|
||||
overrides.push({
|
||||
matcher: { id: matcherId, options: fieldName },
|
||||
properties: [{ id: propId, value: width }],
|
||||
});
|
||||
}
|
||||
|
||||
this.props.onFieldConfigChange({
|
||||
...current,
|
||||
...fieldConfig,
|
||||
overrides,
|
||||
});
|
||||
};
|
||||
@ -43,19 +66,28 @@ export class TablePanel extends Component<Props> {
|
||||
};
|
||||
|
||||
renderTable(frame: DataFrame, width: number, height: number) {
|
||||
const {
|
||||
options: { showHeader, resizable },
|
||||
} = this.props;
|
||||
return <Table height={height} width={width} data={frame} noHeader={!showHeader} resizable={resizable} />;
|
||||
const { options } = this.props;
|
||||
|
||||
return (
|
||||
<Table
|
||||
height={height}
|
||||
width={width}
|
||||
data={frame}
|
||||
noHeader={!options.showHeader}
|
||||
resizable={true}
|
||||
onColumnResize={this.onColumnResize}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
getCurrentFrameIndex() {
|
||||
const { data, options } = this.props;
|
||||
const count = data.series?.length;
|
||||
return options.frameIndex > 0 && options.frameIndex < count ? options.frameIndex : 0;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
data,
|
||||
height,
|
||||
width,
|
||||
options: { frameIndex },
|
||||
} = this.props;
|
||||
const { data, height, width } = this.props;
|
||||
|
||||
const count = data.series?.length;
|
||||
|
||||
@ -65,8 +97,8 @@ export class TablePanel extends Component<Props> {
|
||||
|
||||
if (count > 1) {
|
||||
const inputHeight = config.theme.spacing.formInputHeight;
|
||||
const padding = 8;
|
||||
const index = frameIndex > 0 && frameIndex < count ? frameIndex : 0;
|
||||
const padding = 8 * 2;
|
||||
const currentIndex = this.getCurrentFrameIndex();
|
||||
const names = data.series.map((frame, index) => {
|
||||
return {
|
||||
label: `${frame.name ?? 'Series'}`,
|
||||
@ -76,13 +108,15 @@ export class TablePanel extends Component<Props> {
|
||||
|
||||
return (
|
||||
<div className={tableStyles.wrapper}>
|
||||
{this.renderTable(data.series[index], width, height - inputHeight - padding)}
|
||||
<Select options={names} value={names[index]} onChange={this.onChangeTableSelection} />
|
||||
{this.renderTable(data.series[currentIndex], width, height - inputHeight - padding)}
|
||||
<div className={tableStyles.selectWrapper}>
|
||||
<Select options={names} value={names[currentIndex]} onChange={this.onChangeTableSelection} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.renderTable(data.series[0], width, height);
|
||||
return this.renderTable(data.series[0], width, height - 12);
|
||||
}
|
||||
}
|
||||
|
||||
@ -93,4 +127,7 @@ const tableStyles = {
|
||||
justify-content: space-between;
|
||||
height: 100%;
|
||||
`,
|
||||
selectWrapper: css`
|
||||
padding: 8px;
|
||||
`,
|
||||
};
|
||||
|
@ -6,23 +6,23 @@ import { tablePanelChangedHandler, tableMigrationHandler } from './migrations';
|
||||
export const plugin = new PanelPlugin<Options, CustomFieldConfig>(TablePanel)
|
||||
.setPanelChangeHandler(tablePanelChangedHandler)
|
||||
.setMigrationHandler(tableMigrationHandler)
|
||||
.setNoPadding()
|
||||
.useFieldConfig({
|
||||
useCustomConfig: builder => {
|
||||
builder
|
||||
.addNumberInput({
|
||||
path: 'width',
|
||||
name: 'Column width',
|
||||
description: 'column width (for table)',
|
||||
settings: {
|
||||
placeholder: 'auto',
|
||||
min: 20,
|
||||
max: 300,
|
||||
},
|
||||
shouldApply: () => true,
|
||||
})
|
||||
.addRadio({
|
||||
path: 'align',
|
||||
name: 'Column alignment',
|
||||
description: 'column alignment (for table)',
|
||||
settings: {
|
||||
options: [
|
||||
{ label: 'auto', value: null },
|
||||
@ -50,17 +50,10 @@ export const plugin = new PanelPlugin<Options, CustomFieldConfig>(TablePanel)
|
||||
},
|
||||
})
|
||||
.setPanelOptions(builder => {
|
||||
builder
|
||||
.addBooleanSwitch({
|
||||
path: 'showHeader',
|
||||
name: 'Show header',
|
||||
description: "To display table's header or not to display",
|
||||
defaultValue: true,
|
||||
})
|
||||
.addBooleanSwitch({
|
||||
path: 'resizable',
|
||||
name: 'Resizable',
|
||||
description: 'Toggles if table columns are resizable or not',
|
||||
defaultValue: false,
|
||||
});
|
||||
builder.addBooleanSwitch({
|
||||
path: 'showHeader',
|
||||
name: 'Show header',
|
||||
description: "To display table's header or not to display",
|
||||
defaultValue: true,
|
||||
});
|
||||
});
|
||||
|
@ -1,7 +1,6 @@
|
||||
export interface Options {
|
||||
frameIndex: number;
|
||||
showHeader: boolean;
|
||||
resizable: boolean;
|
||||
}
|
||||
|
||||
export interface CustomFieldConfig {
|
||||
|
@ -33,6 +33,7 @@ $panel-header-no-title-zindex: 1;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
height: $panel-header-height;
|
||||
line-height: $panel-header-height;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user