mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Geomap: Improve tooltip UX and fix data links (#44740)
This commit is contained in:
parent
04d93751b8
commit
b2b584f611
@ -62,7 +62,7 @@ export interface MapLayerOptions<TConfig = any> {
|
||||
// Layer opacity (0-1)
|
||||
opacity?: number;
|
||||
|
||||
//Check tooltip
|
||||
// Check tooltip
|
||||
tooltip?: boolean;
|
||||
}
|
||||
|
||||
|
@ -13,6 +13,7 @@ export interface VizTooltipContainerProps extends HTMLAttributes<HTMLDivElement>
|
||||
position: { x: number; y: number };
|
||||
offset: { x: number; y: number };
|
||||
children?: React.ReactNode;
|
||||
allowPointerEvents?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -22,6 +23,7 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
|
||||
position: { x: positionX, y: positionY },
|
||||
offset: { x: offsetX, y: offsetY },
|
||||
children,
|
||||
allowPointerEvents,
|
||||
className,
|
||||
...otherProps
|
||||
}) => {
|
||||
@ -90,7 +92,7 @@ export const VizTooltipContainer: React.FC<VizTooltipContainerProps> = ({
|
||||
left: 0,
|
||||
// disabling pointer-events is to prevent the tooltip from flickering when moving left to right
|
||||
// see e.g. https://github.com/grafana/grafana/pull/33609
|
||||
pointerEvents: 'none',
|
||||
pointerEvents: allowPointerEvents ? 'auto' : 'none',
|
||||
top: 0,
|
||||
transform: `translate(${placement.x}px, ${placement.y}px)`,
|
||||
transition: 'transform ease-out 0.1s',
|
||||
|
@ -6,11 +6,14 @@ import { GrafanaTheme2 } from '@grafana/data';
|
||||
type Props = {
|
||||
onClick: () => void;
|
||||
'aria-label'?: string;
|
||||
style?: React.CSSProperties;
|
||||
};
|
||||
|
||||
export const CloseButton: React.FC<Props> = ({ onClick, 'aria-label': ariaLabel }) => {
|
||||
export const CloseButton: React.FC<Props> = ({ onClick, 'aria-label': ariaLabel, style }) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
return <IconButton aria-label={ariaLabel ?? 'Close'} className={styles} name="times" onClick={onClick} />;
|
||||
return (
|
||||
<IconButton aria-label={ariaLabel ?? 'Close'} className={styles} name="times" onClick={onClick} style={style} />
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) =>
|
||||
|
@ -23,17 +23,17 @@ import { centerPointRegistry, MapCenterID } from './view';
|
||||
import { fromLonLat, toLonLat } from 'ol/proj';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { css } from '@emotion/css';
|
||||
import { PanelContext, PanelContextRoot, Portal, stylesFactory, VizTooltipContainer } from '@grafana/ui';
|
||||
import { PanelContext, PanelContextRoot, stylesFactory } from '@grafana/ui';
|
||||
import { GeomapOverlay, OverlayProps } from './GeomapOverlay';
|
||||
import { DebugOverlay } from './components/DebugOverlay';
|
||||
import { getGlobalStyles } from './globalStyles';
|
||||
import { Global } from '@emotion/react';
|
||||
import { GeomapHoverFeature, GeomapHoverPayload } from './event';
|
||||
import { DataHoverView } from './components/DataHoverView';
|
||||
import { GeomapHoverPayload, GeomapLayerHover } from './event';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { PanelEditExitedEvent } from 'app/types/events';
|
||||
import { defaultMarkersConfig, MARKERS_LAYER_ID } from './layers/data/markersLayer';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { GeomapTooltip } from './GeomapTooltip';
|
||||
|
||||
// Allows multiple panels to share the same view instance
|
||||
let sharedView: View | undefined = undefined;
|
||||
@ -41,6 +41,7 @@ let sharedView: View | undefined = undefined;
|
||||
type Props = PanelProps<GeomapPanelOptions>;
|
||||
interface State extends OverlayProps {
|
||||
ttip?: GeomapHoverPayload;
|
||||
ttipOpen: boolean;
|
||||
}
|
||||
|
||||
export interface GeomapLayerActions {
|
||||
@ -77,7 +78,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
this.state = { ttipOpen: false };
|
||||
this.subs.add(
|
||||
this.props.eventBus.subscribe(PanelEditExitedEvent, (evt) => {
|
||||
if (this.mapDiv && this.props.id === evt.payload) {
|
||||
@ -288,6 +289,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.forceUpdate(); // first render
|
||||
|
||||
// Tooltip listener
|
||||
this.map.on('singleclick', this.pointerClickListener);
|
||||
this.map.on('pointermove', this.pointerMoveListener);
|
||||
this.map.getViewport().addEventListener('mouseout', (evt) => {
|
||||
this.props.eventBus.publish(new DataHoverClearEvent());
|
||||
@ -305,14 +307,26 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
};
|
||||
|
||||
clearTooltip = () => {
|
||||
if (this.state.ttip) {
|
||||
this.setState({ ttip: undefined });
|
||||
if (this.state.ttip && !this.state.ttipOpen) {
|
||||
this.tooltipPopupClosed();
|
||||
}
|
||||
};
|
||||
|
||||
tooltipPopupClosed = () => {
|
||||
this.setState({ ttipOpen: false, ttip: undefined });
|
||||
};
|
||||
|
||||
pointerClickListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
if (this.pointerMoveListener(evt)) {
|
||||
evt.preventDefault();
|
||||
evt.stopPropagation();
|
||||
this.setState({ ttipOpen: true });
|
||||
}
|
||||
};
|
||||
|
||||
pointerMoveListener = (evt: MapBrowserEvent<UIEvent>) => {
|
||||
if (!this.map) {
|
||||
return;
|
||||
if (!this.map || this.state.ttipOpen) {
|
||||
return false;
|
||||
}
|
||||
const mouse = evt.originalEvent as any;
|
||||
const pixel = this.map.getEventPixel(mouse);
|
||||
@ -328,27 +342,38 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
hoverPayload.data = undefined;
|
||||
hoverPayload.columnIndex = undefined;
|
||||
hoverPayload.rowIndex = undefined;
|
||||
hoverPayload.feature = undefined;
|
||||
hoverPayload.layers = undefined;
|
||||
|
||||
const layers: GeomapLayerHover[] = [];
|
||||
const layerLookup = new Map<MapLayerState, GeomapLayerHover>();
|
||||
|
||||
let ttip: GeomapHoverPayload = {} as GeomapHoverPayload;
|
||||
const features: GeomapHoverFeature[] = [];
|
||||
this.map.forEachFeatureAtPixel(
|
||||
pixel,
|
||||
(feature, layer, geo) => {
|
||||
//match hover layer to layer in layers
|
||||
//check if the layer show tooltip is enabled
|
||||
//then also pass the list of tooltip fields if exists
|
||||
//this is used as the generic hover event
|
||||
if (!hoverPayload.data) {
|
||||
const props = feature.getProperties();
|
||||
const frame = props['frame'];
|
||||
if (frame) {
|
||||
hoverPayload.data = ttip.data = frame as DataFrame;
|
||||
hoverPayload.rowIndex = ttip.rowIndex = props['rowIndex'];
|
||||
} else {
|
||||
hoverPayload.feature = ttip.feature = feature;
|
||||
}
|
||||
}
|
||||
features.push({ feature, layer, geo });
|
||||
|
||||
const s: MapLayerState = (layer as any).__state;
|
||||
if (s) {
|
||||
let h = layerLookup.get(s);
|
||||
if (!h) {
|
||||
h = { layer: s, features: [] };
|
||||
layerLookup.set(s, h);
|
||||
layers.push(h);
|
||||
}
|
||||
h.features.push(feature);
|
||||
}
|
||||
},
|
||||
{
|
||||
layerFilter: (l) => {
|
||||
@ -357,17 +382,11 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
},
|
||||
}
|
||||
);
|
||||
this.hoverPayload.features = features.length ? features : undefined;
|
||||
this.hoverPayload.layers = layers.length ? layers : undefined;
|
||||
this.props.eventBus.publish(this.hoverEvent);
|
||||
|
||||
const currentTTip = this.state.ttip;
|
||||
if (
|
||||
ttip.data !== currentTTip?.data ||
|
||||
ttip.rowIndex !== currentTTip?.rowIndex ||
|
||||
ttip.feature !== currentTTip?.feature
|
||||
) {
|
||||
this.setState({ ttip: { ...hoverPayload } });
|
||||
}
|
||||
this.setState({ ttip: { ...hoverPayload } });
|
||||
return layers.length ? true : false;
|
||||
};
|
||||
|
||||
private updateLayer = async (uid: string, newOptions: MapLayerOptions): Promise<boolean> => {
|
||||
@ -558,7 +577,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { ttip, topRight, bottomLeft } = this.state;
|
||||
const { ttip, ttipOpen, topRight, bottomLeft } = this.state;
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -567,13 +586,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
<div className={this.style.map} ref={this.initMapRef}></div>
|
||||
<GeomapOverlay bottomLeft={bottomLeft} topRight={topRight} />
|
||||
</div>
|
||||
<Portal>
|
||||
{ttip && (ttip.data || ttip.feature) && (
|
||||
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }}>
|
||||
<DataHoverView {...ttip} />
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</Portal>
|
||||
<GeomapTooltip ttip={ttip} isOpen={ttipOpen} onClose={this.tooltipPopupClosed} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
29
public/app/plugins/panel/geomap/GeomapTooltip.tsx
Normal file
29
public/app/plugins/panel/geomap/GeomapTooltip.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { createRef } from 'react';
|
||||
import { VizTooltipContainer } from '@grafana/ui';
|
||||
import { useOverlay } from '@react-aria/overlays';
|
||||
|
||||
import { ComplexDataHoverView } from './components/ComplexDataHoverView';
|
||||
import { GeomapHoverPayload } from './event';
|
||||
|
||||
interface Props {
|
||||
ttip?: GeomapHoverPayload;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const GeomapTooltip = ({ ttip, onClose, isOpen }: Props) => {
|
||||
const ref = createRef<HTMLElement>();
|
||||
const { overlayProps } = useOverlay({ onClose, isDismissable: true, isOpen }, ref);
|
||||
|
||||
return (
|
||||
<>
|
||||
{ttip && ttip.layers && (
|
||||
<VizTooltipContainer position={{ x: ttip.pageX, y: ttip.pageY }} offset={{ x: 10, y: 10 }} allowPointerEvents>
|
||||
<section ref={ref} {...overlayProps}>
|
||||
<ComplexDataHoverView {...ttip} isOpen={isOpen} onClose={onClose} />
|
||||
</section>
|
||||
</VizTooltipContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
@ -0,0 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { GeomapLayerHover } from '../event';
|
||||
import { DataHoverTabs } from './DataHoverTabs';
|
||||
import { DataHoverRows } from './DataHoverRows';
|
||||
import { CloseButton } from 'app/core/components/CloseButton/CloseButton';
|
||||
|
||||
export interface Props {
|
||||
layers?: GeomapLayerHover[];
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ComplexDataHoverView = ({ layers, onClose, isOpen }: Props) => {
|
||||
const [activeTabIndex, setActiveTabIndex] = useState<number>(0);
|
||||
|
||||
if (!layers) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpen && <CloseButton style={{ zIndex: 1 }} onClick={onClose} />}
|
||||
<DataHoverTabs layers={layers} setActiveTabIndex={setActiveTabIndex} activeTabIndex={activeTabIndex} />
|
||||
<DataHoverRows layers={layers} activeTabIndex={activeTabIndex} />
|
||||
</>
|
||||
);
|
||||
};
|
27
public/app/plugins/panel/geomap/components/DataHoverRow.tsx
Normal file
27
public/app/plugins/panel/geomap/components/DataHoverRow.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
import React from 'react';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { ArrayDataFrame, DataFrame } from '@grafana/data';
|
||||
|
||||
import { DataHoverView } from './DataHoverView';
|
||||
|
||||
type Props = {
|
||||
feature?: FeatureLike;
|
||||
};
|
||||
|
||||
export const DataHoverRow = ({ feature }: Props) => {
|
||||
let data: DataFrame;
|
||||
let rowIndex = 0;
|
||||
if (!feature) {
|
||||
return null;
|
||||
}
|
||||
|
||||
data = feature.get('frame');
|
||||
if (data) {
|
||||
rowIndex = feature.get('rowIndex');
|
||||
} else {
|
||||
const { geometry, ...properties } = feature.getProperties();
|
||||
data = new ArrayDataFrame([properties]);
|
||||
}
|
||||
|
||||
return <DataHoverView data={data} rowIndex={rowIndex} />;
|
||||
};
|
103
public/app/plugins/panel/geomap/components/DataHoverRows.tsx
Normal file
103
public/app/plugins/panel/geomap/components/DataHoverRows.tsx
Normal file
@ -0,0 +1,103 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Collapse, TabContent, useStyles2 } from '@grafana/ui';
|
||||
import { DataFrame, FieldType, getFieldDisplayName, GrafanaTheme2 } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
|
||||
import { GeomapLayerHover } from '../event';
|
||||
import { DataHoverRow } from './DataHoverRow';
|
||||
import { isString } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
layers: GeomapLayerHover[];
|
||||
activeTabIndex: number;
|
||||
};
|
||||
|
||||
export const DataHoverRows = ({ layers, activeTabIndex }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
const [rowMap, setRowMap] = useState(new Map<string | number, boolean>());
|
||||
|
||||
const updateRowMap = (key: string | number, value: boolean) => {
|
||||
setRowMap(new Map(rowMap.set(key, value)));
|
||||
};
|
||||
|
||||
return (
|
||||
<TabContent>
|
||||
{layers.map(
|
||||
(geomapLayer, index) =>
|
||||
index === activeTabIndex && (
|
||||
<div key={geomapLayer.layer.getName()}>
|
||||
<div>
|
||||
{geomapLayer.features.map((feature, idx) => {
|
||||
const key = feature.getId() ?? idx;
|
||||
const shouldDisplayCollapse = geomapLayer.features.length > 1;
|
||||
|
||||
return shouldDisplayCollapse ? (
|
||||
<Collapse
|
||||
key={key}
|
||||
collapsible
|
||||
label={generateLabel(feature, idx)}
|
||||
isOpen={rowMap.get(key)}
|
||||
onToggle={() => {
|
||||
updateRowMap(key, !rowMap.get(key));
|
||||
}}
|
||||
className={styles.collapsibleRow}
|
||||
>
|
||||
<DataHoverRow feature={feature} />
|
||||
</Collapse>
|
||||
) : (
|
||||
<DataHoverRow key={key} feature={feature} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</TabContent>
|
||||
);
|
||||
};
|
||||
|
||||
export const generateLabel = (feature: FeatureLike, idx: number): string => {
|
||||
const names = ['Name', 'name', 'Title', 'ID', 'id'];
|
||||
let props = feature.getProperties();
|
||||
let first = '';
|
||||
const frame = feature.get('frame') as DataFrame;
|
||||
if (frame) {
|
||||
const rowIndex = feature.get('rowIndex');
|
||||
for (const f of frame.fields) {
|
||||
if (f.type === FieldType.string) {
|
||||
const k = getFieldDisplayName(f, frame);
|
||||
if (!first) {
|
||||
first = k;
|
||||
}
|
||||
props[k] = f.values.get(rowIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (let k of names) {
|
||||
const v = props[k];
|
||||
if (v) {
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
if (first) {
|
||||
return `${first}: ${props[first]}`;
|
||||
}
|
||||
|
||||
for (let k of Object.keys(props)) {
|
||||
const v = props[k];
|
||||
if (isString(v)) {
|
||||
return `${k}: ${v}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `Match: ${idx + 1}`;
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
collapsibleRow: css`
|
||||
margin-bottom: 0px;
|
||||
`,
|
||||
});
|
29
public/app/plugins/panel/geomap/components/DataHoverTabs.tsx
Normal file
29
public/app/plugins/panel/geomap/components/DataHoverTabs.tsx
Normal file
@ -0,0 +1,29 @@
|
||||
import React, { Dispatch, SetStateAction } from 'react';
|
||||
import { Tab, TabsBar } from '@grafana/ui';
|
||||
|
||||
import { GeomapLayerHover } from '../event';
|
||||
|
||||
type Props = {
|
||||
layers?: GeomapLayerHover[];
|
||||
setActiveTabIndex: Dispatch<SetStateAction<number>>;
|
||||
activeTabIndex: number;
|
||||
};
|
||||
|
||||
export const DataHoverTabs = ({ layers, setActiveTabIndex, activeTabIndex }: Props) => {
|
||||
return (
|
||||
<TabsBar>
|
||||
{layers &&
|
||||
layers.map((g, index) => (
|
||||
<Tab
|
||||
key={index}
|
||||
label={g.layer.getName()}
|
||||
active={index === activeTabIndex}
|
||||
counter={g.features.length > 1 ? g.features.length : null}
|
||||
onChangeTab={() => {
|
||||
setActiveTabIndex(index);
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</TabsBar>
|
||||
);
|
||||
};
|
@ -1,85 +1,97 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
import React from 'react';
|
||||
import { LinkButton, useStyles2, VerticalGroup } from '@grafana/ui';
|
||||
import {
|
||||
ArrayDataFrame,
|
||||
arrayUtils,
|
||||
DataFrame,
|
||||
Field,
|
||||
formattedValueToString,
|
||||
getFieldDisplayName,
|
||||
GrafanaTheme2,
|
||||
LinkModel,
|
||||
} from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { config } from 'app/core/config';
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { SortOrder } from '@grafana/schema';
|
||||
|
||||
export interface Props {
|
||||
data?: DataFrame; // source data
|
||||
feature?: FeatureLike;
|
||||
rowIndex?: number | null; // the hover row
|
||||
columnIndex?: number | null; // the hover column
|
||||
sortOrder?: SortOrder;
|
||||
}
|
||||
|
||||
export class DataHoverView extends PureComponent<Props> {
|
||||
style = getStyles(config.theme2);
|
||||
export const DataHoverView = ({ data, rowIndex, columnIndex, sortOrder }: Props) => {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
render() {
|
||||
const { feature, columnIndex, sortOrder } = this.props;
|
||||
let { data, rowIndex } = this.props;
|
||||
if (feature) {
|
||||
const { geometry, ...properties } = feature.getProperties();
|
||||
data = new ArrayDataFrame([properties]);
|
||||
rowIndex = 0;
|
||||
}
|
||||
|
||||
if (!data || rowIndex == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const displayValues: Array<[string, any, string]> = [];
|
||||
const visibleFields = data.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
||||
|
||||
if (visibleFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 0; i < visibleFields.length; i++) {
|
||||
displayValues.push([
|
||||
getFieldDisplayName(visibleFields[i], data),
|
||||
visibleFields[i].values.get(rowIndex!),
|
||||
fmt(visibleFields[i], rowIndex),
|
||||
]);
|
||||
}
|
||||
|
||||
if (sortOrder && sortOrder !== SortOrder.None) {
|
||||
displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a[1], b[1]));
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={this.style.infoWrap}>
|
||||
<tbody>
|
||||
{displayValues.map((v, i) => (
|
||||
<tr key={`${i}/${rowIndex}`} className={i === columnIndex ? this.style.highlight : ''}>
|
||||
<th>{v[0]}:</th>
|
||||
<td>{v[2]}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
if (!data || rowIndex == null) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(field: Field, row: number): string {
|
||||
const v = field.values.get(row);
|
||||
if (field.display) {
|
||||
return formattedValueToString(field.display(v));
|
||||
const visibleFields = data.fields.filter((f) => !Boolean(f.config.custom?.hideFrom?.tooltip));
|
||||
|
||||
if (visibleFields.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return `${v}`;
|
||||
}
|
||||
|
||||
const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
const displayValues: Array<[string, any, string]> = [];
|
||||
const links: Array<LinkModel<Field>> = [];
|
||||
const linkLookup = new Set<string>();
|
||||
|
||||
for (const f of visibleFields) {
|
||||
const v = f.values.get(rowIndex);
|
||||
const disp = f.display ? f.display(v) : { text: `${v}`, numeric: +v };
|
||||
if (f.getLinks) {
|
||||
f.getLinks({ calculatedValue: disp, valueRowIndex: rowIndex }).forEach((link) => {
|
||||
const key = `${link.title}/${link.href}`;
|
||||
if (!linkLookup.has(key)) {
|
||||
links.push(link);
|
||||
linkLookup.add(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayValues.push([getFieldDisplayName(f, data), v, formattedValueToString(disp)]);
|
||||
}
|
||||
|
||||
if (sortOrder && sortOrder !== SortOrder.None) {
|
||||
displayValues.sort((a, b) => arrayUtils.sortValues(sortOrder)(a[1], b[1]));
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={styles.infoWrap}>
|
||||
<tbody>
|
||||
{displayValues.map((v, i) => (
|
||||
<tr key={`${i}/${rowIndex}`} className={i === columnIndex ? styles.highlight : ''}>
|
||||
<th>{v[0]}:</th>
|
||||
<td>{v[2]}</td>
|
||||
</tr>
|
||||
))}
|
||||
{links.length > 0 && (
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
<VerticalGroup>
|
||||
{links.map((link, i) => (
|
||||
<LinkButton
|
||||
key={i}
|
||||
icon={'external-link-alt'}
|
||||
target={link.target}
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
fill="text"
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
{link.title}
|
||||
</LinkButton>
|
||||
))}
|
||||
</VerticalGroup>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
infoWrap: css`
|
||||
padding: 8px;
|
||||
th {
|
||||
@ -90,4 +102,4 @@ const getStyles = stylesFactory((theme: GrafanaTheme2) => ({
|
||||
highlight: css`
|
||||
background: ${theme.colors.action.hover};
|
||||
`,
|
||||
}));
|
||||
});
|
||||
|
@ -1,17 +1,17 @@
|
||||
import { FeatureLike } from 'ol/Feature';
|
||||
import { SimpleGeometry } from 'ol/geom';
|
||||
import { DataHoverPayload } from '@grafana/data';
|
||||
import BaseLayer from 'ol/layer/Base';
|
||||
import { MapLayerState } from './types';
|
||||
|
||||
export interface GeomapHoverFeature {
|
||||
feature: FeatureLike;
|
||||
layer: BaseLayer;
|
||||
geo: SimpleGeometry;
|
||||
export interface GeomapLayerHover {
|
||||
layer: MapLayerState;
|
||||
features: FeatureLike[];
|
||||
}
|
||||
|
||||
export interface GeomapHoverPayload extends DataHoverPayload {
|
||||
features?: GeomapHoverFeature[];
|
||||
feature?: FeatureLike;
|
||||
// List of layers
|
||||
layers?: GeomapLayerHover[];
|
||||
|
||||
// Global mouse coordinates for the hover layer
|
||||
pageX: number;
|
||||
pageY: number;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user