mirror of https://github.com/grafana/grafana
TablePanel: Add cell inspect option (#45620)
* TablePanel: Add cell preview option * Review comments * Change modal title * Review * Review 2 * Docspull/45842/head
parent
c014f8b806
commit
eb537e2efd
@ -0,0 +1,70 @@ |
|||||||
|
import React, { useCallback, useState } from 'react'; |
||||||
|
import { IconSize } from '../../types/icon'; |
||||||
|
import { IconButton } from '../IconButton/IconButton'; |
||||||
|
import { HorizontalGroup } from '../Layout/Layout'; |
||||||
|
import { TooltipPlacement } from '../Tooltip'; |
||||||
|
import { TableCellInspectModal } from './TableCellInspectModal'; |
||||||
|
import { FILTER_FOR_OPERATOR, FILTER_OUT_OPERATOR, TableCellProps, TableFieldOptions } from './types'; |
||||||
|
import { getTextAlign } from './utils'; |
||||||
|
|
||||||
|
interface CellActionProps extends TableCellProps { |
||||||
|
previewMode: 'text' | 'code'; |
||||||
|
} |
||||||
|
|
||||||
|
export function CellActions({ field, cell, previewMode, onCellFilterAdded }: CellActionProps) { |
||||||
|
const [isInspecting, setIsInspecting] = useState(false); |
||||||
|
|
||||||
|
const isRightAligned = getTextAlign(field) === 'flex-end'; |
||||||
|
const showFilters = Boolean(field.config.filterable) && cell.value !== undefined; |
||||||
|
const inspectEnabled = Boolean((field.config.custom as TableFieldOptions)?.inspect); |
||||||
|
const commonButtonProps = { |
||||||
|
size: 'sm' as IconSize, |
||||||
|
tooltipPlacement: 'top' as TooltipPlacement, |
||||||
|
}; |
||||||
|
|
||||||
|
const onFilterFor = useCallback( |
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => |
||||||
|
onCellFilterAdded({ key: field.name, operator: FILTER_FOR_OPERATOR, value: cell.value }), |
||||||
|
[cell, field, onCellFilterAdded] |
||||||
|
); |
||||||
|
const onFilterOut = useCallback( |
||||||
|
(event: React.MouseEvent<HTMLButtonElement>) => |
||||||
|
onCellFilterAdded({ key: field.name, operator: FILTER_OUT_OPERATOR, value: cell.value }), |
||||||
|
[cell, field, onCellFilterAdded] |
||||||
|
); |
||||||
|
|
||||||
|
return ( |
||||||
|
<> |
||||||
|
<div className={`cellActions ${isRightAligned ? 'cellActionsLeft' : ''}`}> |
||||||
|
<HorizontalGroup spacing="xs"> |
||||||
|
{inspectEnabled && ( |
||||||
|
<IconButton |
||||||
|
name="eye" |
||||||
|
tooltip="Inspect value" |
||||||
|
onClick={() => { |
||||||
|
setIsInspecting(true); |
||||||
|
}} |
||||||
|
{...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} /> |
||||||
|
)} |
||||||
|
</HorizontalGroup> |
||||||
|
</div> |
||||||
|
|
||||||
|
{isInspecting && ( |
||||||
|
<TableCellInspectModal |
||||||
|
mode={previewMode} |
||||||
|
value={cell.value} |
||||||
|
onDismiss={() => { |
||||||
|
setIsInspecting(false); |
||||||
|
}} |
||||||
|
/> |
||||||
|
)} |
||||||
|
</> |
||||||
|
); |
||||||
|
} |
@ -1,31 +0,0 @@ |
|||||||
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> |
|
||||||
); |
|
||||||
}; |
|
@ -0,0 +1,75 @@ |
|||||||
|
import { isString } from 'lodash'; |
||||||
|
import React, { useEffect, useState } from 'react'; |
||||||
|
import { ClipboardButton } from '../ClipboardButton/ClipboardButton'; |
||||||
|
import { Icon } from '../Icon/Icon'; |
||||||
|
import { Modal } from '../Modal/Modal'; |
||||||
|
import { CodeEditor } from '../Monaco/CodeEditor'; |
||||||
|
|
||||||
|
interface TableCellInspectModalProps { |
||||||
|
value: any; |
||||||
|
onDismiss: () => void; |
||||||
|
mode: 'code' | 'text'; |
||||||
|
} |
||||||
|
|
||||||
|
export function TableCellInspectModal({ value, onDismiss, mode }: TableCellInspectModalProps) { |
||||||
|
const [isInClipboard, setIsInClipboard] = useState(false); |
||||||
|
const timeoutRef = React.useRef<number>(); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
if (isInClipboard) { |
||||||
|
timeoutRef.current = window.setTimeout(() => { |
||||||
|
setIsInClipboard(false); |
||||||
|
}, 2000); |
||||||
|
} |
||||||
|
|
||||||
|
return () => { |
||||||
|
if (timeoutRef.current) { |
||||||
|
window.clearTimeout(timeoutRef.current); |
||||||
|
} |
||||||
|
}; |
||||||
|
}, [isInClipboard]); |
||||||
|
|
||||||
|
let displayValue = value; |
||||||
|
if (isString(value)) { |
||||||
|
try { |
||||||
|
value = JSON.parse(value); |
||||||
|
} catch {} // ignore errors
|
||||||
|
} else { |
||||||
|
displayValue = JSON.stringify(value, null, ' '); |
||||||
|
} |
||||||
|
let text = displayValue; |
||||||
|
|
||||||
|
if (mode === 'code') { |
||||||
|
text = JSON.stringify(value, null, ' '); |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<Modal onDismiss={onDismiss} isOpen={true} title="Inspect value"> |
||||||
|
{mode === 'code' ? ( |
||||||
|
<CodeEditor |
||||||
|
width="100%" |
||||||
|
height={500} |
||||||
|
language="json" |
||||||
|
showLineNumbers={true} |
||||||
|
showMiniMap={(text && text.length) > 100} |
||||||
|
value={text} |
||||||
|
readOnly={true} |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<pre>{text}</pre> |
||||||
|
)} |
||||||
|
<Modal.ButtonRow> |
||||||
|
<ClipboardButton getText={() => text} onClipboardCopy={() => setIsInClipboard(true)}> |
||||||
|
{!isInClipboard ? ( |
||||||
|
'Copy to Clipboard' |
||||||
|
) : ( |
||||||
|
<> |
||||||
|
<Icon name="check" /> |
||||||
|
Copied to clipboard |
||||||
|
</> |
||||||
|
)} |
||||||
|
</ClipboardButton> |
||||||
|
</Modal.ButtonRow> |
||||||
|
</Modal> |
||||||
|
); |
||||||
|
} |
Loading…
Reference in new issue