Table panel: Add multiple data links support to Default, Image and JSONView cells (#51162)

* Table panel: Support multiple data links in default cell

* Table panel: Show data links for Image and JSONView cells

* Simplify DataLinksContextMenu api

* Betterer
This commit is contained in:
Dominik Prokop 2022-06-27 14:23:29 +02:00 committed by GitHub
parent 4a397c9c24
commit b2b0be7b93
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 87 additions and 102 deletions

View File

@ -2092,7 +2092,7 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/components/Switch/Switch.story.tsx:3337756944": [ "packages/grafana-ui/src/components/Switch/Switch.story.tsx:3337756944": [
[11, 15, 258, "Do not use any type assertions.", "1871090638"] [11, 15, 258, "Do not use any type assertions.", "1871090638"]
], ],
"packages/grafana-ui/src/components/Table/BarGaugeCell.tsx:2469721145": [ "packages/grafana-ui/src/components/Table/BarGaugeCell.tsx:1481110243": [
[46, 13, 17, "Do not use any type assertions.", "3640560184"] [46, 13, 17, "Do not use any type assertions.", "3640560184"]
], ],
"packages/grafana-ui/src/components/Table/CellActions.tsx:3266589396": [ "packages/grafana-ui/src/components/Table/CellActions.tsx:3266589396": [
@ -2100,8 +2100,8 @@ exports[`better eslint`] = {
[22, 10, 16, "Do not use any type assertions.", "737436615"], [22, 10, 16, "Do not use any type assertions.", "737436615"],
[23, 22, 25, "Do not use any type assertions.", "1478996352"] [23, 22, 25, "Do not use any type assertions.", "1478996352"]
], ],
"packages/grafana-ui/src/components/Table/DefaultCell.tsx:1844923424": [ "packages/grafana-ui/src/components/Table/DefaultCell.tsx:3618905143": [
[14, 34, 40, "Do not use any type assertions.", "91092480"] [16, 34, 40, "Do not use any type assertions.", "91092480"]
], ],
"packages/grafana-ui/src/components/Table/Filter.tsx:1102571026": [ "packages/grafana-ui/src/components/Table/Filter.tsx:1102571026": [
[13, 10, 3, "Unexpected any. Specify a different type.", "193409811"] [13, 10, 3, "Unexpected any. Specify a different type.", "193409811"]
@ -2121,8 +2121,8 @@ exports[`better eslint`] = {
[53, 38, 13, "Do not use any type assertions.", "947160887"], [53, 38, 13, "Do not use any type assertions.", "947160887"],
[53, 48, 3, "Unexpected any. Specify a different type.", "193409811"] [53, 48, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"packages/grafana-ui/src/components/Table/JSONViewCell.tsx:4022130242": [ "packages/grafana-ui/src/components/Table/JSONViewCell.tsx:2181157412": [
[11, 34, 40, "Do not use any type assertions.", "91092480"] [12, 34, 40, "Do not use any type assertions.", "91092480"]
], ],
"packages/grafana-ui/src/components/Table/Table.story.tsx:4272567078": [ "packages/grafana-ui/src/components/Table/Table.story.tsx:4272567078": [
[23, 15, 364, "Do not use any type assertions.", "4121186137"] [23, 15, 364, "Do not use any type assertions.", "4121186137"]
@ -2486,9 +2486,6 @@ exports[`better eslint`] = {
"packages/grafana-ui/src/utils/colors.ts:349987544": [ "packages/grafana-ui/src/utils/colors.ts:349987544": [
[110, 25, 3, "Unexpected any. Specify a different type.", "193409811"] [110, 25, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"packages/grafana-ui/src/utils/dataLinks.ts:2916992126": [
[16, 12, 71, "Do not use any type assertions.", "1504235577"]
],
"packages/grafana-ui/src/utils/debug.ts:2900904491": [ "packages/grafana-ui/src/utils/debug.ts:2900904491": [
[6, 56, 3, "Unexpected any. Specify a different type.", "193409811"] [6, 56, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
@ -2516,9 +2513,9 @@ exports[`better eslint`] = {
[38, 72, 3, "Unexpected any. Specify a different type.", "193409811"], [38, 72, 3, "Unexpected any. Specify a different type.", "193409811"],
[38, 85, 3, "Unexpected any. Specify a different type.", "193409811"] [38, 85, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"packages/grafana-ui/src/utils/table.ts:3564715667": [ "packages/grafana-ui/src/utils/table.ts:2798596296": [
[8, 52, 3, "Unexpected any. Specify a different type.", "193409811"], [7, 52, 3, "Unexpected any. Specify a different type.", "193409811"],
[9, 22, 3, "Unexpected any. Specify a different type.", "193409811"] [8, 29, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"packages/grafana-ui/src/utils/useAsyncDependency.ts:4089662838": [ "packages/grafana-ui/src/utils/useAsyncDependency.ts:4089662838": [
[3, 60, 3, "Unexpected any. Specify a different type.", "193409811"] [3, 60, 3, "Unexpected any. Specify a different type.", "193409811"]
@ -10930,8 +10927,8 @@ exports[`better eslint`] = {
[102, 16, 9, "Do not use any type assertions.", "3692209159"], [102, 16, 9, "Do not use any type assertions.", "3692209159"],
[102, 22, 3, "Unexpected any. Specify a different type.", "193409811"] [102, 22, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/plugins/panel/bargauge/BarGaugePanel.tsx:3184205061": [ "public/app/plugins/panel/bargauge/BarGaugePanel.tsx:2584207123": [
[114, 75, 3, "Unexpected any. Specify a different type.", "193409811"] [112, 75, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/plugins/panel/candlestick/CandlestickPanel.tsx:3928129934": [ "public/app/plugins/panel/candlestick/CandlestickPanel.tsx:3928129934": [
[116, 26, 33, "Do not use any type assertions.", "1940217301"], [116, 26, 33, "Do not use any type assertions.", "1940217301"],
@ -11877,7 +11874,7 @@ exports[`better eslint`] = {
[194, 16, 3, "Unexpected any. Specify a different type.", "193409811"], [194, 16, 3, "Unexpected any. Specify a different type.", "193409811"],
[256, 16, 3, "Unexpected any. Specify a different type.", "193409811"] [256, 16, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],
"public/app/plugins/panel/piechart/PieChart.tsx:1361550264": [ "public/app/plugins/panel/piechart/PieChart.tsx:287896203": [
[201, 12, 3, "Unexpected any. Specify a different type.", "193409811"], [201, 12, 3, "Unexpected any. Specify a different type.", "193409811"],
[217, 12, 3, "Unexpected any. Specify a different type.", "193409811"] [217, 12, 3, "Unexpected any. Specify a different type.", "193409811"]
], ],

View File

@ -24,18 +24,6 @@ describe('DataLinksContextMenu', () => {
origin: {}, origin: {},
}, },
]} ]}
config={{
links: [
{
title: 'Link1',
url: '/link1',
},
{
title: 'Link2',
url: '/link2',
},
],
}}
> >
{() => { {() => {
return <div aria-label="fake aria label" />; return <div aria-label="fake aria label" />;
@ -58,14 +46,6 @@ describe('DataLinksContextMenu', () => {
origin: {}, origin: {},
}, },
]} ]}
config={{
links: [
{
title: 'Link1',
url: '/link1',
},
],
}}
> >
{() => { {() => {
return <div aria-label="fake aria label" />; return <div aria-label="fake aria label" />;

View File

@ -1,7 +1,7 @@
import { css } from '@emotion/css'; import { css } from '@emotion/css';
import React from 'react'; import React from 'react';
import { FieldConfig, LinkModel } from '@grafana/data'; import { LinkModel } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { linkModelToContextMenuItems } from '../../utils/dataLinks'; import { linkModelToContextMenuItems } from '../../utils/dataLinks';
@ -12,7 +12,6 @@ import { MenuItem } from '../Menu/MenuItem';
interface DataLinksContextMenuProps { interface DataLinksContextMenuProps {
children: (props: DataLinksContextMenuApi) => JSX.Element; children: (props: DataLinksContextMenuApi) => JSX.Element;
links: () => LinkModel[]; links: () => LinkModel[];
config: FieldConfig;
} }
export interface DataLinksContextMenuApi { export interface DataLinksContextMenuApi {
@ -20,9 +19,9 @@ export interface DataLinksContextMenuApi {
targetClassName?: string; targetClassName?: string;
} }
export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ children, links, config }) => { export const DataLinksContextMenu: React.FC<DataLinksContextMenuProps> = ({ children, links }) => {
const linksCounter = config.links!.length;
const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }]; const itemsGroup: MenuItemsGroup[] = [{ items: linkModelToContextMenuItems(links), label: 'Data links' }];
const linksCounter = itemsGroup[0].items.length;
const renderMenuGroupItems = () => { const renderMenuGroupItems = () => {
return itemsGroup.map((group, index) => ( return itemsGroup.map((group, index) => (
<MenuGroup key={`${group.label}${index}`} label={group.label}> <MenuGroup key={`${group.label}${index}`} label={group.label}>

View File

@ -76,11 +76,7 @@ export const BarGaugeCell: FC<TableCellProps> = (props) => {
return ( return (
<div {...cellProps} className={tableStyles.cellContainer}> <div {...cellProps} className={tableStyles.cellContainer}>
{hasLinks && ( {hasLinks && <DataLinksContextMenu links={getLinks}>{(api) => renderComponent(api)}</DataLinksContextMenu>}
<DataLinksContextMenu links={getLinks} config={config}>
{(api) => renderComponent(api)}
</DataLinksContextMenu>
)}
{!hasLinks && ( {!hasLinks && (
<BarGauge <BarGauge
width={innerWidth} width={innerWidth}

View File

@ -1,9 +1,11 @@
import { cx } from '@emotion/css';
import React, { FC, ReactElement } from 'react'; import React, { FC, ReactElement } from 'react';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import { DisplayValue, Field, formattedValueToString } from '@grafana/data'; import { DisplayValue, Field, formattedValueToString } from '@grafana/data';
import { getTextColorForBackground, getCellLinks } from '../../utils'; import { getTextColorForBackground, getCellLinks } from '../../utils';
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
import { CellActions } from './CellActions'; import { CellActions } from './CellActions';
import { TableStyles } from './styles'; import { TableStyles } from './styles';
@ -26,16 +28,24 @@ export const DefaultCell: FC<TableCellProps> = (props) => {
const showActions = (showFilters && cell.value !== undefined) || inspectEnabled; const showActions = (showFilters && cell.value !== undefined) || inspectEnabled;
const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled); const cellStyle = getCellStyle(tableStyles, field, displayValue, inspectEnabled);
const { link, onClick } = getCellLinks(field, row); const hasLinks = Boolean(getCellLinks(field, row)?.length);
return ( return (
<div {...cellProps} className={cellStyle}> <div {...cellProps} className={cellStyle}>
{!link && <div className={tableStyles.cellText}>{value}</div>} {!hasLinks && <div className={tableStyles.cellText}>{value}</div>}
{link && (
<a href={link.href} onClick={onClick} target={link.target} title={link.title} className={tableStyles.cellLink}> {hasLinks && (
{value} <DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
</a> {(api) => {
return (
<div onClick={api.openMenu} className={cx(tableStyles.cellLink, api.targetClassName)}>
{value}
</div>
);
}}
</DataLinksContextMenu>
)} )}
{showActions && <CellActions {...props} previewMode="text" />} {showActions && <CellActions {...props} previewMode="text" />}
</div> </div>
); );

View File

@ -1,6 +1,8 @@
import { cx } from '@emotion/css';
import React, { FC } from 'react'; import React, { FC } from 'react';
import { getCellLinks } from '../../utils'; import { getCellLinks } from '../../utils';
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
import { TableCellProps } from './types'; import { TableCellProps } from './types';
@ -9,21 +11,21 @@ export const ImageCell: FC<TableCellProps> = (props) => {
const displayValue = field.display!(cell.value); const displayValue = field.display!(cell.value);
const { link, onClick } = getCellLinks(field, row); const hasLinks = getCellLinks(field, row)?.length;
return ( return (
<div {...cellProps} className={tableStyles.cellContainer}> <div {...cellProps} className={tableStyles.cellContainer}>
{!link && <img src={displayValue.text} className={tableStyles.imageCell} />} {!hasLinks && <img src={displayValue.text} className={tableStyles.imageCell} />}
{link && ( {hasLinks && (
<a <DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
href={link.href} {(api) => {
onClick={onClick} return (
target={link.target} <div onClick={api.openMenu} className={cx(tableStyles.imageCellLink, api.targetClassName)}>
title={link.title} <img src={displayValue.text} className={tableStyles.imageCell} />
className={tableStyles.imageCellLink} </div>
> );
<img src={displayValue.text} className={tableStyles.imageCell} /> }}
</a> </DataLinksContextMenu>
)} )}
</div> </div>
); );

View File

@ -3,6 +3,7 @@ import { isString } from 'lodash';
import React from 'react'; import React from 'react';
import { getCellLinks } from '../../utils'; import { getCellLinks } from '../../utils';
import { DataLinksContextMenu } from '../DataLinks/DataLinksContextMenu';
import { CellActions } from './CellActions'; import { CellActions } from './CellActions';
import { TableCellProps, TableFieldOptions } from './types'; import { TableCellProps, TableFieldOptions } from './types';
@ -26,22 +27,22 @@ export function JSONViewCell(props: TableCellProps): JSX.Element {
displayValue = JSON.stringify(value, null, ' '); displayValue = JSON.stringify(value, null, ' ');
} }
const { link, onClick } = getCellLinks(field, row); const hasLinks = getCellLinks(field, row)?.length;
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)}>
{!link && <div className={tableStyles.cellText}>{displayValue}</div>} {!hasLinks && <div className={tableStyles.cellText}>{displayValue}</div>}
{link && ( {hasLinks && (
<a <DataLinksContextMenu links={() => getCellLinks(field, row) || []}>
href={link.href} {(api) => {
onClick={onClick} return (
target={link.target} <div onClick={api.openMenu} className={api.targetClassName}>
title={link.title} {displayValue}
className={tableStyles.cellLink} </div>
> );
{displayValue} }}
</a> </DataLinksContextMenu>
)} )}
</div> </div>
{inspectEnabled && <CellActions {...props} previewMode="code" />} {inspectEnabled && <CellActions {...props} previewMode="code" />}

View File

@ -1,7 +1,6 @@
import { LinkModel } from '@grafana/data'; import { LinkModel } from '@grafana/data';
import { MenuItemProps } from '../components/Menu/MenuItem'; import { MenuItemProps } from '../components/Menu/MenuItem';
import { IconName } from '../types';
/** /**
* Delays creating links until we need to open the ContextMenu * Delays creating links until we need to open the ContextMenu
@ -14,7 +13,7 @@ export const linkModelToContextMenuItems: (links: () => LinkModel[]) => MenuItem
// TODO: rename to href // TODO: rename to href
url: link.href, url: link.href,
target: link.target, target: link.target,
icon: `${link.target === '_self' ? 'link' : 'external-link-alt'}` as IconName, icon: `${link.target === '_blank' ? 'external-link-alt' : 'link'}`,
onClick: link.onClick, onClick: link.onClick,
}; };
}); });

View File

@ -1,4 +1,3 @@
import { MouseEventHandler } from 'react';
import { Row } from 'react-table'; import { Row } from 'react-table';
import { Field, LinkModel } from '@grafana/data'; import { Field, LinkModel } from '@grafana/data';
@ -7,29 +6,33 @@ import { Field, LinkModel } from '@grafana/data';
* @internal * @internal
*/ */
export const getCellLinks = (field: Field, row: Row<any>) => { export const getCellLinks = (field: Field, row: Row<any>) => {
let link: LinkModel<any> | undefined; let links: Array<LinkModel<any>> | undefined;
let onClick: MouseEventHandler<HTMLAnchorElement> | undefined;
if (field.getLinks) { if (field.getLinks) {
link = field.getLinks({ links = field.getLinks({
valueRowIndex: row.index, valueRowIndex: row.index,
})[0]; });
} }
//const fieldLink = link?.onClick; if (!links) {
if (link?.onClick) { return;
onClick = (event) => {
// Allow opening in new tab
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
event.preventDefault();
link!.onClick!(event, {
field,
rowIndex: row.index,
});
}
};
} }
return {
link, for (let i = 0; i < links?.length; i++) {
onClick, if (links[i].onClick) {
}; const origOnClick = links[i].onClick;
links[i].onClick = (event) => {
// Allow opening in new tab
if (!(event.ctrlKey || event.metaKey || event.shiftKey)) {
event.preventDefault();
origOnClick!(event, {
field,
rowIndex: row.index,
});
}
};
}
}
return links;
}; };

View File

@ -60,9 +60,7 @@ export class BarGaugePanel extends PureComponent<PanelProps<PanelOptions>> {
if (hasLinks && getLinks) { if (hasLinks && getLinks) {
return ( return (
<div style={{ width: '100%', display: orientation === VizOrientation.Vertical ? 'flex' : 'initial' }}> <div style={{ width: '100%', display: orientation === VizOrientation.Vertical ? 'flex' : 'initial' }}>
<DataLinksContextMenu links={getLinks} config={value.field}> <DataLinksContextMenu links={getLinks}>{(api) => this.renderComponent(valueProps, api)}</DataLinksContextMenu>
{(api) => this.renderComponent(valueProps, api)}
</DataLinksContextMenu>
</div> </div>
); );
} }

View File

@ -41,7 +41,7 @@ export class GaugePanel extends PureComponent<PanelProps<PanelOptions>> {
if (hasLinks && getLinks) { if (hasLinks && getLinks) {
return ( return (
<DataLinksContextMenu links={getLinks} config={value.field}> <DataLinksContextMenu links={getLinks}>
{(api) => { {(api) => {
return this.renderComponent(valueProps, api); return this.renderComponent(valueProps, api);
}} }}

View File

@ -118,7 +118,7 @@ export const PieChart: FC<PieChartProps> = ({
if (arc.data.hasLinks && arc.data.getLinks) { if (arc.data.hasLinks && arc.data.getLinks) {
return ( return (
<DataLinksContextMenu config={arc.data.field} key={arc.index} links={arc.data.getLinks}> <DataLinksContextMenu key={arc.index} links={arc.data.getLinks}>
{(api) => ( {(api) => (
<PieSlice <PieSlice
tooltip={tooltip} tooltip={tooltip}

View File

@ -68,7 +68,7 @@ export class StatPanel extends PureComponent<PanelProps<PanelOptions>> {
if (hasLinks && getLinks) { if (hasLinks && getLinks) {
return ( return (
<DataLinksContextMenu links={getLinks} config={value.field}> <DataLinksContextMenu links={getLinks}>
{(api) => { {(api) => {
return this.renderComponent(valueProps, api); return this.renderComponent(valueProps, api);
}} }}