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