Skip to content

Commit d31d157

Browse files
bohandleyleeoniya
andauthored
Prometheus: Metric encyclopedia improvements (grafana#67084)
* move to me directory and sort by relevance * refactor letter search, uFuzzy and styles out of ency * begin state refactor * refactor getMetaData useEffect call with useReducer * refactor pagination with useReducer * refactor fuzzy state for useReducer * refactor all filters for useReducer * remove haystacks arrays in favor of haystack dictionaries w object keys * refactor out functions into state helpers * switch label filter text color to work with light theme * make each row clickable to select metric * add pagination component * fix max results * make a better table with keystrokes, navigate to metric with up&down, select on enter, hide settings, make a nice button * save space, give more real esate to the table * add highlighting for fuzzy search matches * add custom button in metric select option to open metric encyclopedia * open the modal with enter keystroke * remove unnecessary actions and variables from m.e. * fix tests, clean code * remove setting of selected idx on results row when hovering * tests * rename to metrics modal and have select option same as header * reduce width for wider screens * pass in initial metrics list and remove call to labels and series in metrics modal * use createSlice from reduc toolkit to deduce actions * save the metrics modal additional settings * galen alphabet refactor suggestion * remove extra row in results table * fix storing settings, wrap in feature toggle * remove metadata check & load because metric select already handles this * Update public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/LetterSearch.tsx Co-authored-by: Leon Sorokin <leeoniya@gmail.com> * fix styles, show cursor as pointer for select option and clickable row * taller modal for larger screens * turn off metadata settings if usebackend is selected * additional settings button space * add pipe to ufuzzy metadata search --------- Co-authored-by: Leon Sorokin <leeoniya@gmail.com>
1 parent 0d52d19 commit d31d157

File tree

16 files changed

+1474
-980
lines changed

16 files changed

+1474
-980
lines changed

public/app/plugins/datasource/prometheus/querybuilder/components/MetricEncyclopediaModal.tsx

Lines changed: 0 additions & 850 deletions
This file was deleted.

public/app/plugins/datasource/prometheus/querybuilder/components/MetricSelect.tsx

Lines changed: 137 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ import Highlighter from 'react-highlight-words';
55

66
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
77
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
8-
import { AsyncSelect, FormatOptionLabelMeta, useStyles2 } from '@grafana/ui';
8+
import { config } from '@grafana/runtime';
9+
import { AsyncSelect, Button, FormatOptionLabelMeta, useStyles2 } from '@grafana/ui';
10+
import { SelectMenuOptions } from '@grafana/ui/src/components/Select/SelectMenu';
911

1012
import { PrometheusDatasource } from '../../datasource';
1113
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
1214
import { QueryBuilderLabelFilter } from '../shared/types';
1315
import { PromVisualQuery } from '../types';
1416

17+
import { MetricsModal } from './metrics-modal/MetricsModal';
18+
1519
// We are matching words split with space
1620
const splitSeparator = ' ';
1721

@@ -26,6 +30,8 @@ export interface Props {
2630

2731
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;
2832

33+
const prometheusMetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia;
34+
2935
export function MetricSelect({
3036
datasource,
3137
query,
@@ -38,6 +44,8 @@ export function MetricSelect({
3844
const [state, setState] = useState<{
3945
metrics?: Array<SelectableValue<any>>;
4046
isLoading?: boolean;
47+
metricsModalOpen?: boolean;
48+
initialMetrics?: string[];
4149
}>({});
4250

4351
const customFilterOption = useCallback((option: SelectableValue<any>, searchQuery: string) => {
@@ -129,40 +137,118 @@ export function MetricSelect({
129137
(query: string) => getMetricLabels(query),
130138
datasource.getDebounceTimeInMilliseconds()
131139
);
140+
// No type found for the common select props so typing as any
141+
// https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/Select/SelectBase.tsx/#L212-L263
142+
// eslint-disable-next-line
143+
const CustomOption = (props: any) => {
144+
const option = props.data;
132145

133-
return (
134-
<EditorFieldGroup>
135-
<EditorField label="Metric">
136-
<AsyncSelect
137-
inputId="prometheus-metric-select"
138-
className={styles.select}
139-
value={query.metric ? toOption(query.metric) : undefined}
140-
placeholder={'Select metric'}
141-
allowCustomValue
142-
formatOptionLabel={formatOptionLabel}
143-
filterOption={customFilterOption}
144-
onOpenMenu={async () => {
145-
if (metricLookupDisabled) {
146-
return;
147-
}
148-
setState({ isLoading: true });
149-
const metrics = await onGetMetrics();
150-
if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
151-
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
152-
}
153-
setState({ metrics, isLoading: undefined });
154-
}}
155-
loadOptions={metricLookupDisabled ? metricLookupDisabledSearch : debouncedSearch}
156-
isLoading={state.isLoading}
157-
defaultOptions={state.metrics}
158-
onChange={({ value }) => {
159-
if (value) {
160-
onChange({ ...query, metric: value });
146+
if (option.value === 'BrowseMetrics') {
147+
const isFocused = props.isFocused ? styles.focus : '';
148+
149+
return (
150+
<div
151+
{...props.innerProps}
152+
onKeyDown={(e) => {
153+
// if there is no metric and the m.e. is enabled, open the modal
154+
if (e.code === 'Enter') {
155+
setState({ ...state, metricsModalOpen: true });
161156
}
162157
}}
158+
>
159+
{
160+
<div className={`${styles.customOption} ${isFocused}`}>
161+
<div>
162+
<div>{option.label}</div>
163+
<div className={styles.customOptionDesc}>{option.description}</div>
164+
</div>
165+
<Button
166+
variant="primary"
167+
fill="outline"
168+
size="sm"
169+
onClick={() => setState({ ...state, metricsModalOpen: true })}
170+
icon="book"
171+
>
172+
Open
173+
</Button>
174+
</div>
175+
}
176+
</div>
177+
);
178+
}
179+
180+
return SelectMenuOptions(props);
181+
};
182+
183+
return (
184+
<>
185+
{prometheusMetricEncyclopedia && !datasource.lookupsDisabled && state.metricsModalOpen && (
186+
<MetricsModal
187+
datasource={datasource}
188+
isOpen={state.metricsModalOpen}
189+
onClose={() => setState({ ...state, metricsModalOpen: false })}
190+
query={query}
191+
onChange={onChange}
192+
initialMetrics={state.initialMetrics ?? []}
163193
/>
164-
</EditorField>
165-
</EditorFieldGroup>
194+
)}
195+
<EditorFieldGroup>
196+
<EditorField label="Metric">
197+
<AsyncSelect
198+
inputId="prometheus-metric-select"
199+
className={styles.select}
200+
value={query.metric ? toOption(query.metric) : undefined}
201+
placeholder={'Select metric'}
202+
allowCustomValue
203+
formatOptionLabel={formatOptionLabel}
204+
filterOption={customFilterOption}
205+
onOpenMenu={async () => {
206+
if (metricLookupDisabled) {
207+
return;
208+
}
209+
setState({ isLoading: true });
210+
const metrics = await onGetMetrics();
211+
const initialMetrics: string[] = metrics.map((m) => m.value);
212+
if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
213+
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
214+
}
215+
216+
if (config.featureToggles.prometheusMetricEncyclopedia) {
217+
// pass the initial metrics, possibly filtered by labels into the Metrics Modal
218+
const metricsModalOption: SelectableValue[] = [
219+
{
220+
value: 'BrowseMetrics',
221+
label: 'Browse metrics',
222+
description: 'Browse and filter metrics and metadata with a fuzzy search',
223+
},
224+
];
225+
setState({
226+
metrics: [...metricsModalOption, ...metrics],
227+
isLoading: undefined,
228+
initialMetrics: initialMetrics,
229+
});
230+
} else {
231+
setState({ metrics, isLoading: undefined });
232+
}
233+
}}
234+
loadOptions={metricLookupDisabled ? metricLookupDisabledSearch : debouncedSearch}
235+
isLoading={state.isLoading}
236+
defaultOptions={state.metrics}
237+
onChange={({ value }) => {
238+
if (value) {
239+
// if there is no metric and the m.e. is enabled, open the modal
240+
if (prometheusMetricEncyclopedia && value === 'BrowseMetrics') {
241+
setState({ ...state, metricsModalOpen: true });
242+
} else {
243+
onChange({ ...query, metric: value });
244+
}
245+
}
246+
}}
247+
components={{ Option: CustomOption }}
248+
/>
249+
</EditorField>
250+
</EditorFieldGroup>
251+
</>
166252
);
167253
}
168254

@@ -177,4 +263,24 @@ const getStyles = (theme: GrafanaTheme2) => ({
177263
color: ${theme.colors.warning.contrastText};
178264
background-color: ${theme.colors.warning.main};
179265
`,
266+
customOption: css`
267+
padding: 8px;
268+
display: flex;
269+
justify-content: space-between;
270+
cursor: pointer;
271+
:hover {
272+
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
273+
}
274+
`,
275+
customOptionlabel: css`
276+
color: ${theme.colors.text.primary};
277+
`,
278+
customOptionDesc: css`
279+
color: ${theme.colors.text.secondary};
280+
font-size: ${theme.typography.size.xs};
281+
opacity: 50%;
282+
`,
283+
focus: css`
284+
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
285+
`,
180286
});

public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilder.tsx

Lines changed: 9 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import { css } from '@emotion/css';
21
import React, { useCallback, useState } from 'react';
32

4-
import { DataSourceApi, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
3+
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
54
import { EditorRow } from '@grafana/experimental';
6-
import { config } from '@grafana/runtime';
7-
import { Button, Tag, useStyles2 } from '@grafana/ui';
85

96
import { PrometheusDatasource } from '../../datasource';
107
import { getMetadataString } from '../../language_provider';
@@ -22,7 +19,6 @@ import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../shared/types'
2219
import { PromVisualQuery } from '../types';
2320

2421
import { LabelFilters } from './LabelFilters';
25-
import { MetricEncyclopediaModal } from './MetricEncyclopediaModal';
2622
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
2723
import { NestedQueryList } from './NestedQueryList';
2824
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
@@ -39,12 +35,10 @@ export interface Props {
3935
export const PromQueryBuilder = React.memo<Props>((props) => {
4036
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
4137
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
42-
const [metricEncyclopediaModalOpen, setMetricEncyclopediaModalOpen] = useState(false);
4338
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
4439
onChange({ ...query, labels });
4540
};
4641

47-
const styles = useStyles2(getStyles);
4842
/**
4943
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
5044
*/
@@ -208,55 +202,20 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
208202
}, [datasource, query, withTemplateVariableOptions]);
209203

210204
const lang = { grammar: promqlGrammar, name: 'promql' };
211-
const isMetricEncyclopediaEnabled = config.featureToggles.prometheusMetricEncyclopedia;
212205

213206
const initHints = datasource.getInitHints();
214207

215208
return (
216209
<>
217210
<EditorRow>
218-
{isMetricEncyclopediaEnabled && !datasource.lookupsDisabled ? (
219-
<>
220-
<Button
221-
className={styles.button}
222-
variant="secondary"
223-
size="sm"
224-
onClick={() => setMetricEncyclopediaModalOpen((prevValue) => !prevValue)}
225-
>
226-
Metric encyclopedia
227-
</Button>
228-
{query.metric && (
229-
<Tag
230-
name={' ' + query.metric}
231-
color="#3D71D9"
232-
icon="times"
233-
onClick={() => {
234-
onChange({ ...query, metric: '' });
235-
}}
236-
title="Click to remove metric"
237-
className={styles.metricTag}
238-
/>
239-
)}
240-
{metricEncyclopediaModalOpen && (
241-
<MetricEncyclopediaModal
242-
datasource={datasource}
243-
isOpen={metricEncyclopediaModalOpen}
244-
onClose={() => setMetricEncyclopediaModalOpen(false)}
245-
query={query}
246-
onChange={onChange}
247-
/>
248-
)}
249-
</>
250-
) : (
251-
<MetricSelect
252-
query={query}
253-
onChange={onChange}
254-
onGetMetrics={onGetMetrics}
255-
datasource={datasource}
256-
labelsFilters={query.labels}
257-
metricLookupDisabled={datasource.lookupsDisabled}
258-
/>
259-
)}
211+
<MetricSelect
212+
query={query}
213+
onChange={onChange}
214+
onGetMetrics={onGetMetrics}
215+
datasource={datasource}
216+
labelsFilters={query.labels}
217+
metricLookupDisabled={datasource.lookupsDisabled}
218+
/>
260219
<LabelFilters
261220
debounceDuration={datasource.getDebounceTimeInMilliseconds()}
262221
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
@@ -365,15 +324,3 @@ async function getMetrics(
365324
}
366325

367326
PromQueryBuilder.displayName = 'PromQueryBuilder';
368-
369-
const getStyles = (theme: GrafanaTheme2) => {
370-
return {
371-
button: css`
372-
height: auto;
373-
`,
374-
metricTag: css`
375-
margin: '10px 0 10px 0',
376-
backgroundColor: '#3D71D9',
377-
`,
378-
};
379-
};

0 commit comments

Comments
 (0)