Table: Add actions support (#94578)

This commit is contained in:
Adela Almasan 2024-10-17 16:47:39 -06:00 committed by GitHub
parent 4c15266a77
commit 0d70dbe730
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 178 additions and 47 deletions

View File

@ -402,6 +402,7 @@ export const clearLinkButtonStyles = (theme: GrafanaTheme2) => {
fontFamily: 'inherit', fontFamily: 'inherit',
color: 'inherit', color: 'inherit',
height: '100%', height: '100%',
cursor: 'context-menu',
'&:hover': { '&:hover': {
background: 'transparent', background: 'transparent',
color: 'inherit', color: 'inherit',

View File

@ -6,7 +6,7 @@ import { DataLinksContextMenu } from './DataLinksContextMenu';
const fakeAriaLabel = 'fake aria label'; const fakeAriaLabel = 'fake aria label';
describe('DataLinksContextMenu', () => { describe('DataLinksContextMenu', () => {
it('renders context menu when there are more than one data links', () => { it('renders context menu when there are more than one data links or actions', () => {
render( render(
<DataLinksContextMenu <DataLinksContextMenu
links={() => [ links={() => [
@ -23,6 +23,7 @@ describe('DataLinksContextMenu', () => {
origin: {}, origin: {},
}, },
]} ]}
actions={[{ title: 'Action1', onClick: () => {} }]}
> >
{() => { {() => {
return <div aria-label="fake aria label" />; return <div aria-label="fake aria label" />;
@ -34,7 +35,43 @@ describe('DataLinksContextMenu', () => {
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0); expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
}); });
it('renders link when there is a single data link', () => { it('renders context menu when there are actions and one data link', () => {
render(
<DataLinksContextMenu
links={() => [
{
href: '/link1',
title: 'Link1',
target: '_blank',
origin: {},
},
]}
actions={[{ title: 'Action1', onClick: () => {} }]}
>
{() => {
return <div aria-label="fake aria label" />;
}}
</DataLinksContextMenu>
);
expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument();
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
});
it('renders context menu when there are only actions', () => {
render(
<DataLinksContextMenu links={() => []} actions={[{ title: 'Action1', onClick: () => {} }]}>
{() => {
return <div aria-label="fake aria label" />;
}}
</DataLinksContextMenu>
);
expect(screen.getByLabelText(fakeAriaLabel)).toBeInTheDocument();
expect(screen.queryAllByLabelText(selectors.components.DataLinksContextMenu.singleLink)).toHaveLength(0);
});
it('renders link when there is a single data link and no actions', () => {
render( render(
<DataLinksContextMenu <DataLinksContextMenu
links={() => [ links={() => [

View File

@ -2,10 +2,11 @@ import { css } from '@emotion/css';
import { CSSProperties } from 'react'; import { CSSProperties } from 'react';
import * as React from 'react'; import * as React from 'react';
import { LinkModel } from '@grafana/data'; import { ActionModel, GrafanaTheme2, LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { linkModelToContextMenuItems } from '../../utils/dataLinks'; import { useStyles2 } from '../../themes';
import { actionModelToContextMenuItems, linkModelToContextMenuItems } from '../../utils/dataLinks';
import { WithContextMenu } from '../ContextMenu/WithContextMenu'; import { WithContextMenu } from '../ContextMenu/WithContextMenu';
import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup'; import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup';
import { MenuItem } from '../Menu/MenuItem'; import { MenuItem } from '../Menu/MenuItem';
@ -14,6 +15,7 @@ export interface DataLinksContextMenuProps {
children: (props: DataLinksContextMenuApi) => JSX.Element; children: (props: DataLinksContextMenuApi) => JSX.Element;
links: () => LinkModel[]; links: () => LinkModel[];
style?: CSSProperties; style?: CSSProperties;
actions?: ActionModel[];
} }
export interface DataLinksContextMenuApi { export interface DataLinksContextMenuApi {
@ -21,8 +23,17 @@ export interface DataLinksContextMenuApi {
targetClassName?: string; targetClassName?: string;
} }
export const DataLinksContextMenu = ({ children, links, style }: DataLinksContextMenuProps) => { export const DataLinksContextMenu = ({ children, links, actions, style }: DataLinksContextMenuProps) => {
const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }]; const styles = useStyles2(getStyles);
const itemsGroup: MenuItemsGroup[] = [
{ items: linkModelToContextMenuItems(links), label: Boolean(links().length) ? 'Data links' : '' },
];
const hasActions = Boolean(actions?.length);
if (hasActions) {
itemsGroup.push({ items: actionModelToContextMenuItems(actions!), label: 'Actions' });
}
const linksCounter = itemsGroup[0].items.length; const linksCounter = itemsGroup[0].items.length;
const renderMenuGroupItems = () => { const renderMenuGroupItems = () => {
return itemsGroup.map((group, groupIdx) => ( return itemsGroup.map((group, groupIdx) => (
@ -36,6 +47,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
icon={item.icon} icon={item.icon}
active={item.active} active={item.active}
onClick={item.onClick} onClick={item.onClick}
className={styles.itemWrapper}
/> />
))} ))}
</MenuGroup> </MenuGroup>
@ -47,7 +59,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
cursor: 'context-menu', cursor: 'context-menu',
}); });
if (linksCounter > 1) { if (linksCounter > 1 || hasActions) {
return ( return (
<WithContextMenu renderMenuItems={renderMenuGroupItems}> <WithContextMenu renderMenuItems={renderMenuGroupItems}>
{({ openMenu }) => { {({ openMenu }) => {
@ -71,3 +83,9 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
); );
} }
}; };
const getStyles = (theme: GrafanaTheme2) => ({
itemWrapper: css({
fontSize: 12,
}),
});

View File

@ -24,7 +24,7 @@ const defaultScale: ThresholdsConfig = {
}; };
export const BarGaugeCell = (props: TableCellProps) => { export const BarGaugeCell = (props: TableCellProps) => {
const { field, innerWidth, tableStyles, cell, cellProps, row } = props; const { field, innerWidth, tableStyles, cell, cellProps, row, actions } = props;
const displayValue = field.display!(cell.value); const displayValue = field.display!(cell.value);
const cellOptions = getCellOptions(field); const cellOptions = getCellOptions(field);
@ -56,6 +56,7 @@ export const BarGaugeCell = (props: TableCellProps) => {
}; };
const hasLinks = Boolean(getLinks().length); const hasLinks = Boolean(getLinks().length);
const hasActions = Boolean(actions?.length);
const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index); const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index);
const renderComponent = (menuProps: DataLinksContextMenuApi) => { const renderComponent = (menuProps: DataLinksContextMenuApi) => {
@ -84,12 +85,13 @@ export const BarGaugeCell = (props: TableCellProps) => {
return ( return (
<div {...cellProps} className={tableStyles.cellContainer}> <div {...cellProps} className={tableStyles.cellContainer}>
{hasLinks && ( {hasLinks || hasActions ? (
<DataLinksContextMenu links={getLinks} style={{ display: 'flex', width: '100%' }}> <DataLinksContextMenu links={getLinks} actions={actions} style={{ display: 'flex', width: '100%' }}>
{(api) => renderComponent(api)} {(api) => renderComponent(api)}
</DataLinksContextMenu> </DataLinksContextMenu>
) : (
renderComponent({})
)} )}
{!hasLinks && renderComponent({})}
</div> </div>
); );
}; };

View File

@ -17,8 +17,8 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ
import { getCellColors, getCellOptions } from './utils'; import { getCellColors, getCellOptions } from './utils';
export const DefaultCell = (props: TableCellProps) => { export const DefaultCell = (props: TableCellProps) => {
const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height } = props; const { field, cell, tableStyles, row, cellProps, frame, rowStyled, rowExpanded, textWrapped, height, actions } =
props;
const inspectEnabled = Boolean(field.config.custom?.inspect); const inspectEnabled = Boolean(field.config.custom?.inspect);
const displayValue = field.display!(cell.value); const displayValue = field.display!(cell.value);
@ -26,6 +26,7 @@ export const DefaultCell = (props: TableCellProps) => {
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
const cellOptions = getCellOptions(field); const cellOptions = getCellOptions(field);
const hasLinks = Boolean(getCellLinks(field, row)?.length); const hasLinks = Boolean(getCellLinks(field, row)?.length);
const hasActions = Boolean(actions?.length);
const clearButtonStyle = useStyles2(clearLinkButtonStyles); const clearButtonStyle = useStyles2(clearLinkButtonStyles);
const [hover, setHover] = useState(false); const [hover, setHover] = useState(false);
let value: string | ReactElement; let value: string | ReactElement;
@ -94,10 +95,8 @@ export const DefaultCell = (props: TableCellProps) => {
onMouseLeave={showActions ? onMouseLeave : undefined} onMouseLeave={showActions ? onMouseLeave : undefined}
className={cellStyle} className={cellStyle}
> >
{!hasLinks && (isStringValue ? `${value}` : <div className={tableStyles.cellText}>{value}</div>)} {hasLinks || hasActions ? (
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
{hasLinks && (
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
{(api) => { {(api) => {
if (api.openMenu) { if (api.openMenu) {
return ( return (
@ -113,6 +112,10 @@ export const DefaultCell = (props: TableCellProps) => {
} }
}} }}
</DataLinksContextMenu> </DataLinksContextMenu>
) : isStringValue ? (
`${value}`
) : (
<div className={tableStyles.cellText}>{value}</div>
)} )}
{hover && showActions && ( {hover && showActions && (

View File

@ -9,12 +9,13 @@ import { getCellOptions } from './utils';
const DATALINKS_HEIGHT_OFFSET = 10; const DATALINKS_HEIGHT_OFFSET = 10;
export const ImageCell = (props: TableCellProps) => { export const ImageCell = (props: TableCellProps) => {
const { field, cell, tableStyles, row, cellProps } = props; const { field, cell, tableStyles, row, cellProps, actions } = props;
const cellOptions = getCellOptions(field); const cellOptions = getCellOptions(field);
const { title, alt } = const { title, alt } =
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined }; cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined };
const displayValue = field.display!(cell.value); const displayValue = field.display!(cell.value);
const hasLinks = Boolean(getCellLinks(field, row)?.length); const hasLinks = Boolean(getCellLinks(field, row)?.length);
const hasActions = Boolean(actions?.length);
// The image element // The image element
const img = ( const img = (
@ -29,13 +30,13 @@ export const ImageCell = (props: TableCellProps) => {
return ( return (
<div {...cellProps} className={tableStyles.cellContainer}> <div {...cellProps} className={tableStyles.cellContainer}>
{/* If there are no links we simply render the image */} {/* If there are data links/actions, we render them with image */}
{!hasLinks && img} {/* Otherwise we simply render the image */}
{/* Otherwise render data links with image */} {hasLinks || hasActions ? (
{hasLinks && (
<DataLinksContextMenu <DataLinksContextMenu
style={{ height: tableStyles.cellHeight - DATALINKS_HEIGHT_OFFSET, width: 'auto' }} style={{ height: tableStyles.cellHeight - DATALINKS_HEIGHT_OFFSET, width: 'auto' }}
links={() => getCellLinks(field, row) || []} links={() => getCellLinks(field, row) || []}
actions={actions}
> >
{(api) => { {(api) => {
if (api.openMenu) { if (api.openMenu) {
@ -59,6 +60,8 @@ export const ImageCell = (props: TableCellProps) => {
} }
}} }}
</DataLinksContextMenu> </DataLinksContextMenu>
) : (
img
)} )}
</div> </div>
); );

View File

@ -11,7 +11,7 @@ import { TableCellInspectorMode } from './TableCellInspector';
import { TableCellProps } from './types'; import { TableCellProps } from './types';
export function JSONViewCell(props: TableCellProps): JSX.Element { export function JSONViewCell(props: TableCellProps): JSX.Element {
const { cell, tableStyles, cellProps, field, row } = props; const { cell, tableStyles, cellProps, field, row, actions } = props;
const inspectEnabled = Boolean(field.config.custom?.inspect); const inspectEnabled = Boolean(field.config.custom?.inspect);
const txt = css({ const txt = css({
cursor: 'pointer', cursor: 'pointer',
@ -30,14 +30,14 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
} }
const hasLinks = Boolean(getCellLinks(field, row)?.length); const hasLinks = Boolean(getCellLinks(field, row)?.length);
const hasActions = Boolean(actions?.length);
const clearButtonStyle = useStyles2(clearLinkButtonStyles); const clearButtonStyle = useStyles2(clearLinkButtonStyles);
return ( return (
<div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}> <div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}>
<div className={cx(tableStyles.cellText, txt)}> <div className={cx(tableStyles.cellText, txt)}>
{!hasLinks && <div className={tableStyles.cellText}>{displayValue}</div>} {hasLinks || hasActions ? (
{hasLinks && ( <DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
{(api) => { {(api) => {
if (api.openMenu) { if (api.openMenu) {
return ( return (
@ -50,6 +50,8 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
} }
}} }}
</DataLinksContextMenu> </DataLinksContextMenu>
) : (
<div className={tableStyles.cellText}>{displayValue}</div>
)} )}
</div> </div>
{inspectEnabled && <CellActions {...props} previewMode={TableCellInspectorMode.code} />} {inspectEnabled && <CellActions {...props} previewMode={TableCellInspectorMode.code} />}

View File

@ -23,7 +23,7 @@ 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, TableFieldOptions, TableFilterActionCallback } from './types'; import { CellColors, GetActionsFunction, TableFieldOptions, TableFilterActionCallback } from './types';
import { import {
calculateAroundPointThreshold, calculateAroundPointThreshold,
getCellColors, getCellColors,
@ -54,6 +54,7 @@ interface RowsListProps {
headerGroups: HeaderGroup[]; headerGroups: HeaderGroup[];
longestField?: Field; longestField?: Field;
textWrapField?: Field; textWrapField?: Field;
getActions?: GetActionsFunction;
} }
export const RowsList = (props: RowsListProps) => { export const RowsList = (props: RowsListProps) => {
@ -80,6 +81,7 @@ export const RowsList = (props: RowsListProps) => {
headerGroups, headerGroups,
longestField, longestField,
textWrapField, textWrapField,
getActions,
} = props; } = props;
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex); const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
@ -334,32 +336,34 @@ export const RowsList = (props: RowsListProps) => {
rowExpanded={rowExpanded} rowExpanded={rowExpanded}
textWrapped={textWrapFinal !== undefined} textWrapped={textWrapFinal !== undefined}
height={Number(style.height)} height={Number(style.height)}
getActions={getActions}
/> />
))} ))}
</div> </div>
); );
}, },
[ [
cellHeight,
data,
nestedDataField,
onCellFilterAdded,
onRowHover,
onRowLeave,
prepareRow,
rowIndexForPagination, rowIndexForPagination,
rows, rows,
prepareRow,
tableState.expanded, tableState.expanded,
tableStyles, nestedDataField,
textWrapFinal,
theme.components.table.rowSelected,
theme.typography.fontSize,
theme.typography.body.lineHeight,
timeRange,
width,
rowBg, rowBg,
textWrapFinal,
tableStyles,
onRowLeave,
width,
cellHeight,
theme.components.table.rowSelected,
theme.typography.body.lineHeight,
theme.typography.fontSize,
data,
headerGroups, headerGroups,
osContext, osContext,
onRowHover,
onCellFilterAdded,
timeRange,
getActions,
] ]
); );

View File

@ -59,6 +59,7 @@ export const Table = memo((props: Props) => {
enableSharedCrosshair = false, enableSharedCrosshair = false,
initialRowIndex = undefined, initialRowIndex = undefined,
fieldConfig, fieldConfig,
getActions,
} = props; } = props;
const listRef = useRef<VariableSizeList>(null); const listRef = useRef<VariableSizeList>(null);
@ -117,7 +118,7 @@ export const Table = memo((props: Props) => {
// React-table column definitions // React-table column definitions
const memoizedColumns = useMemo( const memoizedColumns = useMemo(
() => getColumns(data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet), () => getColumns(data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet),
[data, width, columnMinWidth, footerItems, hasNestedData, isCountRowsSet] [data, width, columnMinWidth, hasNestedData, footerItems, isCountRowsSet]
); );
// we need a ref to later store the `toggleAllRowsExpanded` function, returned by `useTable`. // we need a ref to later store the `toggleAllRowsExpanded` function, returned by `useTable`.
@ -355,6 +356,7 @@ export const Table = memo((props: Props) => {
initialRowIndex={initialRowIndex} initialRowIndex={initialRowIndex}
longestField={longestField} longestField={longestField}
textWrapField={textWrapField} textWrapField={textWrapField}
getActions={getActions}
/> />
</div> </div>
) : ( ) : (

View File

@ -3,7 +3,7 @@ import { Cell } from 'react-table';
import { TimeRange, DataFrame } from '@grafana/data'; import { TimeRange, DataFrame } from '@grafana/data';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
import { GrafanaTableColumn, TableFilterActionCallback } from './types'; import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback } from './types';
export interface Props { export interface Props {
cell: Cell; cell: Cell;
@ -18,6 +18,7 @@ export interface Props {
rowExpanded?: boolean; rowExpanded?: boolean;
textWrapped?: boolean; textWrapped?: boolean;
height?: number; height?: number;
getActions?: GetActionsFunction;
} }
export const TableCell = ({ export const TableCell = ({
@ -31,6 +32,7 @@ export const TableCell = ({
rowExpanded, rowExpanded,
textWrapped, textWrapped,
height, height,
getActions,
}: 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;
@ -56,6 +58,8 @@ export const TableCell = ({
let innerWidth = (typeof cell.column.width === 'number' ? cell.column.width : 24) - tableStyles.cellPadding * 2; let innerWidth = (typeof cell.column.width === 'number' ? cell.column.width : 24) - tableStyles.cellPadding * 2;
const actions = getActions ? getActions(frame, field) : [];
return ( return (
<> <>
{cell.render('Cell', { {cell.render('Cell', {
@ -71,6 +75,7 @@ export const TableCell = ({
rowExpanded, rowExpanded,
textWrapped, textWrapped,
height, height,
actions,
})} })}
</> </>
); );

View File

@ -179,6 +179,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
textOverflow: 'ellipsis', textOverflow: 'ellipsis',
userSelect: 'text', userSelect: 'text',
whiteSpace: 'nowrap', whiteSpace: 'nowrap',
cursor: 'text',
}), }),
sortIcon: css({ sortIcon: css({
marginLeft: theme.spacing(0.5), marginLeft: theme.spacing(0.5),

View File

@ -2,7 +2,7 @@ import { Property } from 'csstype';
import { FC } from 'react'; import { FC } from 'react';
import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table'; import { CellProps, Column, Row, TableState, UseExpandedRowProps } from 'react-table';
import { DataFrame, Field, KeyValue, SelectableValue, TimeRange, FieldConfigSource } from '@grafana/data'; import { DataFrame, Field, KeyValue, SelectableValue, TimeRange, FieldConfigSource, ActionModel } from '@grafana/data';
import * as schema from '@grafana/schema'; import * as schema from '@grafana/schema';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
@ -44,6 +44,7 @@ export interface TableCellProps extends CellProps<any> {
onCellFilterAdded?: TableFilterActionCallback; onCellFilterAdded?: TableFilterActionCallback;
innerWidth: number; innerWidth: number;
frame: DataFrame; frame: DataFrame;
actions?: ActionModel[];
} }
export type CellComponent = FC<TableCellProps>; export type CellComponent = FC<TableCellProps>;
@ -106,6 +107,7 @@ export interface Props {
// The index of the field value that the table will initialize scrolled to // The index of the field value that the table will initialize scrolled to
initialRowIndex?: number; initialRowIndex?: number;
fieldConfig?: FieldConfigSource; fieldConfig?: FieldConfigSource;
getActions?: GetActionsFunction;
} }
/** /**
@ -154,3 +156,6 @@ export interface CellColors {
bgColor?: string; bgColor?: string;
bgHoverColor?: string; bgHoverColor?: string;
} }
// export type GetActionsFunction = (frame: DataFrame, field: Field, fieldScopedVars: any, replaceVariables: any, actions: Action[], config: any) => ActionModel[];
export type GetActionsFunction = (frame: DataFrame, field: Field) => ActionModel[];

View File

@ -1,4 +1,4 @@
import { LinkModel } from '@grafana/data'; import { ActionModel, LinkModel } from '@grafana/data';
import { MenuItemProps } from '../components/Menu/MenuItem'; import { MenuItemProps } from '../components/Menu/MenuItem';
@ -19,6 +19,17 @@ export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem
}); });
}; };
export const actionModelToContextMenuItems: (actions: ActionModel[]) => MenuItemProps[] = (actions) => {
return actions.map((action) => {
return {
label: action.title,
ariaLabel: action.title,
icon: 'record-audio',
onClick: action.onClick,
};
});
};
export const isCompactUrl = (url: string) => { export const isCompactUrl = (url: string) => {
const compactExploreUrlRegex = /\/explore\?.*&(left|right)=\[(.*\,){2,}(.*){1}\]/; const compactExploreUrlRegex = /\/explore\?.*&(left|right)=\[(.*\,){2,}(.*){1}\]/;
return compactExploreUrlRegex.test(url); return compactExploreUrlRegex.test(url);

View File

@ -39,8 +39,8 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act
</div> </div>
</div> </div>
<div className={styles.icons}> <div className={styles.icons}>
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action title" /> <IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action" />
<IconButton name="times" onClick={onRemove} className={styles.icon} tooltip="Remove action title" /> <IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove action" />
<div className={styles.dragIcon} {...provided.dragHandleProps}> <div className={styles.dragIcon} {...provided.dragHandleProps}>
<Icon name="draggabledots" size="lg" /> <Icon name="draggabledots" size="lg" />
</div> </div>

View File

@ -1,17 +1,22 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import { import {
ActionModel,
DashboardCursorSync, DashboardCursorSync,
DataFrame, DataFrame,
FieldMatcherID, FieldMatcherID,
getFrameDisplayName, getFrameDisplayName,
InterpolateFunction,
PanelProps, PanelProps,
SelectableValue, SelectableValue,
Field,
} from '@grafana/data'; } from '@grafana/data';
import { config, PanelDataErrorView } from '@grafana/runtime'; import { config, PanelDataErrorView } from '@grafana/runtime';
import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui'; import { Select, Table, usePanelContext, useTheme2 } from '@grafana/ui';
import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types'; import { TableSortByFieldState } from '@grafana/ui/src/components/Table/types';
import { getActions } from '../../../features/actions/utils';
import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames } from './migrations'; import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames } from './migrations';
import { Options } from './panelcfg.gen'; import { Options } from './panelcfg.gen';
@ -63,6 +68,7 @@ export function TablePanel(props: Props) {
timeRange={timeRange} timeRange={timeRange}
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair} enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
fieldConfig={fieldConfig} fieldConfig={fieldConfig}
getActions={getCellActions}
/> />
); );
@ -136,6 +142,37 @@ function onChangeTableSelection(val: SelectableValue<number>, props: Props) {
}); });
} }
// placeholder function; assuming the values are already interpolated
const replaceVars: InterpolateFunction = (value: string) => value;
const getCellActions = (dataFrame: DataFrame, field: Field) => {
if (!config.featureToggles?.vizActions) {
return [];
}
const actions: Array<ActionModel<Field>> = [];
const actionLookup = new Set<string>();
const actionsModel = getActions(
dataFrame,
field,
field.state!.scopedVars!,
replaceVars,
field.config.actions ?? [],
{}
);
actionsModel.forEach((action) => {
const key = `${action.title}`;
if (!actionLookup.has(key)) {
actions.push(action);
actionLookup.add(key);
}
});
return actions;
};
const tableStyles = { const tableStyles = {
wrapper: css` wrapper: css`
display: flex; display: flex;