Table: Fix inspect drawer disappears unexpectedly (#99025)

fix(table): inspect drawer disappears unexpectedly
pull/99101/head
Ihor Yeromin 11 months ago committed by GitHub
parent b532df36c4
commit 019ee9c2d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .betterer.results
  2. 69
      packages/grafana-ui/src/components/Table/CellActions.tsx
  3. 22
      packages/grafana-ui/src/components/Table/DefaultCell.tsx
  4. 12
      packages/grafana-ui/src/components/Table/RowsList.tsx
  5. 136
      packages/grafana-ui/src/components/Table/Table.tsx
  6. 5
      packages/grafana-ui/src/components/Table/TableCell.tsx
  7. 5
      packages/grafana-ui/src/components/Table/types.ts

@ -830,7 +830,8 @@ exports[`better eslint`] = {
], ],
"packages/grafana-ui/src/components/Table/types.ts:5381": [ "packages/grafana-ui/src/components/Table/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"] [0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
], ],
"packages/grafana-ui/src/components/Table/utils.ts:5381": [ "packages/grafana-ui/src/components/Table/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"], [0, 0, 0, "Unexpected any. Specify a different type.", "0"],

@ -1,4 +1,4 @@
import { useCallback, useState } from 'react'; import { useCallback } from 'react';
import * as React from 'react'; import * as React from 'react';
import { IconSize } from '../../types/icon'; import { IconSize } from '../../types/icon';
@ -6,7 +6,7 @@ import { IconButton } from '../IconButton/IconButton';
import { Stack } from '../Layout/Stack/Stack'; import { Stack } from '../Layout/Stack/Stack';
import { TooltipPlacement } from '../Tooltip'; import { TooltipPlacement } from '../Tooltip';
import { TableCellInspector, TableCellInspectorMode } from './TableCellInspector'; import { TableCellInspectorMode } from './TableCellInspector';
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps } from './types'; import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps } from './types';
import { getTextAlign } from './utils'; import { getTextAlign } from './utils';
@ -20,9 +20,14 @@ interface CommonButtonProps {
tooltipPlacement: TooltipPlacement; tooltipPlacement: TooltipPlacement;
} }
export function CellActions({ field, cell, previewMode, showFilters, onCellFilterAdded }: CellActionProps) { export function CellActions({
const [isInspecting, setIsInspecting] = useState(false); field,
cell,
previewMode,
showFilters,
onCellFilterAdded,
setInspectCell,
}: CellActionProps) {
const isRightAligned = getTextAlign(field) === 'flex-end'; const isRightAligned = getTextAlign(field) === 'flex-end';
const inspectEnabled = Boolean(field.config.custom?.inspect); const inspectEnabled = Boolean(field.config.custom?.inspect);
const commonButtonProps: CommonButtonProps = { const commonButtonProps: CommonButtonProps = {
@ -48,37 +53,27 @@ export function CellActions({ field, cell, previewMode, showFilters, onCellFilte
); );
return ( return (
<> <div className={`cellActions${isRightAligned ? ' cellActionsLeft' : ''}`}>
<div className={`cellActions${isRightAligned ? ' cellActionsLeft' : ''}`}> <Stack gap={0.5}>
<Stack gap={0.5}> {inspectEnabled && (
{inspectEnabled && ( <IconButton
<IconButton name="eye"
name="eye" tooltip="Inspect value"
tooltip="Inspect value" onClick={() => {
onClick={() => { if (setInspectCell) {
setIsInspecting(true); setInspectCell({ value: cell.value, mode: previewMode });
}} }
{...commonButtonProps} }}
/> {...commonButtonProps}
)} />
{showFilters && ( )}
<IconButton name={'search-plus'} onClick={onFilterFor} tooltip="Filter for value" {...commonButtonProps} /> {showFilters && (
)} <IconButton name={'search-plus'} onClick={onFilterFor} tooltip="Filter for value" {...commonButtonProps} />
{showFilters && ( )}
<IconButton name={'search-minus'} onClick={onFilterOut} tooltip="Filter out value" {...commonButtonProps} /> {showFilters && (
)} <IconButton name={'search-minus'} onClick={onFilterOut} tooltip="Filter out value" {...commonButtonProps} />
</Stack> )}
</div> </Stack>
</div>
{isInspecting && (
<TableCellInspector
mode={previewMode}
value={cell.value}
onDismiss={() => {
setIsInspecting(false);
}}
/>
)}
</>
); );
} }

@ -1,5 +1,5 @@
import { cx } from '@emotion/css'; import { cx } from '@emotion/css';
import { ReactElement, useState } from 'react'; import { ReactElement } from 'react';
import * as React from 'react'; import * as React from 'react';
import { DisplayValue, formattedValueToString } from '@grafana/data'; import { DisplayValue, formattedValueToString } from '@grafana/data';
@ -28,18 +28,10 @@ export const DefaultCell = (props: TableCellProps) => {
const hasLinks = Boolean(getCellLinks(field, row)?.length); const hasLinks = Boolean(getCellLinks(field, row)?.length);
const hasActions = Boolean(actions?.length); const hasActions = Boolean(actions?.length);
const clearButtonStyle = useStyles2(clearLinkButtonStyles); const clearButtonStyle = useStyles2(clearLinkButtonStyles);
const [hover, setHover] = useState(false);
let value: string | ReactElement; let value: string | ReactElement;
const OG_TWEET_LENGTH = 140; // 🙏 const OG_TWEET_LENGTH = 140; // 🙏
const onMouseLeave = () => {
setHover(false);
};
const onMouseEnter = () => {
setHover(true);
};
if (cellOptions.type === TableCellDisplayMode.Custom) { if (cellOptions.type === TableCellDisplayMode.Custom) {
const CustomCellComponent: React.ComponentType<CustomCellRendererProps> = cellOptions.cellComponent; const CustomCellComponent: React.ComponentType<CustomCellRendererProps> = cellOptions.cellComponent;
value = <CustomCellComponent field={field} value={cell.value} rowIndex={row.index} frame={frame} />; value = <CustomCellComponent field={field} value={cell.value} rowIndex={row.index} frame={frame} />;
@ -88,13 +80,7 @@ export const DefaultCell = (props: TableCellProps) => {
const { key, ...rest } = cellProps; const { key, ...rest } = cellProps;
return ( return (
<div <div key={key} {...rest} className={cellStyle}>
key={key}
{...rest}
onMouseEnter={showActions ? onMouseEnter : undefined}
onMouseLeave={showActions ? onMouseLeave : undefined}
className={cellStyle}
>
{hasLinks || hasActions ? ( {hasLinks || hasActions ? (
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}> <DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
{(api) => { {(api) => {
@ -118,9 +104,7 @@ export const DefaultCell = (props: TableCellProps) => {
<div className={tableStyles.cellText}>{value}</div> <div className={tableStyles.cellText}>{value}</div>
)} )}
{hover && showActions && ( {showActions && <CellActions {...props} previewMode={TableCellInspectorMode.text} showFilters={showFilters} />}
<CellActions {...props} previewMode={TableCellInspectorMode.text} showFilters={showFilters} />
)}
</div> </div>
); );
}; };

@ -24,7 +24,13 @@ import { usePanelContext } from '../PanelChrome';
import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow'; import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow';
import { TableCell } from './TableCell'; import { TableCell } from './TableCell';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
import { CellColors, GetActionsFunction, TableFieldOptions, TableFilterActionCallback } from './types'; import {
CellColors,
GetActionsFunction,
TableFieldOptions,
TableFilterActionCallback,
TableInspectCellCallback,
} from './types';
import { import {
calculateAroundPointThreshold, calculateAroundPointThreshold,
getCellColors, getCellColors,
@ -57,6 +63,7 @@ interface RowsListProps {
textWrapField?: Field; textWrapField?: Field;
getActions?: GetActionsFunction; getActions?: GetActionsFunction;
replaceVariables?: InterpolateFunction; replaceVariables?: InterpolateFunction;
setInspectCell?: TableInspectCellCallback;
} }
export const RowsList = (props: RowsListProps) => { export const RowsList = (props: RowsListProps) => {
@ -85,6 +92,7 @@ export const RowsList = (props: RowsListProps) => {
textWrapField, textWrapField,
getActions, getActions,
replaceVariables, replaceVariables,
setInspectCell,
} = props; } = props;
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex); const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
@ -346,6 +354,7 @@ export const RowsList = (props: RowsListProps) => {
height={Number(style.height)} height={Number(style.height)}
getActions={getActions} getActions={getActions}
replaceVariables={replaceVariables} replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/> />
))} ))}
</div> </div>
@ -374,6 +383,7 @@ export const RowsList = (props: RowsListProps) => {
timeRange, timeRange,
getActions, getActions,
replaceVariables, replaceVariables,
setInspectCell,
] ]
); );

@ -22,10 +22,11 @@ import { Pagination } from '../Pagination/Pagination';
import { FooterRow } from './FooterRow'; import { FooterRow } from './FooterRow';
import { HeaderRow } from './HeaderRow'; import { HeaderRow } from './HeaderRow';
import { RowsList } from './RowsList'; import { RowsList } from './RowsList';
import { TableCellInspector } from './TableCellInspector';
import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks'; import { useFixScrollbarContainer, useResetVariableListSizeCache } from './hooks';
import { getInitialState, useTableStateReducer } from './reducer'; import { getInitialState, useTableStateReducer } from './reducer';
import { useTableStyles } from './styles'; import { useTableStyles } from './styles';
import { FooterItem, GrafanaTableState, Props } from './types'; import { FooterItem, GrafanaTableState, InspectCell, Props } from './types';
import { import {
getColumns, getColumns,
sortCaseInsensitive, sortCaseInsensitive,
@ -72,6 +73,7 @@ export const Table = memo((props: Props) => {
const headerHeight = noHeader ? 0 : tableStyles.rowHeight; const headerHeight = noHeader ? 0 : tableStyles.rowHeight;
const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues); const [footerItems, setFooterItems] = useState<FooterItem[] | undefined>(footerValues);
const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT; const noValuesDisplayText = fieldConfig?.defaults?.noValue ?? NO_DATA_TEXT;
const [inspectCell, setInspectCell] = useState<InspectCell | null>(null);
const footerHeight = useMemo(() => { const footerHeight = useMemo(() => {
const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT; const EXTENDED_ROW_HEIGHT = FOOTER_ROW_HEIGHT;
@ -324,66 +326,82 @@ export const Table = memo((props: Props) => {
} }
return ( return (
<div <>
{...getTableProps()} <div
className={tableStyles.table} {...getTableProps()}
aria-label={ariaLabel} className={tableStyles.table}
role="table" aria-label={ariaLabel}
ref={tableDivRef} role="table"
style={{ width, height }} ref={tableDivRef}
> style={{ width, height }}
<CustomScrollbar hideVerticalTrack={true}> >
<div className={tableStyles.tableContentWrapper(totalColumnsWidth)}> <CustomScrollbar hideVerticalTrack={true}>
{!noHeader && ( <div className={tableStyles.tableContentWrapper(totalColumnsWidth)}>
<HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} /> {!noHeader && (
)} <HeaderRow headerGroups={headerGroups} showTypeIcons={showTypeIcons} tableStyles={tableStyles} />
{itemCount > 0 ? ( )}
<div data-testid={selectors.components.Panels.Visualization.Table.body} ref={variableSizeListScrollbarRef}> {itemCount > 0 ? (
<RowsList <div
headerGroups={headerGroups} data-testid={selectors.components.Panels.Visualization.Table.body}
data={data} ref={variableSizeListScrollbarRef}
rows={rows} >
width={width} <RowsList
cellHeight={cellHeight} headerGroups={headerGroups}
headerHeight={headerHeight} data={data}
rowHeight={tableStyles.rowHeight} rows={rows}
itemCount={itemCount} width={width}
pageIndex={state.pageIndex} cellHeight={cellHeight}
listHeight={listHeight} headerHeight={headerHeight}
listRef={listRef} rowHeight={tableStyles.rowHeight}
tableState={state} itemCount={itemCount}
prepareRow={prepareRow} pageIndex={state.pageIndex}
timeRange={timeRange} listHeight={listHeight}
onCellFilterAdded={onCellFilterAdded} listRef={listRef}
nestedDataField={nestedDataField} tableState={state}
prepareRow={prepareRow}
timeRange={timeRange}
onCellFilterAdded={onCellFilterAdded}
nestedDataField={nestedDataField}
tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
setInspectCell={setInspectCell}
/>
</div>
) : (
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}>
{noValuesDisplayText}
</div>
)}
{footerItems && (
<FooterRow
isPaginationVisible={Boolean(enablePagination)}
footerValues={footerItems}
footerGroups={footerGroups}
totalColumnsWidth={totalColumnsWidth}
tableStyles={tableStyles} tableStyles={tableStyles}
footerPaginationEnabled={Boolean(enablePagination)}
enableSharedCrosshair={enableSharedCrosshair}
initialRowIndex={initialRowIndex}
longestField={longestField}
textWrapField={textWrapField}
getActions={getActions}
replaceVariables={replaceVariables}
/> />
</div> )}
) : ( </div>
<div style={{ height: height - headerHeight, width }} className={tableStyles.noData}> </CustomScrollbar>
{noValuesDisplayText} {paginationEl}
</div> </div>
)}
{footerItems && ( {inspectCell !== null && (
<FooterRow <TableCellInspector
isPaginationVisible={Boolean(enablePagination)} mode={inspectCell.mode}
footerValues={footerItems} value={inspectCell.value}
footerGroups={footerGroups} onDismiss={() => {
totalColumnsWidth={totalColumnsWidth} setInspectCell(null);
tableStyles={tableStyles} }}
/> />
)} )}
</div> </>
</CustomScrollbar>
{paginationEl}
</div>
); );
}); });

@ -3,7 +3,7 @@ import { Cell } from 'react-table';
import { TimeRange, DataFrame, InterpolateFunction } from '@grafana/data'; import { TimeRange, DataFrame, InterpolateFunction } from '@grafana/data';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback } from './types'; import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback, TableInspectCellCallback } from './types';
export interface Props { export interface Props {
cell: Cell; cell: Cell;
@ -20,6 +20,7 @@ export interface Props {
height?: number; height?: number;
getActions?: GetActionsFunction; getActions?: GetActionsFunction;
replaceVariables?: InterpolateFunction; replaceVariables?: InterpolateFunction;
setInspectCell?: TableInspectCellCallback;
} }
export const TableCell = ({ export const TableCell = ({
@ -35,6 +36,7 @@ export const TableCell = ({
height, height,
getActions, getActions,
replaceVariables, replaceVariables,
setInspectCell,
}: Props) => { }: Props) => {
const cellProps = cell.getCellProps(); const cellProps = cell.getCellProps();
const field = (cell.column as unknown as GrafanaTableColumn).field; const field = (cell.column as unknown as GrafanaTableColumn).field;
@ -78,6 +80,7 @@ export const TableCell = ({
textWrapped, textWrapped,
height, height,
actions, actions,
setInspectCell,
})} })}
</> </>
); );

@ -14,6 +14,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import * as schema from '@grafana/schema'; import * as schema from '@grafana/schema';
import { TableCellInspectorMode } from './TableCellInspector';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
export { export {
@ -33,6 +34,8 @@ export interface TableRow {
[x: string]: any; [x: string]: any;
} }
export type InspectCell = { value: any; mode: TableCellInspectorMode };
export const FILTER_FOR_OPERATOR = '='; export const FILTER_FOR_OPERATOR = '=';
export const FILTER_OUT_OPERATOR = '!='; export const FILTER_OUT_OPERATOR = '!=';
export type AdHocFilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR; export type AdHocFilterOperator = typeof FILTER_FOR_OPERATOR | typeof FILTER_OUT_OPERATOR;
@ -40,6 +43,7 @@ export type AdHocFilterItem = { key: string; value: string; operator: AdHocFilte
export type TableFilterActionCallback = (item: AdHocFilterItem) => void; export type TableFilterActionCallback = (item: AdHocFilterItem) => void;
export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void; export type TableColumnResizeActionCallback = (fieldDisplayName: string, width: number) => void;
export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void; export type TableSortByActionCallback = (state: TableSortByFieldState[]) => void;
export type TableInspectCellCallback = (state: InspectCell) => void;
export interface TableSortByFieldState { export interface TableSortByFieldState {
displayName: string; displayName: string;
@ -54,6 +58,7 @@ export interface TableCellProps extends CellProps<any> {
innerWidth: number; innerWidth: number;
frame: DataFrame; frame: DataFrame;
actions?: ActionModel[]; actions?: ActionModel[];
setInspectCell?: TableInspectCellCallback;
} }
export type CellComponent = FC<TableCellProps>; export type CellComponent = FC<TableCellProps>;

Loading…
Cancel
Save