mirror of
https://github.com/grafana/grafana.git
synced 2024-11-22 08:56:43 -06:00
Table: Add actions support (#94578)
This commit is contained in:
parent
4c15266a77
commit
0d70dbe730
@ -402,6 +402,7 @@ export const clearLinkButtonStyles = (theme: GrafanaTheme2) => {
|
||||
fontFamily: 'inherit',
|
||||
color: 'inherit',
|
||||
height: '100%',
|
||||
cursor: 'context-menu',
|
||||
'&:hover': {
|
||||
background: 'transparent',
|
||||
color: 'inherit',
|
||||
|
@ -6,7 +6,7 @@ import { DataLinksContextMenu } from './DataLinksContextMenu';
|
||||
|
||||
const fakeAriaLabel = 'fake aria label';
|
||||
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(
|
||||
<DataLinksContextMenu
|
||||
links={() => [
|
||||
@ -23,6 +23,7 @@ describe('DataLinksContextMenu', () => {
|
||||
origin: {},
|
||||
},
|
||||
]}
|
||||
actions={[{ title: 'Action1', onClick: () => {} }]}
|
||||
>
|
||||
{() => {
|
||||
return <div aria-label="fake aria label" />;
|
||||
@ -34,7 +35,43 @@ describe('DataLinksContextMenu', () => {
|
||||
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(
|
||||
<DataLinksContextMenu
|
||||
links={() => [
|
||||
|
@ -2,10 +2,11 @@ import { css } from '@emotion/css';
|
||||
import { CSSProperties } 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 { linkModelToContextMenuItems } from '../../utils/dataLinks';
|
||||
import { useStyles2 } from '../../themes';
|
||||
import { actionModelToContextMenuItems, linkModelToContextMenuItems } from '../../utils/dataLinks';
|
||||
import { WithContextMenu } from '../ContextMenu/WithContextMenu';
|
||||
import { MenuGroup, MenuItemsGroup } from '../Menu/MenuGroup';
|
||||
import { MenuItem } from '../Menu/MenuItem';
|
||||
@ -14,6 +15,7 @@ export interface DataLinksContextMenuProps {
|
||||
children: (props: DataLinksContextMenuApi) => JSX.Element;
|
||||
links: () => LinkModel[];
|
||||
style?: CSSProperties;
|
||||
actions?: ActionModel[];
|
||||
}
|
||||
|
||||
export interface DataLinksContextMenuApi {
|
||||
@ -21,8 +23,17 @@ export interface DataLinksContextMenuApi {
|
||||
targetClassName?: string;
|
||||
}
|
||||
|
||||
export const DataLinksContextMenu = ({ children, links, style }: DataLinksContextMenuProps) => {
|
||||
const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }];
|
||||
export const DataLinksContextMenu = ({ children, links, actions, style }: DataLinksContextMenuProps) => {
|
||||
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 renderMenuGroupItems = () => {
|
||||
return itemsGroup.map((group, groupIdx) => (
|
||||
@ -36,6 +47,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
|
||||
icon={item.icon}
|
||||
active={item.active}
|
||||
onClick={item.onClick}
|
||||
className={styles.itemWrapper}
|
||||
/>
|
||||
))}
|
||||
</MenuGroup>
|
||||
@ -47,7 +59,7 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
|
||||
cursor: 'context-menu',
|
||||
});
|
||||
|
||||
if (linksCounter > 1) {
|
||||
if (linksCounter > 1 || hasActions) {
|
||||
return (
|
||||
<WithContextMenu renderMenuItems={renderMenuGroupItems}>
|
||||
{({ openMenu }) => {
|
||||
@ -71,3 +83,9 @@ export const DataLinksContextMenu = ({ children, links, style }: DataLinksContex
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
itemWrapper: css({
|
||||
fontSize: 12,
|
||||
}),
|
||||
});
|
||||
|
@ -24,7 +24,7 @@ const defaultScale: ThresholdsConfig = {
|
||||
};
|
||||
|
||||
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 cellOptions = getCellOptions(field);
|
||||
|
||||
@ -56,6 +56,7 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
};
|
||||
|
||||
const hasLinks = Boolean(getLinks().length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const alignmentFactors = getAlignmentFactor(field, displayValue, cell.row.index);
|
||||
|
||||
const renderComponent = (menuProps: DataLinksContextMenuApi) => {
|
||||
@ -84,12 +85,13 @@ export const BarGaugeCell = (props: TableCellProps) => {
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{hasLinks && (
|
||||
<DataLinksContextMenu links={getLinks} style={{ display: 'flex', width: '100%' }}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={getLinks} actions={actions} style={{ display: 'flex', width: '100%' }}>
|
||||
{(api) => renderComponent(api)}
|
||||
</DataLinksContextMenu>
|
||||
) : (
|
||||
renderComponent({})
|
||||
)}
|
||||
{!hasLinks && renderComponent({})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -17,8 +17,8 @@ import { TableCellProps, CustomCellRendererProps, TableCellOptions } from './typ
|
||||
import { getCellColors, getCellOptions } from './utils';
|
||||
|
||||
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 displayValue = field.display!(cell.value);
|
||||
|
||||
@ -26,6 +26,7 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
|
||||
const cellOptions = getCellOptions(field);
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
const [hover, setHover] = useState(false);
|
||||
let value: string | ReactElement;
|
||||
@ -94,10 +95,8 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
onMouseLeave={showActions ? onMouseLeave : undefined}
|
||||
className={cellStyle}
|
||||
>
|
||||
{!hasLinks && (isStringValue ? `${value}` : <div className={tableStyles.cellText}>{value}</div>)}
|
||||
|
||||
{hasLinks && (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
@ -113,6 +112,10 @@ export const DefaultCell = (props: TableCellProps) => {
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
) : isStringValue ? (
|
||||
`${value}`
|
||||
) : (
|
||||
<div className={tableStyles.cellText}>{value}</div>
|
||||
)}
|
||||
|
||||
{hover && showActions && (
|
||||
|
@ -9,12 +9,13 @@ import { getCellOptions } from './utils';
|
||||
const DATALINKS_HEIGHT_OFFSET = 10;
|
||||
|
||||
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 { title, alt } =
|
||||
cellOptions.type === TableCellDisplayMode.Image ? cellOptions : { title: undefined, alt: undefined };
|
||||
const displayValue = field.display!(cell.value);
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
|
||||
// The image element
|
||||
const img = (
|
||||
@ -29,13 +30,13 @@ export const ImageCell = (props: TableCellProps) => {
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={tableStyles.cellContainer}>
|
||||
{/* If there are no links we simply render the image */}
|
||||
{!hasLinks && img}
|
||||
{/* Otherwise render data links with image */}
|
||||
{hasLinks && (
|
||||
{/* If there are data links/actions, we render them with image */}
|
||||
{/* Otherwise we simply render the image */}
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu
|
||||
style={{ height: tableStyles.cellHeight - DATALINKS_HEIGHT_OFFSET, width: 'auto' }}
|
||||
links={() => getCellLinks(field, row) || []}
|
||||
actions={actions}
|
||||
>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
@ -59,6 +60,8 @@ export const ImageCell = (props: TableCellProps) => {
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
) : (
|
||||
img
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -11,7 +11,7 @@ import { TableCellInspectorMode } from './TableCellInspector';
|
||||
import { TableCellProps } from './types';
|
||||
|
||||
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 txt = css({
|
||||
cursor: 'pointer',
|
||||
@ -30,14 +30,14 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
}
|
||||
|
||||
const hasLinks = Boolean(getCellLinks(field, row)?.length);
|
||||
const hasActions = Boolean(actions?.length);
|
||||
const clearButtonStyle = useStyles2(clearLinkButtonStyles);
|
||||
|
||||
return (
|
||||
<div {...cellProps} className={inspectEnabled ? tableStyles.cellContainerNoOverflow : tableStyles.cellContainer}>
|
||||
<div className={cx(tableStyles.cellText, txt)}>
|
||||
{!hasLinks && <div className={tableStyles.cellText}>{displayValue}</div>}
|
||||
{hasLinks && (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
|
||||
{hasLinks || hasActions ? (
|
||||
<DataLinksContextMenu links={() => getCellLinks(field, row) || []} actions={actions}>
|
||||
{(api) => {
|
||||
if (api.openMenu) {
|
||||
return (
|
||||
@ -50,6 +50,8 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
|
||||
}
|
||||
}}
|
||||
</DataLinksContextMenu>
|
||||
) : (
|
||||
<div className={tableStyles.cellText}>{displayValue}</div>
|
||||
)}
|
||||
</div>
|
||||
{inspectEnabled && <CellActions {...props} previewMode={TableCellInspectorMode.code} />}
|
||||
|
@ -23,7 +23,7 @@ import { usePanelContext } from '../PanelChrome';
|
||||
import { ExpandedRow, getExpandedRowHeight } from './ExpandedRow';
|
||||
import { TableCell } from './TableCell';
|
||||
import { TableStyles } from './styles';
|
||||
import { CellColors, TableFieldOptions, TableFilterActionCallback } from './types';
|
||||
import { CellColors, GetActionsFunction, TableFieldOptions, TableFilterActionCallback } from './types';
|
||||
import {
|
||||
calculateAroundPointThreshold,
|
||||
getCellColors,
|
||||
@ -54,6 +54,7 @@ interface RowsListProps {
|
||||
headerGroups: HeaderGroup[];
|
||||
longestField?: Field;
|
||||
textWrapField?: Field;
|
||||
getActions?: GetActionsFunction;
|
||||
}
|
||||
|
||||
export const RowsList = (props: RowsListProps) => {
|
||||
@ -80,6 +81,7 @@ export const RowsList = (props: RowsListProps) => {
|
||||
headerGroups,
|
||||
longestField,
|
||||
textWrapField,
|
||||
getActions,
|
||||
} = props;
|
||||
|
||||
const [rowHighlightIndex, setRowHighlightIndex] = useState<number | undefined>(initialRowIndex);
|
||||
@ -334,32 +336,34 @@ export const RowsList = (props: RowsListProps) => {
|
||||
rowExpanded={rowExpanded}
|
||||
textWrapped={textWrapFinal !== undefined}
|
||||
height={Number(style.height)}
|
||||
getActions={getActions}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
[
|
||||
cellHeight,
|
||||
data,
|
||||
nestedDataField,
|
||||
onCellFilterAdded,
|
||||
onRowHover,
|
||||
onRowLeave,
|
||||
prepareRow,
|
||||
rowIndexForPagination,
|
||||
rows,
|
||||
prepareRow,
|
||||
tableState.expanded,
|
||||
tableStyles,
|
||||
textWrapFinal,
|
||||
theme.components.table.rowSelected,
|
||||
theme.typography.fontSize,
|
||||
theme.typography.body.lineHeight,
|
||||
timeRange,
|
||||
width,
|
||||
nestedDataField,
|
||||
rowBg,
|
||||
textWrapFinal,
|
||||
tableStyles,
|
||||
onRowLeave,
|
||||
width,
|
||||
cellHeight,
|
||||
theme.components.table.rowSelected,
|
||||
theme.typography.body.lineHeight,
|
||||
theme.typography.fontSize,
|
||||
data,
|
||||
headerGroups,
|
||||
osContext,
|
||||
onRowHover,
|
||||
onCellFilterAdded,
|
||||
timeRange,
|
||||
getActions,
|
||||
]
|
||||
);
|
||||
|
||||
|
@ -59,6 +59,7 @@ export const Table = memo((props: Props) => {
|
||||
enableSharedCrosshair = false,
|
||||
initialRowIndex = undefined,
|
||||
fieldConfig,
|
||||
getActions,
|
||||
} = props;
|
||||
|
||||
const listRef = useRef<VariableSizeList>(null);
|
||||
@ -117,7 +118,7 @@ export const Table = memo((props: Props) => {
|
||||
// React-table column definitions
|
||||
const memoizedColumns = useMemo(
|
||||
() => 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`.
|
||||
@ -355,6 +356,7 @@ export const Table = memo((props: Props) => {
|
||||
initialRowIndex={initialRowIndex}
|
||||
longestField={longestField}
|
||||
textWrapField={textWrapField}
|
||||
getActions={getActions}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
@ -3,7 +3,7 @@ import { Cell } from 'react-table';
|
||||
import { TimeRange, DataFrame } from '@grafana/data';
|
||||
|
||||
import { TableStyles } from './styles';
|
||||
import { GrafanaTableColumn, TableFilterActionCallback } from './types';
|
||||
import { GetActionsFunction, GrafanaTableColumn, TableFilterActionCallback } from './types';
|
||||
|
||||
export interface Props {
|
||||
cell: Cell;
|
||||
@ -18,6 +18,7 @@ export interface Props {
|
||||
rowExpanded?: boolean;
|
||||
textWrapped?: boolean;
|
||||
height?: number;
|
||||
getActions?: GetActionsFunction;
|
||||
}
|
||||
|
||||
export const TableCell = ({
|
||||
@ -31,6 +32,7 @@ export const TableCell = ({
|
||||
rowExpanded,
|
||||
textWrapped,
|
||||
height,
|
||||
getActions,
|
||||
}: Props) => {
|
||||
const cellProps = cell.getCellProps();
|
||||
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;
|
||||
|
||||
const actions = getActions ? getActions(frame, field) : [];
|
||||
|
||||
return (
|
||||
<>
|
||||
{cell.render('Cell', {
|
||||
@ -71,6 +75,7 @@ export const TableCell = ({
|
||||
rowExpanded,
|
||||
textWrapped,
|
||||
height,
|
||||
actions,
|
||||
})}
|
||||
</>
|
||||
);
|
||||
|
@ -179,6 +179,7 @@ export function useTableStyles(theme: GrafanaTheme2, cellHeightOption: TableCell
|
||||
textOverflow: 'ellipsis',
|
||||
userSelect: 'text',
|
||||
whiteSpace: 'nowrap',
|
||||
cursor: 'text',
|
||||
}),
|
||||
sortIcon: css({
|
||||
marginLeft: theme.spacing(0.5),
|
||||
|
@ -2,7 +2,7 @@ import { Property } from 'csstype';
|
||||
import { FC } from 'react';
|
||||
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 { TableStyles } from './styles';
|
||||
@ -44,6 +44,7 @@ export interface TableCellProps extends CellProps<any> {
|
||||
onCellFilterAdded?: TableFilterActionCallback;
|
||||
innerWidth: number;
|
||||
frame: DataFrame;
|
||||
actions?: ActionModel[];
|
||||
}
|
||||
|
||||
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
|
||||
initialRowIndex?: number;
|
||||
fieldConfig?: FieldConfigSource;
|
||||
getActions?: GetActionsFunction;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,3 +156,6 @@ export interface CellColors {
|
||||
bgColor?: 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[];
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { LinkModel } from '@grafana/data';
|
||||
import { ActionModel, LinkModel } from '@grafana/data';
|
||||
|
||||
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) => {
|
||||
const compactExploreUrlRegex = /\/explore\?.*&(left|right)=\[(.*\,){2,}(.*){1}\]/;
|
||||
return compactExploreUrlRegex.test(url);
|
||||
|
@ -39,8 +39,8 @@ export const ActionListItem = ({ action, onEdit, onRemove, index, itemKey }: Act
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.icons}>
|
||||
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action title" />
|
||||
<IconButton name="times" onClick={onRemove} className={styles.icon} tooltip="Remove action title" />
|
||||
<IconButton name="pen" onClick={onEdit} className={styles.icon} tooltip="Edit action" />
|
||||
<IconButton name="trash-alt" onClick={onRemove} className={styles.icon} tooltip="Remove action" />
|
||||
<div className={styles.dragIcon} {...provided.dragHandleProps}>
|
||||
<Icon name="draggabledots" size="lg" />
|
||||
</div>
|
||||
|
@ -1,17 +1,22 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import {
|
||||
ActionModel,
|
||||
DashboardCursorSync,
|
||||
DataFrame,
|
||||
FieldMatcherID,
|
||||
getFrameDisplayName,
|
||||
InterpolateFunction,
|
||||
PanelProps,
|
||||
SelectableValue,
|
||||
Field,
|
||||
} 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';
|
||||
|
||||
import { getActions } from '../../../features/actions/utils';
|
||||
|
||||
import { hasDeprecatedParentRowIndex, migrateFromParentRowIndexToNestedFrames } from './migrations';
|
||||
import { Options } from './panelcfg.gen';
|
||||
|
||||
@ -63,6 +68,7 @@ export function TablePanel(props: Props) {
|
||||
timeRange={timeRange}
|
||||
enableSharedCrosshair={config.featureToggles.tableSharedCrosshair && enableSharedCrosshair}
|
||||
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 = {
|
||||
wrapper: css`
|
||||
display: flex;
|
||||
|
Loading…
Reference in New Issue
Block a user