mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 11:11:47 +08:00
Table: Highlight row on shared crosshair (#78392)
* bidirectional shared crosshair table WIP * add shared crosshair to table panel * lower around point threshold * add feature toggle * add index based verification * add adaptive threshold * switch to debounceTime * lower debounce to 100 * raise debounce back to 200 * revert azure dashboard * re-render only rows list on data hover event * further break down table component * refactor * raise debounce time * fix build
This commit is contained in:
@ -167,6 +167,7 @@ Experimental features might be changed or removed without prior notice.
|
||||
| `flameGraphItemCollapsing` | Allow collapsing of flame graph items |
|
||||
| `logRowsPopoverMenu` | Enable filtering menu displayed when text of a log line is selected |
|
||||
| `pluginsSkipHostEnvVars` | Disables passing host environment variable to plugin processes |
|
||||
| `tableSharedCrosshair` | Enables shared crosshair in table panel |
|
||||
| `regressionTransformation` | Enables regression analysis transformation |
|
||||
|
||||
## Development feature toggles
|
||||
|
@ -7,5 +7,11 @@ export * from './dimensions';
|
||||
export * from './ArrayDataFrame';
|
||||
export * from './DataFrameJSON';
|
||||
export * from './frameComparisons';
|
||||
export { anySeriesWithTimeField, isTimeSeriesFrame, isTimeSeriesFrames, isTimeSeriesField } from './utils';
|
||||
export {
|
||||
anySeriesWithTimeField,
|
||||
hasTimeField,
|
||||
isTimeSeriesFrame,
|
||||
isTimeSeriesFrames,
|
||||
isTimeSeriesField,
|
||||
} from './utils';
|
||||
export { StreamingDataFrame, StreamingFrameAction, type StreamingFrameOptions, closestIdx } from './StreamingDataFrame';
|
||||
|
@ -78,3 +78,11 @@ export function anySeriesWithTimeField(data: DataFrame[]) {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicates if there is any time field in the data frame
|
||||
* @param data
|
||||
*/
|
||||
export function hasTimeField(data: DataFrame): boolean {
|
||||
return data.fields.some((field) => field.type === FieldType.time);
|
||||
}
|
||||
|
@ -166,6 +166,7 @@ export interface FeatureToggles {
|
||||
alertingSimplifiedRouting?: boolean;
|
||||
logRowsPopoverMenu?: boolean;
|
||||
pluginsSkipHostEnvVars?: boolean;
|
||||
tableSharedCrosshair?: boolean;
|
||||
regressionTransformation?: boolean;
|
||||
displayAnonymousStats?: boolean;
|
||||
alertStateHistoryAnnotationsFromLoki?: boolean;
|
||||
|
301
packages/grafana-ui/src/components/Table/RowsList.tsx
Normal file
301
packages/grafana-ui/src/components/Table/RowsList.tsx
Normal file
@ -0,0 +1,301 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { CSSProperties, UIEventHandler, useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Cell, Row, TableState } from 'react-table';
|
||||
import { VariableSizeList } from 'react-window';
|
||||
import { Subscription, debounceTime } from 'rxjs';
|
||||
|
||||
import {
|
||||
DataFrame,
|
||||
DataHoverClearEvent,
|
||||
DataHoverEvent,
|
||||
Field,
|
||||
FieldType,
|
||||
TimeRange,
|
||||
hasTimeField,
|
||||
} from '@grafana/data';
|
||||
import { TableCellHeight } from '@grafana/schema';
|
||||
|
||||
import { useTheme2 } from '../../themes';
|
||||
import CustomScrollbar from '../CustomScrollbar/CustomScrollbar';
|
||||
import { usePanelContext } from '../PanelChrome';
|
||||
|
||||
import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow';
|
||||
import { TableCell } from './TableCell';
|
||||
import { TableStyles } from './styles';
|
||||
import { TableFilterActionCallback } from './types';
|
||||
import { calculateAroundPointThreshold, isPointTimeValAroundTableTimeVal } from './utils';
|
||||
|
||||
interface RowsListProps {
|
||||
data: DataFrame;
|
||||
rows: Row[];
|
||||
enableSharedCrosshair: boolean;
|
||||
headerHeight: number;
|
||||
rowHeight: number;
|
||||
itemCount: number;
|
||||
pageIndex: number;
|
||||
listHeight: number;
|
||||
width: number;
|
||||
cellHeight?: TableCellHeight;
|
||||
listRef: React.RefObject<VariableSizeList>;
|
||||
tableState: TableState;
|
||||
tableStyles: TableStyles;
|
||||
nestedDataField?: Field;
|
||||
prepareRow: (row: Row) => void;
|
||||
onCellFilterAdded?: TableFilterActionCallback;
|
||||
timeRange?: TimeRange;
|
||||
footerPaginationEnabled: boolean;
|
||||
}
|
||||
|
||||
export const RowsList = (props: RowsListProps) => {
|
||||
const {
|
||||
data,
|
||||
rows,
|
||||
headerHeight,
|
||||
footerPaginationEnabled,
|
||||
rowHeight,
|
||||
itemCount,
|
||||
pageIndex,
|
||||
tableState,
|
||||
prepareRow,
|
||||
onCellFilterAdded,
|
||||
width,
|
||||
cellHeight = TableCellHeight.Sm,
|
||||
timeRange,
|
||||
tableStyles,
|
||||
nestedDataField,
|
||||
listHeight,
|
||||
listRef,
|
||||
enableSharedCrosshair = false,
|
||||
} = props;
|
||||
|
||||
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(undefined);
|
||||
|
||||
const theme = useTheme2();
|
||||
const panelContext = usePanelContext();
|
||||
|
||||
const threshold = useMemo(() => {
|
||||
const timeField = data.fields.find((f) => f.type === FieldType.time);
|
||||
|
||||
if (!timeField) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return calculateAroundPointThreshold(timeField);
|
||||
}, [data]);
|
||||
|
||||
const onRowHover = useCallback(
|
||||
(idx: number, frame: DataFrame) => {
|
||||
if (!panelContext || !enableSharedCrosshair || !hasTimeField(frame)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const timeField: Field = frame!.fields.find((f) => f.type === FieldType.time)!;
|
||||
|
||||
panelContext.eventBus.publish(
|
||||
new DataHoverEvent({
|
||||
point: {
|
||||
time: timeField.values[idx],
|
||||
},
|
||||
})
|
||||
);
|
||||
},
|
||||
[enableSharedCrosshair, panelContext]
|
||||
);
|
||||
|
||||
const onRowLeave = useCallback(() => {
|
||||
if (!panelContext || !enableSharedCrosshair) {
|
||||
return;
|
||||
}
|
||||
|
||||
panelContext.eventBus.publish(new DataHoverClearEvent());
|
||||
}, [enableSharedCrosshair, panelContext]);
|
||||
|
||||
const onDataHoverEvent = useCallback(
|
||||
(evt: DataHoverEvent) => {
|
||||
if (evt.payload.point?.time && evt.payload.rowIndex !== undefined) {
|
||||
const timeField = data.fields.find((f) => f.type === FieldType.time);
|
||||
const time = timeField!.values[evt.payload.rowIndex];
|
||||
const pointTime = evt.payload.point.time;
|
||||
|
||||
// If the time value of the hovered point is around the time value of the
|
||||
// row with same index, highlight the row
|
||||
if (isPointTimeValAroundTableTimeVal(pointTime, time, threshold)) {
|
||||
setRowHighlightIndex(evt.payload.rowIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
// If the time value of the hovered point is not around the time value of the
|
||||
// row with same index, try to find a row with same time value
|
||||
const matchedRowIndex = timeField!.values.findIndex((t) =>
|
||||
isPointTimeValAroundTableTimeVal(pointTime, t, threshold)
|
||||
);
|
||||
|
||||
if (matchedRowIndex !== -1) {
|
||||
setRowHighlightIndex(matchedRowIndex);
|
||||
return;
|
||||
}
|
||||
|
||||
setRowHighlightIndex(undefined);
|
||||
}
|
||||
},
|
||||
[data.fields, threshold]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!panelContext || !enableSharedCrosshair || !hasTimeField(data) || footerPaginationEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const subs = new Subscription();
|
||||
|
||||
subs.add(
|
||||
panelContext.eventBus
|
||||
.getStream(DataHoverEvent)
|
||||
.pipe(debounceTime(250))
|
||||
.subscribe({
|
||||
next: (evt) => {
|
||||
if (panelContext.eventBus === evt.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
onDataHoverEvent(evt);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
subs.add(
|
||||
panelContext.eventBus
|
||||
.getStream(DataHoverClearEvent)
|
||||
.pipe(debounceTime(250))
|
||||
.subscribe({
|
||||
next: (evt) => {
|
||||
if (panelContext.eventBus === evt.origin) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRowHighlightIndex(undefined);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
subs.unsubscribe();
|
||||
};
|
||||
}, [data, enableSharedCrosshair, footerPaginationEnabled, onDataHoverEvent, panelContext]);
|
||||
|
||||
let scrollTop: number | undefined = undefined;
|
||||
if (rowHighlightIndex !== undefined) {
|
||||
const firstMatchedRowIndex = rows.findIndex((row) => row.index === rowHighlightIndex);
|
||||
|
||||
if (firstMatchedRowIndex !== -1) {
|
||||
scrollTop = headerHeight + (firstMatchedRowIndex - 1) * rowHeight;
|
||||
}
|
||||
}
|
||||
|
||||
const rowIndexForPagination = useCallback(
|
||||
(index: number) => {
|
||||
return tableState.pageIndex * tableState.pageSize + index;
|
||||
},
|
||||
[tableState.pageIndex, tableState.pageSize]
|
||||
);
|
||||
|
||||
const RenderRow = useCallback(
|
||||
({ index, style, rowHighlightIndex }: { index: number; style: CSSProperties; rowHighlightIndex?: number }) => {
|
||||
const indexForPagination = rowIndexForPagination(index);
|
||||
const row = rows[indexForPagination];
|
||||
|
||||
prepareRow(row);
|
||||
|
||||
const expandedRowStyle = tableState.expanded[row.index] ? css({ '&:hover': { background: 'inherit' } }) : {};
|
||||
|
||||
if (rowHighlightIndex !== undefined && row.index === rowHighlightIndex) {
|
||||
style = { ...style, backgroundColor: theme.components.table.rowHoverBackground };
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...row.getRowProps({ style })}
|
||||
className={cx(tableStyles.row, expandedRowStyle)}
|
||||
onMouseEnter={() => onRowHover(index, data)}
|
||||
onMouseLeave={onRowLeave}
|
||||
>
|
||||
{/*add the nested data to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/}
|
||||
{nestedDataField && tableState.expanded[row.index] && (
|
||||
<ExpandedRow
|
||||
nestedData={nestedDataField}
|
||||
tableStyles={tableStyles}
|
||||
rowIndex={index}
|
||||
width={width}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
)}
|
||||
{row.cells.map((cell: Cell, index: number) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
tableStyles={tableStyles}
|
||||
cell={cell}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
columnIndex={index}
|
||||
columnCount={row.cells.length}
|
||||
timeRange={timeRange}
|
||||
frame={data}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[
|
||||
cellHeight,
|
||||
data,
|
||||
nestedDataField,
|
||||
onCellFilterAdded,
|
||||
onRowHover,
|
||||
onRowLeave,
|
||||
prepareRow,
|
||||
rowIndexForPagination,
|
||||
rows,
|
||||
tableState.expanded,
|
||||
tableStyles,
|
||||
theme.components.table.rowHoverBackground,
|
||||
timeRange,
|
||||
width,
|
||||
]
|
||||
);
|
||||
|
||||
const getItemSize = (index: number): number => {
|
||||
const indexForPagination = rowIndexForPagination(index);
|
||||
const row = rows[indexForPagination];
|
||||
if (tableState.expanded[row.index] && nestedDataField) {
|
||||
return getExpandedRowHeight(nestedDataField, index, tableStyles);
|
||||
}
|
||||
|
||||
return tableStyles.rowHeight;
|
||||
};
|
||||
|
||||
const handleScroll: UIEventHandler = (event) => {
|
||||
const { scrollTop } = event.currentTarget;
|
||||
|
||||
if (listRef.current !== null) {
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true} scrollTop={scrollTop}>
|
||||
<VariableSizeList
|
||||
// This component needs an unmount/remount when row height or page changes
|
||||
key={rowHeight + pageIndex}
|
||||
height={listHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={getItemSize}
|
||||
width={'100%'}
|
||||
ref={listRef}
|
||||
style={{ overflow: undefined }}
|
||||
>
|
||||
{({ index, style }) => RenderRow({ index, style, rowHighlightIndex })}
|
||||
</VariableSizeList>
|
||||
</CustomScrollbar>
|
||||
</>
|
||||
);
|
||||
};
|
@ -1,7 +1,5 @@
|
||||
import { css, cx } from '@emotion/css';
|
||||
import React, { CSSProperties, memo, useCallback, useEffect, useMemo, useRef, useState, UIEventHandler } from 'react';
|
||||
import React, { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Cell,
|
||||
useAbsoluteLayout,
|
||||
useExpanded,
|
||||
useFilters,
|
||||
@ -20,10 +18,9 @@ import { useTheme2 } from '../../themes';
|
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||
import { Pagination } from '../Pagination/Pagination';
|
||||
|
||||
import { getExpandedRowHeight, ExpandedRow } from './ExpandedRow';
|
||||
import { FooterRow } from './FooterRow';
|
||||
import { HeaderRow } from './HeaderRow';
|
||||
import { TableCell } from './TableCell';
|
||||
import { RowsList } from './RowsList';
|
||||
import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks';
|
||||
import { getInitialState, useTableStateReducer } from './reducer';
|
||||
import { useTableStyles } from './styles';
|
||||
@ -50,6 +47,7 @@ export const Table = memo((props: Props) => {
|
||||
enablePagination,
|
||||
cellHeight = TableCellHeight.Sm,
|
||||
timeRange,
|
||||
enableSharedCrosshair = false,
|
||||
} = props;
|
||||
|
||||
const listRef = useRef<VariableSizeList>(null);
|
||||
@ -231,64 +229,6 @@ export const Table = memo((props: Props) => {
|
||||
useResetVariableListSizeCache(extendedState, listRef, data);
|
||||
useFixScrollbarContainer(variableSizeListScrollbarRef, tableDivRef);
|
||||
|
||||
const rowIndexForPagination = useCallback(
|
||||
(index: number) => {
|
||||
return state.pageIndex * state.pageSize + index;
|
||||
},
|
||||
[state.pageIndex, state.pageSize]
|
||||
);
|
||||
|
||||
const RenderRow = useCallback(
|
||||
({ index, style }: { index: number; style: CSSProperties }) => {
|
||||
const indexForPagination = rowIndexForPagination(index);
|
||||
const row = rows[indexForPagination];
|
||||
|
||||
prepareRow(row);
|
||||
|
||||
const expandedRowStyle = state.expanded[row.index] ? css({ '&:hover': { background: 'inherit' } }) : {};
|
||||
|
||||
return (
|
||||
<div {...row.getRowProps({ style })} className={cx(tableStyles.row, expandedRowStyle)}>
|
||||
{/*add the nested data to the DOM first to prevent a 1px border CSS issue on the last cell of the row*/}
|
||||
{nestedDataField && state.expanded[row.index] && (
|
||||
<ExpandedRow
|
||||
nestedData={nestedDataField}
|
||||
tableStyles={tableStyles}
|
||||
rowIndex={index}
|
||||
width={width}
|
||||
cellHeight={cellHeight}
|
||||
/>
|
||||
)}
|
||||
{row.cells.map((cell: Cell, index: number) => (
|
||||
<TableCell
|
||||
key={index}
|
||||
tableStyles={tableStyles}
|
||||
cell={cell}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
columnIndex={index}
|
||||
columnCount={row.cells.length}
|
||||
timeRange={timeRange}
|
||||
frame={data}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[
|
||||
rowIndexForPagination,
|
||||
rows,
|
||||
prepareRow,
|
||||
state.expanded,
|
||||
tableStyles,
|
||||
nestedDataField,
|
||||
width,
|
||||
cellHeight,
|
||||
onCellFilterAdded,
|
||||
timeRange,
|
||||
data,
|
||||
]
|
||||
);
|
||||
|
||||
const onNavigate = useCallback(
|
||||
(toPage: number) => {
|
||||
gotoPage(toPage - 1);
|
||||
@ -322,24 +262,6 @@ export const Table = memo((props: Props) => {
|
||||
);
|
||||
}
|
||||
|
||||
const getItemSize = (index: number): number => {
|
||||
const indexForPagination = rowIndexForPagination(index);
|
||||
const row = rows[indexForPagination];
|
||||
if (state.expanded[row.index] && nestedDataField) {
|
||||
return getExpandedRowHeight(nestedDataField, index, tableStyles);
|
||||
}
|
||||
|
||||
return tableStyles.rowHeight;
|
||||
};
|
||||
|
||||
const handleScroll: UIEventHandler = (event) => {
|
||||
const { scrollTop } = event.currentTarget;
|
||||
|
||||
if (listRef.current !== null) {
|
||||
listRef.current.scrollTo(scrollTop);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
{...getTableProps()}
|
||||
@ -356,20 +278,26 @@ export const Table = memo((props: Props) => {
|
||||
)}
|
||||
{itemCount > 0 ? (
|
||||
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}>
|
||||
<CustomScrollbar onScroll={handleScroll} hideHorizontalTrack={true}>
|
||||
<VariableSizeList
|
||||
// This component needs an unmount/remount when row height or page changes
|
||||
key={tableStyles.rowHeight + state.pageIndex}
|
||||
height={listHeight}
|
||||
<RowsList
|
||||
data={data}
|
||||
rows={rows}
|
||||
width={width}
|
||||
cellHeight={cellHeight}
|
||||
headerHeight={headerHeight}
|
||||
rowHeight={tableStyles.rowHeight}
|
||||
itemCount={itemCount}
|
||||
itemSize={getItemSize}
|
||||
width={'100%'}
|
||||
ref={listRef}
|
||||
style={{ overflow: undefined }}
|
||||
>
|
||||
{RenderRow}
|
||||
</VariableSizeList>
|
||||
</CustomScrollbar>
|
||||
pageIndex={state.pageIndex}
|
||||
listHeight={listHeight}
|
||||
listRef={listRef}
|
||||
tableState={state}
|
||||
prepareRow={prepareRow}
|
||||
timeRange={timeRange}
|
||||
onCellFilterAdded={onCellFilterAdded}
|
||||
nestedDataField={nestedDataField}
|
||||
tableStyles={tableStyles}
|
||||
footerPaginationEnabled={Boolean(enablePagination)}
|
||||
enableSharedCrosshair={enableSharedCrosshair}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
|
||||
|
@ -94,6 +94,7 @@ export interface Props {
|
||||
cellHeight?: schema.TableCellHeight;
|
||||
/** @alpha Used by SparklineCell when provided */
|
||||
timeRange?: TimeRange;
|
||||
enableSharedCrosshair?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -533,3 +533,33 @@ export function getAlignmentFactor(
|
||||
return alignmentFactor;
|
||||
}
|
||||
}
|
||||
|
||||
// since the conversion from timeseries panel crosshair to time is pixel based, we need
|
||||
// to set a threshold where the table row highlights when the crosshair is hovered over a certain point
|
||||
// because multiple pixels (converted to times) may represent the same point/row in table
|
||||
export function isPointTimeValAroundTableTimeVal(pointTime: number, rowTime: number, threshold: number) {
|
||||
return Math.abs(Math.floor(pointTime) - rowTime) < threshold;
|
||||
}
|
||||
|
||||
// calculate the threshold for which we consider a point in a chart
|
||||
// to match a row in a table based on a time value
|
||||
export function calculateAroundPointThreshold(timeField: Field): number {
|
||||
let max = -Number.MAX_VALUE;
|
||||
let min = Number.MAX_VALUE;
|
||||
|
||||
if (timeField.values.length < 2) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
for (let i = 0; i < timeField.values.length; i++) {
|
||||
const value = timeField.values[i];
|
||||
if (value > max) {
|
||||
max = value;
|
||||
}
|
||||
if (value < min) {
|
||||
min = value;
|
||||
}
|
||||
}
|
||||
|
||||
return (max - min) / timeField.values.length;
|
||||
}
|
||||
|
@ -1241,6 +1241,14 @@ var (
|
||||
Owner: grafanaPluginsPlatformSquad,
|
||||
Created: time.Date(2023, time.November, 15, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "tableSharedCrosshair",
|
||||
Description: "Enables shared crosshair in table panel",
|
||||
FrontendOnly: true,
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaBiSquad,
|
||||
Created: time.Date(2023, time.December, 12, 12, 0, 0, 0, time.UTC),
|
||||
},
|
||||
{
|
||||
Name: "regressionTransformation",
|
||||
Description: "Enables regression analysis transformation",
|
||||
|
@ -147,6 +147,7 @@ datatrails,experimental,@grafana/dashboards-squad,2023-11-15,false,false,false,t
|
||||
alertingSimplifiedRouting,experimental,@grafana/alerting-squad,2023-11-10,false,false,false,false
|
||||
logRowsPopoverMenu,experimental,@grafana/observability-logs,2023-11-16,false,false,false,true
|
||||
pluginsSkipHostEnvVars,experimental,@grafana/plugins-platform-backend,2023-11-15,false,false,false,false
|
||||
tableSharedCrosshair,experimental,@grafana/grafana-bi-squad,2023-12-12,false,false,false,true
|
||||
regressionTransformation,experimental,@grafana/grafana-bi-squad,2023-11-24,false,false,false,true
|
||||
displayAnonymousStats,GA,@grafana/identity-access-team,2023-11-29,false,false,false,true
|
||||
alertStateHistoryAnnotationsFromLoki,experimental,@grafana/alerting-squad,2023-11-30,false,false,true,false
|
||||
|
|
@ -599,6 +599,10 @@ const (
|
||||
// Disables passing host environment variable to plugin processes
|
||||
FlagPluginsSkipHostEnvVars = "pluginsSkipHostEnvVars"
|
||||
|
||||
// FlagTableSharedCrosshair
|
||||
// Enables shared crosshair in table panel
|
||||
FlagTableSharedCrosshair = "tableSharedCrosshair"
|
||||
|
||||
// FlagRegressionTransformation
|
||||
// Enables regression analysis transformation
|
||||
FlagRegressionTransformation = "regressionTransformation"
|
||||
|
@ -1,8 +1,15 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { DataFrame, FieldMatcherID, getFrameDisplayName, PanelProps, SelectableValue } from '@grafana/data';
|
||||
import { PanelDataErrorView } from '@grafana/runtime';
|
||||
import {
|
||||
DashboardCursorSync,
|
||||
DataFrame,
|
||||
FieldMatcherID,
|
||||
getFrameDisplayName,
|
||||
PanelProps,
|
||||
SelectableValue,
|
||||
} from '@grafana/data';
|
||||
import { config, PanelDataErrorView } from '@grafana/runtime';
|
||||
import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui';
|
||||
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
|
||||
|
||||
@ -37,6 +44,8 @@ export function TablePanel(props: Props) {
|
||||
tableHeight = height - inputHeight - padding;
|
||||
}
|
||||
|
||||
const enableSharedCrosshair = panelContext.sync && panelContext.sync() !== DashboardCursorSync.Off;
|
||||
|
||||
const tableElement = (
|
||||
<Table
|
||||
height={tableHeight}
|
||||
@ -53,6 +62,7 @@ export function TablePanel(props: Props) {
|
||||
enablePagination={options.footer?.enablePagination}
|
||||
cellHeight={options.cellHeight}
|
||||
timeRange={timeRange}
|
||||
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
|
||||
/>
|
||||
);
|
||||
|
||||
|
Reference in New Issue
Block a user