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 => {
|
||||
const { field, column, tableStyles, cell } = props;
|
||||
|
||||
if (!field.display) {
|
||||
return null;
|
||||
}
|
||||
const { field, column, tableStyles, cell, cellProps } = props;
|
||||
|
||||
let { config } = field;
|
||||
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;
|
||||
|
||||
if (field.config.custom && field.config.custom.displayMode === TableCellDisplayMode.LcdGauge) {
|
||||
@ -49,7 +45,7 @@ export const BarGaugeCell: FC<TableCellProps> = props => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tableStyles.tableCell}>
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
<BarGauge
|
||||
width={width}
|
||||
height={tableStyles.cellHeightInner}
|
||||
|
@ -1,46 +1,65 @@
|
||||
import React, { FC } from 'react';
|
||||
import { formattedValueToString, LinkModel } from '@grafana/data';
|
||||
import React, { FC, MouseEventHandler } from 'react';
|
||||
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 => {
|
||||
const { field, cell, tableStyles, row } = props;
|
||||
let link: LinkModel<any> | undefined;
|
||||
const { field, cell, tableStyles, row, cellProps } = props;
|
||||
|
||||
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) {
|
||||
link = field.getLinks({
|
||||
valueRowIndex: row.index,
|
||||
})[0];
|
||||
}
|
||||
const value = field.display ? formattedValueToString(displayValue) : `${displayValue}`;
|
||||
|
||||
if (!link) {
|
||||
return <div className={tableStyles.tableCell}>{value}</div>;
|
||||
if (link && link.onClick) {
|
||||
onClick = event => {
|
||||
// Allow opening in new tab
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) {
|
||||
event.preventDefault();
|
||||
link!.onClick(event);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={tableStyles.tableCell}>
|
||||
<a
|
||||
href={link.href}
|
||||
onClick={
|
||||
link.onClick
|
||||
? event => {
|
||||
// Allow opening in new tab
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link!.onClick) {
|
||||
event.preventDefault();
|
||||
link!.onClick(event);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
target={link.target}
|
||||
title={link.title}
|
||||
className={tableStyles.tableCellLink}
|
||||
>
|
||||
{value}
|
||||
</a>
|
||||
<div {...cellProps} className={cellStyle}>
|
||||
{!link && <div className={tableStyles.cellText}>{value}</div>}
|
||||
{link && (
|
||||
<a href={link.href} onClick={onClick} target={link.target} title={link.title} className={tableStyles.cellLink}>
|
||||
{value}
|
||||
</a>
|
||||
)}
|
||||
{showFilters && cell.value && <FilterActions {...props} />}
|
||||
</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';
|
||||
|
||||
export const JSONViewCell: FC<TableCellProps> = props => {
|
||||
const { field, cell, tableStyles } = props;
|
||||
|
||||
if (!field.display) {
|
||||
return null;
|
||||
}
|
||||
const { cell, tableStyles, cellProps } = props;
|
||||
|
||||
const txt = css`
|
||||
cursor: pointer;
|
||||
@ -21,6 +17,7 @@ export const JSONViewCell: FC<TableCellProps> = props => {
|
||||
|
||||
let value = cell.value;
|
||||
let displayValue = value;
|
||||
|
||||
if (isString(value)) {
|
||||
try {
|
||||
value = JSON.parse(value);
|
||||
@ -28,11 +25,13 @@ export const JSONViewCell: FC<TableCellProps> = props => {
|
||||
} else {
|
||||
displayValue = JSON.stringify(value);
|
||||
}
|
||||
|
||||
const content = <JSONTooltip value={value} />;
|
||||
|
||||
return (
|
||||
<div className={cx(txt, tableStyles.tableCell)}>
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
<Tooltip placement="auto" content={content} theme="info-alt">
|
||||
<div className={tableStyles.overflow}>{displayValue}</div>
|
||||
<div className={cx(tableStyles.cellText, txt)}>{displayValue}</div>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
useTable,
|
||||
} from 'react-table';
|
||||
import { FixedSizeList } from 'react-window';
|
||||
import { getColumns, getHeaderAlign } from './utils';
|
||||
import { getColumns } from './utils';
|
||||
import { useTheme } from '../../themes';
|
||||
import {
|
||||
TableColumnResizeActionCallback,
|
||||
@ -23,10 +23,10 @@ import {
|
||||
TableSortByFieldState,
|
||||
} from './types';
|
||||
import { getTableStyles, TableStyles } from './styles';
|
||||
import { TableCell } from './TableCell';
|
||||
import { Icon } from '../Icon/Icon';
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { Filter } from './Filter';
|
||||
import { TableCell } from './TableCell';
|
||||
|
||||
const COLUMN_MIN_WIDTH = 150;
|
||||
|
||||
@ -229,7 +229,7 @@ function renderHeaderCell(column: any, tableStyles: TableStyles, field?: Field)
|
||||
}
|
||||
|
||||
headerProps.style.position = 'absolute';
|
||||
headerProps.style.justifyContent = getHeaderAlign(field);
|
||||
headerProps.style.justifyContent = (column as any).justifyContent;
|
||||
|
||||
return (
|
||||
<div className={tableStyles.headerCell} {...headerProps}>
|
||||
|
@ -1,11 +1,8 @@
|
||||
import React, { FC } from 'react';
|
||||
import { Cell } from 'react-table';
|
||||
import { Field } from '@grafana/data';
|
||||
|
||||
import { getTextAlign } from './utils';
|
||||
import { TableFilterActionCallback } from './types';
|
||||
import { TableStyles } from './styles';
|
||||
import { FilterableTableCell } from './FilterableTableCell';
|
||||
|
||||
export interface Props {
|
||||
cell: Cell;
|
||||
@ -15,31 +12,25 @@ export interface Props {
|
||||
}
|
||||
|
||||
export const TableCell: FC<Props> = ({ cell, field, tableStyles, onCellFilterAdded }) => {
|
||||
const filterable = field.config.filterable;
|
||||
const cellProps = cell.getCellProps();
|
||||
|
||||
if (cellProps.style) {
|
||||
cellProps.style.textAlign = getTextAlign(field);
|
||||
if (!field.display) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (filterable && onCellFilterAdded) {
|
||||
return (
|
||||
<FilterableTableCell
|
||||
cell={cell}
|
||||
field={field}
|
||||
tableStyles={tableStyles}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
cellProps={cellProps}
|
||||
/>
|
||||
);
|
||||
if (cellProps.style) {
|
||||
cellProps.style.minWidth = cellProps.style.width;
|
||||
cellProps.style.justifyContent = (cell.column as any).justifyContent;
|
||||
}
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.tableCellWrapper}>
|
||||
{renderCell(cell, field, tableStyles)}
|
||||
</div>
|
||||
<>
|
||||
{cell.render('Cell', {
|
||||
field,
|
||||
tableStyles,
|
||||
onCellFilterAdded,
|
||||
cellProps,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const renderCell = (cell: Cell, field: Field, tableStyles: TableStyles) =>
|
||||
cell.render('Cell', { field, tableStyles });
|
||||
|
@ -1,140 +1,163 @@
|
||||
import { css } from 'emotion';
|
||||
import { css, cx } from 'emotion';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { styleMixins, stylesFactory } from '../../themes';
|
||||
import { getScrollbarWidth } from '../../utils';
|
||||
|
||||
export interface TableStyles {
|
||||
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) => {
|
||||
const { palette, colors } = theme;
|
||||
const headerBg = theme.colors.bg2;
|
||||
const borderColor = theme.colors.border1;
|
||||
const resizerColor = theme.isLight ? palette.blue95 : palette.blue77;
|
||||
const cellPadding = 6;
|
||||
const lineHeight = theme.typography.lineHeight.md;
|
||||
const bodyFontSize = 14;
|
||||
const cellHeight = cellPadding * 2 + bodyFontSize * lineHeight;
|
||||
const rowHoverBg = styleMixins.hoverColor(theme.colors.bg1, theme);
|
||||
const scollbarWidth = getScrollbarWidth();
|
||||
|
||||
export const getTableStyles = stylesFactory(
|
||||
(theme: GrafanaTheme): TableStyles => {
|
||||
const { palette, colors } = theme;
|
||||
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);
|
||||
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};
|
||||
|
||||
return {
|
||||
theme,
|
||||
cellHeight,
|
||||
cellPadding: padding,
|
||||
cellHeightInner: bodyFontSize * lineHeight,
|
||||
rowHeight: cellHeight + 2,
|
||||
table: css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
`,
|
||||
thead: css`
|
||||
label: thead;
|
||||
height: ${cellHeight}px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: ${headerBg};
|
||||
position: relative;
|
||||
`,
|
||||
headerCell: css`
|
||||
padding: ${padding}px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: ${colors.textBlue};
|
||||
border-right: 1px solid ${theme.colors.panelBg};
|
||||
display: flex;
|
||||
${color ? `color: ${color};` : ''};
|
||||
${background ? `background: ${background};` : ''};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
|
||||
> div {
|
||||
padding-right: ${scollbarWidth + cellPadding}px;
|
||||
}
|
||||
`,
|
||||
headerCellLabel: css`
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
margin-right: ${theme.spacing.xs};
|
||||
`,
|
||||
headerFilter: css`
|
||||
label: headerFilter;
|
||||
cursor: pointer;
|
||||
`,
|
||||
row: css`
|
||||
label: row;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: ${rowHoverBg};
|
||||
&: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;
|
||||
}
|
||||
`,
|
||||
tableCellWrapper: css`
|
||||
border-right: 1px solid ${borderColor};
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
return {
|
||||
theme,
|
||||
cellHeight,
|
||||
buildCellContainerStyle,
|
||||
cellPadding,
|
||||
cellHeightInner: bodyFontSize * lineHeight,
|
||||
rowHeight: cellHeight + 2,
|
||||
table: css`
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
`,
|
||||
thead: css`
|
||||
label: thead;
|
||||
height: ${cellHeight}px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background: ${headerBg};
|
||||
position: relative;
|
||||
`,
|
||||
headerCell: css`
|
||||
padding: ${cellPadding}px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
color: ${colors.textBlue};
|
||||
border-right: 1px solid ${theme.colors.panelBg};
|
||||
display: flex;
|
||||
|
||||
> 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`
|
||||
label: resizeHandle;
|
||||
cursor: col-resize !important;
|
||||
display: inline-block;
|
||||
background: ${resizerColor};
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
border-radius: 3px;
|
||||
top: 0;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
touch-action: none;
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
`,
|
||||
headerCellLabel: css`
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: flex;
|
||||
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`
|
||||
label: headerFilter;
|
||||
cursor: pointer;
|
||||
`,
|
||||
row: css`
|
||||
label: row;
|
||||
border-bottom: 1px solid ${borderColor};
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
&:hover {
|
||||
background-color: ${rowHoverBg};
|
||||
}
|
||||
`,
|
||||
imageCell: css`
|
||||
height: 100%;
|
||||
`,
|
||||
resizeHandle: css`
|
||||
label: resizeHandle;
|
||||
cursor: col-resize !important;
|
||||
display: inline-block;
|
||||
background: ${resizerColor};
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease-in-out;
|
||||
width: 8px;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
border-radius: 3px;
|
||||
top: 0;
|
||||
z-index: ${theme.zIndex.dropdown};
|
||||
touch-action: none;
|
||||
|
||||
&:hover {
|
||||
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 { Field } from '@grafana/data';
|
||||
import { TableStyles } from './styles';
|
||||
import { FC } from 'react';
|
||||
import { CSSProperties, FC } from 'react';
|
||||
|
||||
export interface TableFieldOptions {
|
||||
width: number;
|
||||
@ -18,6 +18,7 @@ export enum TableCellDisplayMode {
|
||||
LcdGauge = 'lcd-gauge',
|
||||
JSONView = 'json-view',
|
||||
BasicGauge = 'basic',
|
||||
Image = 'image',
|
||||
}
|
||||
|
||||
export type FieldTextAlignment = 'auto' | 'left' | 'right' | 'center';
|
||||
@ -41,7 +42,9 @@ export interface TableSortByFieldState {
|
||||
|
||||
export interface TableCellProps extends CellProps<any> {
|
||||
tableStyles: TableStyles;
|
||||
cellProps: CSSProperties;
|
||||
field: Field;
|
||||
onCellFilterAdded: TableFilterActionCallback;
|
||||
}
|
||||
|
||||
export type CellComponent = FC<TableCellProps>;
|
||||
|
@ -66,7 +66,7 @@ describe('Table utils', () => {
|
||||
it('Should set textAlign to right for number values', () => {
|
||||
const data = getData();
|
||||
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 memoizeOne from 'memoize-one';
|
||||
import { css, cx } from 'emotion';
|
||||
import tinycolor from 'tinycolor2';
|
||||
import { ContentPosition, TextAlignProperty } from 'csstype';
|
||||
import { ContentPosition } from 'csstype';
|
||||
import {
|
||||
DataFrame,
|
||||
Field,
|
||||
@ -14,13 +12,13 @@ import {
|
||||
|
||||
import { DefaultCell } from './DefaultCell';
|
||||
import { BarGaugeCell } from './BarGaugeCell';
|
||||
import { TableCellDisplayMode, TableCellProps, TableFieldOptions } from './types';
|
||||
import { withTableStyles } from './withTableStyles';
|
||||
import { TableCellDisplayMode, TableFieldOptions } from './types';
|
||||
import { JSONViewCell } from './JSONViewCell';
|
||||
import { ImageCell } from './ImageCell';
|
||||
|
||||
export function getTextAlign(field?: Field): TextAlignProperty {
|
||||
export function getTextAlign(field?: Field): ContentPosition {
|
||||
if (!field) {
|
||||
return 'left';
|
||||
return 'flex-start';
|
||||
}
|
||||
|
||||
if (field.config.custom) {
|
||||
@ -28,19 +26,19 @@ export function getTextAlign(field?: Field): TextAlignProperty {
|
||||
|
||||
switch (custom.align) {
|
||||
case 'right':
|
||||
return 'right';
|
||||
return 'flex-end';
|
||||
case 'left':
|
||||
return 'left';
|
||||
return 'flex-start';
|
||||
case 'center':
|
||||
return 'center';
|
||||
}
|
||||
}
|
||||
|
||||
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[] {
|
||||
@ -68,6 +66,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||
return 'alphanumeric';
|
||||
}
|
||||
};
|
||||
|
||||
const Cell = getCellComponent(fieldTableOptions.displayMode, field);
|
||||
columns.push({
|
||||
Cell,
|
||||
@ -80,6 +79,7 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||
width: fieldTableOptions.width,
|
||||
minWidth: 50,
|
||||
filter: memoizeOne(filterByValue),
|
||||
justifyContent: getTextAlign(field),
|
||||
});
|
||||
}
|
||||
|
||||
@ -97,9 +97,10 @@ export function getColumns(data: DataFrame, availableWidth: number, columnMinWid
|
||||
function getCellComponent(displayMode: TableCellDisplayMode, field: Field) {
|
||||
switch (displayMode) {
|
||||
case TableCellDisplayMode.ColorText:
|
||||
return withTableStyles(DefaultCell, getTextColorStyle);
|
||||
case TableCellDisplayMode.ColorBackground:
|
||||
return withTableStyles(DefaultCell, getBackgroundColorStyle);
|
||||
return DefaultCell;
|
||||
case TableCellDisplayMode.Image:
|
||||
return ImageCell;
|
||||
case TableCellDisplayMode.LcdGauge:
|
||||
case TableCellDisplayMode.BasicGauge:
|
||||
case TableCellDisplayMode.GradientGauge:
|
||||
@ -115,58 +116,6 @@ function getCellComponent(displayMode: TableCellDisplayMode, field: Field) {
|
||||
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[]) {
|
||||
if (rows.length === 0) {
|
||||
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) {
|
||||
if (!field || rows.length === 0) {
|
||||
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.BasicGauge, label: 'Basic gauge' },
|
||||
{ value: TableCellDisplayMode.JSONView, label: 'JSON View' },
|
||||
{ value: TableCellDisplayMode.Image, label: 'Image' },
|
||||
],
|
||||
},
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user