mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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',
|
fontFamily: 'inherit',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
|
cursor: 'context-menu',
|
||||||
'&:hover': {
|
'&:hover': {
|
||||||
background: 'transparent',
|
background: 'transparent',
|
||||||
color: 'inherit',
|
color: 'inherit',
|
||||||
|
@ -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={() => [
|
||||||
|
@ -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,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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 && (
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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} />}
|
||||||
|
@ -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,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
@ -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,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
@ -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),
|
||||||
|
@ -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[];
|
||||||
|
@ -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);
|
||||||
|
@ -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>
|
||||||
|
@ -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;
|
||||||
|
Loading…
Reference in New Issue
Block a user