Skip to content

Commit 1f4b066

Browse files
authored
Merge pull request #19 from grafana/bohandley/metrics-explorer-type-fixes
Metrics Explorer SelecMenuOptions fix from grafana/ui
2 parents 4bf82c7 + fed7bb2 commit 1f4b066

File tree

2 files changed

+128
-1
lines changed

2 files changed

+128
-1
lines changed
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { cx } from '@emotion/css';
2+
import { SelectableValue, toIconName } from '@grafana/data';
3+
import { CustomScrollbar, Icon, getSelectStyles, useTheme2 } from '@grafana/ui';
4+
import { max } from 'lodash';
5+
import React, { RefCallback } from 'react';
6+
import { MenuListProps } from 'react-select';
7+
import { FixedSizeList as List } from 'react-window';
8+
9+
interface SelectMenuProps {
10+
maxHeight: number;
11+
innerRef: RefCallback<HTMLDivElement>;
12+
innerProps: {};
13+
}
14+
15+
export const SelectMenu = ({ children, maxHeight, innerRef, innerProps }: React.PropsWithChildren<SelectMenuProps>) => {
16+
const theme = useTheme2();
17+
const styles = getSelectStyles(theme);
18+
19+
return (
20+
<div {...innerProps} className={styles.menu} style={{ maxHeight }} aria-label="Select options menu">
21+
<CustomScrollbar scrollRefCallback={innerRef} autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
22+
{children}
23+
</CustomScrollbar>
24+
</div>
25+
);
26+
};
27+
28+
SelectMenu.displayName = 'SelectMenu';
29+
30+
const VIRTUAL_LIST_ITEM_HEIGHT = 37;
31+
const VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER = 7;
32+
33+
// A virtualized version of the SelectMenu, descriptions for SelectableValue options not supported since those are of a variable height.
34+
//
35+
// To support the virtualized list we have to "guess" the width of the menu container based on the longest available option.
36+
// the reason for this is because all of the options will be positioned absolute, this takes them out of the document and no space
37+
// is created for them, thus the container can't grow to accomodate.
38+
//
39+
// VIRTUAL_LIST_ITEM_HEIGHT and WIDTH_ESTIMATE_MULTIPLIER are both magic numbers.
40+
// Some characters (such as emojis and other unicode characters) may consist of multiple code points in which case the width would be inaccurate (but larger than needed).
41+
export const VirtualizedSelectMenu = ({ children, maxHeight, options, getValue }: MenuListProps<SelectableValue>) => {
42+
const theme = useTheme2();
43+
const styles = getSelectStyles(theme);
44+
const [value] = getValue();
45+
46+
const valueIndex = value ? options.findIndex((option: SelectableValue<unknown>) => option.value === value.value) : 0;
47+
const initialOffset = valueIndex * VIRTUAL_LIST_ITEM_HEIGHT;
48+
49+
if (!Array.isArray(children)) {
50+
return null;
51+
}
52+
53+
const longestOption = max(options.map((option) => option.label?.length)) ?? 0;
54+
const widthEstimate = longestOption * VIRTUAL_LIST_WIDTH_ESTIMATE_MULTIPLIER;
55+
const heightEstimate = Math.min(options.length * VIRTUAL_LIST_ITEM_HEIGHT, maxHeight);
56+
57+
return (
58+
<List
59+
className={styles.menu}
60+
height={heightEstimate}
61+
width={widthEstimate}
62+
aria-label="Select options menu"
63+
itemCount={children.length}
64+
itemSize={VIRTUAL_LIST_ITEM_HEIGHT}
65+
initialScrollOffset={initialOffset}
66+
>
67+
{({ index, style }) => <div style={{ ...style, overflow: 'hidden' }}>{children[index]}</div>}
68+
</List>
69+
);
70+
};
71+
72+
VirtualizedSelectMenu.displayName = 'VirtualizedSelectMenu';
73+
74+
interface SelectMenuOptionProps<T> {
75+
isDisabled: boolean;
76+
isFocused: boolean;
77+
isSelected: boolean;
78+
innerProps: JSX.IntrinsicElements['div'];
79+
innerRef: RefCallback<HTMLDivElement>;
80+
renderOptionLabel?: (value: SelectableValue<T>) => JSX.Element;
81+
data: SelectableValue<T>;
82+
}
83+
84+
export const SelectMenuOptions = ({
85+
children,
86+
data,
87+
innerProps,
88+
innerRef,
89+
isFocused,
90+
isSelected,
91+
renderOptionLabel,
92+
}: React.PropsWithChildren<SelectMenuOptionProps<unknown>>) => {
93+
const theme = useTheme2();
94+
const styles = getSelectStyles(theme);
95+
const icon = data.icon ? toIconName(data.icon) : undefined;
96+
// We are removing onMouseMove and onMouseOver from innerProps because they cause the whole
97+
// list to re-render everytime the user hovers over an option. This is a performance issue.
98+
// See https://github.com/JedWatson/react-select/issues/3128#issuecomment-451936743
99+
const { onMouseMove, onMouseOver, ...rest } = innerProps;
100+
101+
return (
102+
<div
103+
ref={innerRef}
104+
className={cx(
105+
styles.option,
106+
isFocused && styles.optionFocused,
107+
isSelected && styles.optionSelected,
108+
data.isDisabled && styles.optionDisabled
109+
)}
110+
{...rest}
111+
aria-label="Select option"
112+
title={data.title}
113+
>
114+
{icon && <Icon name={icon} className={styles.optionIcon} />}
115+
{data.imgUrl && <img className={styles.optionImage} src={data.imgUrl} alt={data.label || String(data.value)} />}
116+
<div className={styles.optionBody}>
117+
<span>{renderOptionLabel ? renderOptionLabel(data) : children}</span>
118+
{data.description && <div className={styles.optionDescription}>{data.description}</div>}
119+
{data.component && <data.component />}
120+
</div>
121+
</div>
122+
);
123+
};
124+
125+
SelectMenuOptions.displayName = 'SelectMenuOptions';

src/querybuilder/components/MetricSelect.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ import {
1414
useStyles2,
1515
useTheme2,
1616
} from '@grafana/ui';
17-
import {SelectMenuOptions} from '@grafana/ui/src/components/Select/SelectMenu';
17+
// import {SelectMenuOptions} from '@grafana/ui/src/components/Select/SelectMenu';
1818
import debounce from 'debounce-promise';
19+
import { SelectMenuOptions } from 'gcopypaste/packages/grafana-ui/src/components/Select/SelectBase';
1920
import React, {RefCallback, useCallback, useState} from 'react';
2021
import Highlighter from 'react-highlight-words';
2122

@@ -28,6 +29,7 @@ import {PromVisualQuery} from '../types';
2829
import {MetricsModal} from './metrics-modal/MetricsModal';
2930
import {tracking} from './metrics-modal/state/helpers';
3031

32+
3133
// We are matching words split with space
3234
const splitSeparator = ' ';
3335

0 commit comments

Comments
 (0)