mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Table: New cell hover behavior and major refactoring of table cells & style build (#27669)
* Table: Image cell and new hover behavior * ImageCell: progress * Table: refactoring cell style generation, tricky stuff * About to do something * Getting close * Need another big change * Almost everything working * Filter actions working * Updated * Updated * removed unused prop from interface * Fixed unit test * remove unused type
This commit is contained in:
parent
8018059fc4
commit
f06dcfc9ee
@ -18,11 +18,7 @@ const defaultScale: ThresholdsConfig = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const BarGaugeCell: FC<TableCellProps> = props => {
|
export const BarGaugeCell: FC<TableCellProps> = props => {
|
||||||
const { field, column, tableStyles, cell } = props;
|
const { field, column, tableStyles, cell, cellProps } = props;
|
||||||
|
|
||||||
if (!field.display) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let { config } = field;
|
let { config } = field;
|
||||||
if (!config.thresholds) {
|
if (!config.thresholds) {
|
||||||
@ -32,7 +28,7 @@ export const BarGaugeCell: FC<TableCellProps> = props => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const displayValue = field.display(cell.value);
|
const displayValue = field.display!(cell.value);
|
||||||
let barGaugeMode = BarGaugeDisplayMode.Gradient;
|
let barGaugeMode = BarGaugeDisplayMode.Gradient;
|
||||||
|
|
||||||
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
|
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
|
||||||
@ -49,7 +45,7 @@ export const BarGaugeCell: FC<TableCellProps> = props => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={tableStyles.tableCell}>
|
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||||
<BarGauge
|
<BarGauge
|
||||||
width={width}
|
width={width}
|
||||||
height={tableStyles.cellHeightInner}
|
height={tableStyles.cellHeightInner}
|
||||||
|
@ -1,46 +1,65 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC, MouseEventHandler } from 'react';
|
||||||
import { formattedValueToString, LinkModel } from '@grafana/data';
|
import { DisplayValue, Field, formattedValueToString, LinkModel } from '@grafana/data';
|
||||||
|
|
||||||
import { TableCellProps } from './types';
|
import { TableCellDisplayMode, TableCellProps } from './types';
|
||||||
|
import tinycolor from 'tinycolor2';
|
||||||
|
import { TableStyles } from './styles';
|
||||||
|
import { FilterActions } from './FilterActions';
|
||||||
|
|
||||||
export const DefaultCell: FC<TableCellProps> = props => {
|
export const DefaultCell: FC<TableCellProps> = props => {
|
||||||
const { field, cell, tableStyles, row } = props;
|
const { field, cell, tableStyles, row, cellProps } = props;
|
||||||
let link: LinkModel<any> | undefined;
|
|
||||||
|
|
||||||
const displayValue = field.display ? field.display(cell.value) : cell.value;
|
const displayValue = field.display!(cell.value);
|
||||||
|
const value = formattedValueToString(displayValue);
|
||||||
|
const cellStyle = getCellStyle(tableStyles, field, displayValue);
|
||||||
|
const showFilters = field.config.filterable;
|
||||||
|
|
||||||
|
let link: LinkModel<any> | undefined;
|
||||||
|
let onClick: MouseEventHandler<HTMLAnchorElement> | undefined;
|
||||||
|
|
||||||
if (field.getLinks) {
|
if (field.getLinks) {
|
||||||
link = field.getLinks({
|
link = field.getLinks({
|
||||||
valueRowIndex: row.index,
|
valueRowIndex: row.index,
|
||||||
})[0];
|
})[0];
|
||||||
}
|
}
|
||||||
const value = field.display ? formattedValueToString(displayValue) : `${displayValue}`;
|
|
||||||
|
|
||||||
if (!link) {
|
if (link && link.onClick) {
|
||||||
return <div className={tableStyles.tableCell}>{value}</div>;
|
onClick = event => {
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={tableStyles.tableCell}>
|
|
||||||
<a
|
|
||||||
href={link.href}
|
|
||||||
onClick={
|
|
||||||
link.onClick
|
|
||||||
? event => {
|
|
||||||
// Allow opening in new tab
|
// Allow opening in new tab
|
||||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) {
|
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
link!.onClick(event);
|
link!.onClick(event);
|
||||||
}
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
: undefined
|
|
||||||
}
|
return (
|
||||||
target={link.target}
|
<div {...cellProps} className={cellStyle}>
|
||||||
title={link.title}
|
{!link && <div className={tableStyles.cellText}>{value}</div>}
|
||||||
className={tableStyles.tableCellLink}
|
{link && (
|
||||||
>
|
<a href={link.href} onClick={onClick} target={link.target} title={link.title} className={tableStyles.cellLink}>
|
||||||
{value}
|
{value}
|
||||||
</a>
|
</a>
|
||||||
|
)}
|
||||||
|
{showFilters && cell.value && <FilterActions {...props} />}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function getCellStyle(tableStyles: TableStyles, field: Field, displayValue: DisplayValue) {
|
||||||
|
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorText) {
|
||||||
|
return tableStyles.buildCellContainerStyle(displayValue.color);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.config.custom?.displayMode === TableCellDisplayMode.ColorBackground) {
|
||||||
|
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
|
||||||
|
const bgColor2 = tinycolor(displayValue.color)
|
||||||
|
.darken(10 * themeFactor)
|
||||||
|
.spin(5)
|
||||||
|
.toRgbString();
|
||||||
|
|
||||||
|
return tableStyles.buildCellContainerStyle('white', `linear-gradient(120deg, ${bgColor2}, ${displayValue.color})`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableStyles.cellContainer;
|
||||||
|
}
|
||||||
|
31
packages/grafana-ui/src/components/Table/FilterActions.tsx
Normal file
31
packages/grafana-ui/src/components/Table/FilterActions.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React, { FC, useCallback } from 'react';
|
||||||
|
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps } from './types';
|
||||||
|
import { Icon, Tooltip } from '..';
|
||||||
|
|
||||||
|
export const FilterActions: FC<TableCellProps> = ({ cell, field, tableStyles, onCellFilterAdded }) => {
|
||||||
|
const onFilterFor = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }),
|
||||||
|
[cell, field, onCellFilterAdded]
|
||||||
|
);
|
||||||
|
const onFilterOut = useCallback(
|
||||||
|
(event: React.MouseEvent<HTMLDivElement>) =>
|
||||||
|
onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }),
|
||||||
|
[cell, field, onCellFilterAdded]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={tableStyles.filterWrapper}>
|
||||||
|
<div className={tableStyles.filterItem}>
|
||||||
|
<Tooltip content="Filter for value" placement="top">
|
||||||
|
<Icon name={'search-plus'} onClick={onFilterFor} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className={tableStyles.filterItem}>
|
||||||
|
<Tooltip content="Filter out value" placement="top">
|
||||||
|
<Icon name={'search-minus'} onClick={onFilterOut} />
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,72 +0,0 @@
|
|||||||
import React, { FC, useCallback, useState } from 'react';
|
|
||||||
import { TableCellProps } from 'react-table';
|
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
|
||||||
import { css } from 'emotion';
|
|
||||||
|
|
||||||
import { stylesFactory, useTheme } from '../../themes';
|
|
||||||
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableFilterActionCallback } from './types';
|
|
||||||
import { Icon, Tooltip } from '..';
|
|
||||||
import { Props, renderCell } from './TableCell';
|
|
||||||
|
|
||||||
interface FilterableTableCellProps extends Pick<Props, 'cell' | 'field' | 'tableStyles'> {
|
|
||||||
onCellFilterAdded: TableFilterActionCallback;
|
|
||||||
cellProps: TableCellProps;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FilterableTableCell: FC<FilterableTableCellProps> = ({
|
|
||||||
cell,
|
|
||||||
field,
|
|
||||||
tableStyles,
|
|
||||||
onCellFilterAdded,
|
|
||||||
cellProps,
|
|
||||||
}) => {
|
|
||||||
const [showFilters, setShowFilter] = useState(false);
|
|
||||||
const onMouseOver = useCallback((event: React.MouseEvent<HTMLDivElement>) => setShowFilter(true), [setShowFilter]);
|
|
||||||
const onMouseLeave = useCallback((event: React.MouseEvent<HTMLDivElement>) => setShowFilter(false), [setShowFilter]);
|
|
||||||
const onFilterFor = useCallback(
|
|
||||||
(event: React.MouseEvent<HTMLDivElement>) =>
|
|
||||||
onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }),
|
|
||||||
[cell, field, onCellFilterAdded]
|
|
||||||
);
|
|
||||||
const onFilterOut = useCallback(
|
|
||||||
(event: React.MouseEvent<HTMLDivElement>) =>
|
|
||||||
onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }),
|
|
||||||
[cell, field, onCellFilterAdded]
|
|
||||||
);
|
|
||||||
const theme = useTheme();
|
|
||||||
const styles = getFilterableTableCellStyles(theme);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div {...cellProps} className={tableStyles.tableCellWrapper} onMouseOver={onMouseOver} onMouseLeave={onMouseLeave}>
|
|
||||||
{renderCell(cell, field, tableStyles)}
|
|
||||||
{showFilters && cell.value && (
|
|
||||||
<div className={styles.filterWrapper}>
|
|
||||||
<div className={styles.filterItem}>
|
|
||||||
<Tooltip content="Filter for value" placement="top">
|
|
||||||
<Icon name={'search-plus'} onClick={onFilterFor} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
<div className={styles.filterItem}>
|
|
||||||
<Tooltip content="Filter out value" placement="top">
|
|
||||||
<Icon name={'search-minus'} onClick={onFilterOut} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const getFilterableTableCellStyles = stylesFactory((theme: GrafanaTheme) => ({
|
|
||||||
filterWrapper: css`
|
|
||||||
label: filterWrapper;
|
|
||||||
display: inline-flex;
|
|
||||||
justify-content: space-around;
|
|
||||||
cursor: pointer;
|
|
||||||
`,
|
|
||||||
filterItem: css`
|
|
||||||
label: filterItem;
|
|
||||||
color: ${theme.colors.textSemiWeak};
|
|
||||||
padding: 0 ${theme.spacing.xxs};
|
|
||||||
`,
|
|
||||||
}));
|
|
12
packages/grafana-ui/src/components/Table/ImageCell.tsx
Normal file
12
packages/grafana-ui/src/components/Table/ImageCell.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import React, { FC } from 'react';
|
||||||
|
import { TableCellProps } from './types';
|
||||||
|
|
||||||
|
export const ImageCell: FC<TableCellProps> = props => {
|
||||||
|
const { cell, tableStyles, cellProps } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||||
|
<img src={cell.value} className={tableStyles.imageCell} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -8,11 +8,7 @@ import { TableCellProps } from './types';
|
|||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
export const JSONViewCell: FC<TableCellProps> = props => {
|
export const JSONViewCell: FC<TableCellProps> = props => {
|
||||||
const { field, cell, tableStyles } = props;
|
const { cell, tableStyles, cellProps } = props;
|
||||||
|
|
||||||
if (!field.display) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const txt = css`
|
const txt = css`
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -21,6 +17,7 @@ export const JSONViewCell: FC<TableCellProps> = props => {
|
|||||||
|
|
||||||
let value = cell.value;
|
let value = cell.value;
|
||||||
let displayValue = value;
|
let displayValue = value;
|
||||||
|
|
||||||
if (isString(value)) {
|
if (isString(value)) {
|
||||||
try {
|
try {
|
||||||
value = JSON.parse(value);
|
value = JSON.parse(value);
|
||||||
@ -28,11 +25,13 @@ export const JSONViewCell: FC<TableCellProps> = props => {
|
|||||||
} else {
|
} else {
|
||||||
displayValue = JSON.stringify(value);
|
displayValue = JSON.stringify(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
const content = <JSONTooltip value={value} />;
|
const content = <JSONTooltip value={value} />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cx(txt, tableStyles.tableCell)}>
|
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||||
<Tooltip placement="auto" content={content} theme="info-alt">
|
<Tooltip placement="auto" content={content} theme="info-alt">
|
||||||
<div className={tableStyles.overflow}>{displayValue}</div>
|
<div className={cx(tableStyles.cellText, txt)}>{displayValue}</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -14,7 +14,7 @@ import {
|
|||||||
useTable,
|
useTable,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { FixedSizeList } from 'react-window';
|
import { FixedSizeList } from 'react-window';
|
||||||
import { getColumns, getHeaderAlign } from './utils';
|
import { getColumns } from './utils';
|
||||||
import { useTheme } from '../../themes';
|
import { useTheme } from '../../themes';
|
||||||
import {
|
import {
|
||||||
TableColumnResizeActionCallback,
|
TableColumnResizeActionCallback,
|
||||||
@ -23,10 +23,10 @@ import {
|
|||||||
TableSortByFieldState,
|
TableSortByFieldState,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getTableStyles, TableStyles } from './styles';
|
import { getTableStyles, TableStyles } from './styles';
|
||||||
import { TableCell } from './TableCell';
|
|
||||||
import { Icon } from '../Icon/Icon';
|
import { Icon } from '../Icon/Icon';
|
||||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||||
import { Filter } from './Filter';
|
import { Filter } from './Filter';
|
||||||
|
import { TableCell } from './TableCell';
|
||||||
|
|
||||||
const COLUMN_MIN_WIDTH = 150;
|
const COLUMN_MIN_WIDTH = 150;
|
||||||
|
|
||||||
@ -229,7 +229,7 @@ function renderHeaderCell(column: any, tableStyles: TableStyles, field?: Field)
|
|||||||
}
|
}
|
||||||
|
|
||||||
headerProps.style.position = 'absolute';
|
headerProps.style.position = 'absolute';
|
||||||
headerProps.style.justifyContent = getHeaderAlign(field);
|
headerProps.style.justifyContent = (column as any).justifyContent;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={tableStyles.headerCell} {...headerProps}>
|
<div className={tableStyles.headerCell} {...headerProps}>
|
||||||
|
@ -1,11 +1,8 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import { Cell } from 'react-table';
|
import { Cell } from 'react-table';
|
||||||
import { Field } from '@grafana/data';
|
import { Field } from '@grafana/data';
|
||||||
|
|
||||||
import { getTextAlign } from './utils';
|
|
||||||
import { TableFilterActionCallback } from './types';
|
import { TableFilterActionCallback } from './types';
|
||||||
import { TableStyles } from './styles';
|
import { TableStyles } from './styles';
|
||||||
import { FilterableTableCell } from './FilterableTableCell';
|
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
cell: Cell;
|
cell: Cell;
|
||||||
@ -15,31 +12,25 @@ export interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellFilterAdded }) => {
|
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellFilterAdded }) => {
|
||||||
const filterable = field.config.filterable;
|
|
||||||
const cellProps = cell.getCellProps();
|
const cellProps = cell.getCellProps();
|
||||||
|
|
||||||
|
if (!field.display) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (cellProps.style) {
|
if (cellProps.style) {
|
||||||
cellProps.style.textAlign = getTextAlign(field);
|
cellProps.style.minWidth = cellProps.style.width;
|
||||||
}
|
cellProps.style.justifyContent = (cell.column as any).justifyContent;
|
||||||
|
|
||||||
if (filterable && onCellFilterAdded) {
|
|
||||||
return (
|
|
||||||
<FilterableTableCell
|
|
||||||
cell={cell}
|
|
||||||
field={field}
|
|
||||||
tableStyles={tableStyles}
|
|
||||||
onCellFilterAdded={onCellFilterAdded}
|
|
||||||
cellProps={cellProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div {...cellProps} className={tableStyles.tableCellWrapper}>
|
<>
|
||||||
{renderCell(cell, field, tableStyles)}
|
{cell.render('Cell', {
|
||||||
</div>
|
field,
|
||||||
|
tableStyles,
|
||||||
|
onCellFilterAdded,
|
||||||
|
cellProps,
|
||||||
|
})}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const renderCell = (cell: Cell, field: Field, tableStyles: TableStyles) =>
|
|
||||||
cell.render('Cell', { field, tableStyles });
|
|
||||||
|
@ -1,44 +1,59 @@
|
|||||||
import { css } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { styleMixins, stylesFactory } from '../../themes';
|
import { styleMixins, stylesFactory } from '../../themes';
|
||||||
import { getScrollbarWidth } from '../../utils';
|
import { getScrollbarWidth } from '../../utils';
|
||||||
|
|
||||||
export interface TableStyles {
|
export const getTableStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
cellHeight: number;
|
|
||||||
cellHeightInner: number;
|
|
||||||
cellPadding: number;
|
|
||||||
rowHeight: number;
|
|
||||||
table: string;
|
|
||||||
thead: string;
|
|
||||||
headerCell: string;
|
|
||||||
headerCellLabel: string;
|
|
||||||
headerFilter: string;
|
|
||||||
tableCell: string;
|
|
||||||
tableCellWrapper: string;
|
|
||||||
tableCellLink: string;
|
|
||||||
row: string;
|
|
||||||
theme: GrafanaTheme;
|
|
||||||
resizeHandle: string;
|
|
||||||
overflow: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getTableStyles = stylesFactory(
|
|
||||||
(theme: GrafanaTheme): TableStyles => {
|
|
||||||
const { palette, colors } = theme;
|
const { palette, colors } = theme;
|
||||||
const headerBg = theme.colors.bg2;
|
const headerBg = theme.colors.bg2;
|
||||||
const borderColor = theme.colors.border1;
|
const borderColor = theme.colors.border1;
|
||||||
const resizerColor = theme.isLight ? palette.blue95 : palette.blue77;
|
const resizerColor = theme.isLight ? palette.blue95 : palette.blue77;
|
||||||
const padding = 6;
|
const cellPadding = 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 = cellPadding * 2 + bodyFontSize * lineHeight;
|
||||||
const rowHoverBg = styleMixins.hoverColor(theme.colors.bg1, theme);
|
const rowHoverBg = styleMixins.hoverColor(theme.colors.bg1, theme);
|
||||||
const scollbarWidth = getScrollbarWidth();
|
const scollbarWidth = getScrollbarWidth();
|
||||||
|
|
||||||
|
const buildCellContainerStyle = (color?: string, background?: string) => {
|
||||||
|
return css`
|
||||||
|
padding: ${cellPadding}px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-right: 1px solid ${borderColor};
|
||||||
|
|
||||||
|
${color ? `color: ${color};` : ''};
|
||||||
|
${background ? `background: ${background};` : ''};
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-right: none;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
padding-right: ${scollbarWidth + cellPadding}px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
overflow: visible;
|
||||||
|
width: auto !important;
|
||||||
|
box-shadow: 0 0 2px ${theme.colors.formFocusOutline};
|
||||||
|
background: ${background ?? rowHoverBg};
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
.cell-filter-actions {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
theme,
|
theme,
|
||||||
cellHeight,
|
cellHeight,
|
||||||
cellPadding: padding,
|
buildCellContainerStyle,
|
||||||
|
cellPadding,
|
||||||
cellHeightInner: bodyFontSize * lineHeight,
|
cellHeightInner: bodyFontSize * lineHeight,
|
||||||
rowHeight: cellHeight + 2,
|
rowHeight: cellHeight + 2,
|
||||||
table: css`
|
table: css`
|
||||||
@ -56,7 +71,7 @@ export const getTableStyles = stylesFactory(
|
|||||||
position: relative;
|
position: relative;
|
||||||
`,
|
`,
|
||||||
headerCell: css`
|
headerCell: css`
|
||||||
padding: ${padding}px;
|
padding: ${cellPadding}px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: ${colors.textBlue};
|
color: ${colors.textBlue};
|
||||||
@ -75,6 +90,22 @@ export const getTableStyles = stylesFactory(
|
|||||||
display: flex;
|
display: flex;
|
||||||
margin-right: ${theme.spacing.xs};
|
margin-right: ${theme.spacing.xs};
|
||||||
`,
|
`,
|
||||||
|
cellContainer: buildCellContainerStyle(),
|
||||||
|
cellText: css`
|
||||||
|
cursor: text;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: text;
|
||||||
|
white-space: nowrap;
|
||||||
|
`,
|
||||||
|
cellLink: css`
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
user-select: text;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: underline;
|
||||||
|
`,
|
||||||
headerFilter: css`
|
headerFilter: css`
|
||||||
label: headerFilter;
|
label: headerFilter;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -87,33 +118,8 @@ export const getTableStyles = stylesFactory(
|
|||||||
background-color: ${rowHoverBg};
|
background-color: ${rowHoverBg};
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
tableCellWrapper: css`
|
imageCell: css`
|
||||||
border-right: 1px solid ${borderColor};
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-right: none;
|
|
||||||
|
|
||||||
> div {
|
|
||||||
padding-right: ${scollbarWidth + padding}px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
tableCellLink: css`
|
|
||||||
text-decoration: underline;
|
|
||||||
`,
|
|
||||||
tableCell: css`
|
|
||||||
padding: ${padding}px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
flex: 1;
|
|
||||||
`,
|
|
||||||
overflow: css`
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
`,
|
`,
|
||||||
resizeHandle: css`
|
resizeHandle: css`
|
||||||
label: resizeHandle;
|
label: resizeHandle;
|
||||||
@ -135,6 +141,23 @@ export const getTableStyles = stylesFactory(
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
filterWrapper: cx(
|
||||||
|
css`
|
||||||
|
label: filterWrapper;
|
||||||
|
display: none;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
opacity: 0.6;
|
||||||
|
padding-left: ${theme.spacing.xxs};
|
||||||
|
`,
|
||||||
|
'cell-filter-actions'
|
||||||
|
),
|
||||||
|
filterItem: css`
|
||||||
|
label: filterItem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0 ${theme.spacing.xxs};
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
export type TableStyles = ReturnType<typeof getTableStyles>;
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { CellProps } from 'react-table';
|
import { CellProps } from 'react-table';
|
||||||
import { Field } from '@grafana/data';
|
import { Field } from '@grafana/data';
|
||||||
import { TableStyles } from './styles';
|
import { TableStyles } from './styles';
|
||||||
import { FC } from 'react';
|
import { CSSProperties, FC } from 'react';
|
||||||
|
|
||||||
export interface TableFieldOptions {
|
export interface TableFieldOptions {
|
||||||
width: number;
|
width: number;
|
||||||
@ -18,6 +18,7 @@ export enum TableCellDisplayMode {
|
|||||||
LcdGauge = 'lcd-gauge',
|
LcdGauge = 'lcd-gauge',
|
||||||
JSONView = 'json-view',
|
JSONView = 'json-view',
|
||||||
BasicGauge = 'basic',
|
BasicGauge = 'basic',
|
||||||
|
Image = 'image',
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
|
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
|
||||||
@ -41,7 +42,9 @@ export interface TableSortByFieldState {
|
|||||||
|
|
||||||
export interface TableCellProps extends CellProps<any> {
|
export interface TableCellProps extends CellProps<any> {
|
||||||
tableStyles: TableStyles;
|
tableStyles: TableStyles;
|
||||||
|
cellProps: CSSProperties;
|
||||||
field: Field;
|
field: Field;
|
||||||
|
onCellFilterAdded: TableFilterActionCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CellComponent = FC<TableCellProps>;
|
export type CellComponent = FC<TableCellProps>;
|
||||||
|
@ -66,7 +66,7 @@ describe('Table utils', () => {
|
|||||||
it('Should set textAlign to right for number values', () => {
|
it('Should set textAlign to right for number values', () => {
|
||||||
const data = getData();
|
const data = getData();
|
||||||
const textAlign = getTextAlign(data.fields[1]);
|
const textAlign = getTextAlign(data.fields[1]);
|
||||||
expect(textAlign).toBe('right');
|
expect(textAlign).toBe('flex-end');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
import { Column, Row } from 'react-table';
|
import { Column, Row } from 'react-table';
|
||||||
import memoizeOne from 'memoize-one';
|
import memoizeOne from 'memoize-one';
|
||||||
import { css, cx } from 'emotion';
|
import { ContentPosition } from 'csstype';
|
||||||
import tinycolor from 'tinycolor2';
|
|
||||||
import { ContentPosition, TextAlignProperty } from 'csstype';
|
|
||||||
import {
|
import {
|
||||||
DataFrame,
|
DataFrame,
|
||||||
Field,
|
Field,
|
||||||
@ -14,13 +12,13 @@ import {
|
|||||||
|
|
||||||
import { DefaultCell } from './DefaultCell';
|
import { DefaultCell } from './DefaultCell';
|
||||||
import { BarGaugeCell } from './BarGaugeCell';
|
import { BarGaugeCell } from './BarGaugeCell';
|
||||||
import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types';
|
import { TableCellDisplayMode, TableFieldOptions } from './types';
|
||||||
import { withTableStyles } from './withTableStyles';
|
|
||||||
import { JSONViewCell } from './JSONViewCell';
|
import { JSONViewCell } from './JSONViewCell';
|
||||||
|
import { ImageCell } from './ImageCell';
|
||||||
|
|
||||||
export function getTextAlign(field?: Field): TextAlignProperty {
|
export function getTextAlign(field?: Field): ContentPosition {
|
||||||
if (!field) {
|
if (!field) {
|
||||||
return 'left';
|
return 'flex-start';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.config.custom) {
|
if (field.config.custom) {
|
||||||
@ -28,19 +26,19 @@ export function getTextAlign(field?: Field): TextAlignProperty {
|
|||||||
|
|
||||||
switch (custom.align) {
|
switch (custom.align) {
|
||||||
case 'right':
|
case 'right':
|
||||||
return 'right';
|
return 'flex-end';
|
||||||
case 'left':
|
case 'left':
|
||||||
return 'left';
|
return 'flex-start';
|
||||||
case 'center':
|
case 'center':
|
||||||
return 'center';
|
return 'center';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (field.type === FieldType.number) {
|
if (field.type === FieldType.number) {
|
||||||
return 'right';
|
return 'flex-end';
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'left';
|
return 'flex-start';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getColumns(data: DataFrame, availableWidth: number, columnMinWidth: number): Column[] {
|
export function getColumns(data: DataFrame, availableWidth: number, columnMinWidth: number): Column[] {
|
||||||
@ -68,6 +66,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
|||||||
return 'alphanumeric';
|
return 'alphanumeric';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
|
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
|
||||||
columns.push({
|
columns.push({
|
||||||
Cell,
|
Cell,
|
||||||
@ -80,6 +79,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
|||||||
width: fieldTableOptions.width,
|
width: fieldTableOptions.width,
|
||||||
minWidth: 50,
|
minWidth: 50,
|
||||||
filter: memoizeOne(filterByValue),
|
filter: memoizeOne(filterByValue),
|
||||||
|
justifyContent: getTextAlign(field),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,9 +97,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
|||||||
function getCellComponent(displayMode: TableCellDisplayMode, field: Field) {
|
function getCellComponent(displayMode: TableCellDisplayMode, field: Field) {
|
||||||
switch (displayMode) {
|
switch (displayMode) {
|
||||||
case TableCellDisplayMode.ColorText:
|
case TableCellDisplayMode.ColorText:
|
||||||
return withTableStyles(DefaultCell, getTextColorStyle);
|
|
||||||
case TableCellDisplayMode.ColorBackground:
|
case TableCellDisplayMode.ColorBackground:
|
||||||
return withTableStyles(DefaultCell, getBackgroundColorStyle);
|
return DefaultCell;
|
||||||
|
case TableCellDisplayMode.Image:
|
||||||
|
return ImageCell;
|
||||||
case TableCellDisplayMode.LcdGauge:
|
case TableCellDisplayMode.LcdGauge:
|
||||||
case TableCellDisplayMode.BasicGauge:
|
case TableCellDisplayMode.BasicGauge:
|
||||||
case TableCellDisplayMode.GradientGauge:
|
case TableCellDisplayMode.GradientGauge:
|
||||||
@ -115,58 +116,6 @@ function getCellComponent(displayMode: TableCellDisplayMode, field: Field) {
|
|||||||
return DefaultCell;
|
return DefaultCell;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTextColorStyle(props: TableCellProps) {
|
|
||||||
const { field, cell, tableStyles } = props;
|
|
||||||
|
|
||||||
if (!field.display) {
|
|
||||||
return tableStyles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayValue = field.display(cell.value);
|
|
||||||
if (!displayValue.color) {
|
|
||||||
return tableStyles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extendedStyle = css`
|
|
||||||
color: ${displayValue.color};
|
|
||||||
`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...tableStyles,
|
|
||||||
tableCell: cx(tableStyles.tableCell, extendedStyle),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getBackgroundColorStyle(props: TableCellProps) {
|
|
||||||
const { field, cell, tableStyles } = props;
|
|
||||||
if (!field.display) {
|
|
||||||
return tableStyles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const displayValue = field.display(cell.value);
|
|
||||||
if (!displayValue.color) {
|
|
||||||
return tableStyles;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themeFactor = tableStyles.theme.isDark ? 1 : -0.7;
|
|
||||||
const bgColor2 = tinycolor(displayValue.color)
|
|
||||||
.darken(10 * themeFactor)
|
|
||||||
.spin(5)
|
|
||||||
.toRgbString();
|
|
||||||
|
|
||||||
const extendedStyle = css`
|
|
||||||
background: linear-gradient(120deg, ${bgColor2}, ${displayValue.color});
|
|
||||||
color: white;
|
|
||||||
height: ${tableStyles.cellHeight}px;
|
|
||||||
padding: ${tableStyles.cellPadding}px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
return {
|
|
||||||
...tableStyles,
|
|
||||||
tableCell: cx(tableStyles.tableCell, extendedStyle),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function filterByValue(rows: Row[], id: string, filterValues?: SelectableValue[]) {
|
export function filterByValue(rows: Row[], id: string, filterValues?: SelectableValue[]) {
|
||||||
if (rows.length === 0) {
|
if (rows.length === 0) {
|
||||||
return rows;
|
return rows;
|
||||||
@ -186,20 +135,6 @@ export function filterByValue(rows: Row[], id: string, filterValues?: Selectable
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getHeaderAlign(field?: Field): ContentPosition {
|
|
||||||
const align = getTextAlign(field);
|
|
||||||
|
|
||||||
if (align === 'right') {
|
|
||||||
return 'flex-end';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (align === 'center') {
|
|
||||||
return align;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'flex-start';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function calculateUniqueFieldValues(rows: any[], field?: Field) {
|
export function calculateUniqueFieldValues(rows: any[], field?: Field) {
|
||||||
if (!field || rows.length === 0) {
|
if (!field || rows.length === 0) {
|
||||||
return {};
|
return {};
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
import { CellComponent, TableCellProps } from './types';
|
|
||||||
import { TableStyles } from './styles';
|
|
||||||
|
|
||||||
export const withTableStyles = (
|
|
||||||
CellComponent: CellComponent,
|
|
||||||
getExtendedStyles: (props: TableCellProps) => TableStyles
|
|
||||||
): CellComponent => {
|
|
||||||
function WithTableStyles(props: TableCellProps) {
|
|
||||||
return CellComponent({ ...props, tableStyles: getExtendedStyles(props) });
|
|
||||||
}
|
|
||||||
|
|
||||||
WithTableStyles.displayName = CellComponent.displayName || CellComponent.name;
|
|
||||||
return WithTableStyles;
|
|
||||||
};
|
|
@ -47,6 +47,7 @@ export const plugin = new PanelPlugin<Options, CustomFieldConfig>(TablePanel)
|
|||||||
{ value: TableCellDisplayMode.LcdGauge, label: 'LCD gauge' },
|
{ value: TableCellDisplayMode.LcdGauge, label: 'LCD gauge' },
|
||||||
{ value: TableCellDisplayMode.BasicGauge, label: 'Basic gauge' },
|
{ value: TableCellDisplayMode.BasicGauge, label: 'Basic gauge' },
|
||||||
{ value: TableCellDisplayMode.JSONView, label: 'JSON View' },
|
{ value: TableCellDisplayMode.JSONView, label: 'JSON View' },
|
||||||
|
{ value: TableCellDisplayMode.Image, label: 'Image' },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user