Prometheus: Feedback link for metric encyclopedia plus performance im… (#64773)

Prometheus: Feedback link for metric encyclopedia plus performance improvements (#64576)

* add filtered results

* add feedback link

* correctly debounce fuzzy search thank you leon

* reduce calls to filterMetrics()in letter search

* clean up the alphabet type

* bug fix, filter type should be subtractive

* each filter is don;t explicitly, subtracting from the results of each

* Revert "each filter is don;t explicitly, subtracting from the results of each"

This reverts commit 5648788451.

* each filter is filtered explicitly, one after the other, subtracting from the results of each

(cherry picked from commit 2e4b134743)
This commit is contained in:
Brendan O'Handley
2023-03-21 11:56:49 -04:00
committed by GitHub
parent 4b6a015256
commit 2c6ad8867f
2 changed files with 210 additions and 144 deletions
@@ -0,0 +1,40 @@
import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Stack } from '@grafana/experimental';
import { Icon, useStyles2 } from '@grafana/ui';
export interface Props {
feedbackUrl?: string;
}
export function FeedbackLink({ feedbackUrl }: Props) {
const styles = useStyles2(getStyles);
return (
<Stack gap={1}>
<a
href={feedbackUrl}
className={styles.link}
title="The Metric Encyclopedia is new, please let us know how we can improve it"
target="_blank"
rel="noreferrer noopener"
>
<Icon name="comment-alt-message" /> Give feedback
</a>
</Stack>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
link: css({
color: theme.colors.text.secondary,
fontSize: theme.typography.bodySmall.fontSize,
':hover': {
color: theme.colors.text.link,
},
}),
};
}
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import uFuzzy from '@leeoniya/ufuzzy';
import debounce from 'debounce-promise';
import { debounce as debounceLodash } from 'lodash';
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import React, { useCallback, useEffect, useMemo, useState } from 'react';
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
import { reportInteraction } from '@grafana/runtime';
@@ -27,6 +27,8 @@ import { promQueryModeller } from '../PromQueryModeller';
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
import { PromVisualQuery } from '../types';
import { FeedbackLink } from './FeedbackLink';
type Props = {
datasource: PrometheusDatasource;
isOpen: boolean;
@@ -81,9 +83,22 @@ export const placeholders = {
export const DEFAULT_RESULTS_PER_PAGE = 10;
export const MetricEncyclopediaModal = (props: Props) => {
const uf = UseUfuzzy();
const uf = new uFuzzy({
intraMode: 1,
intraIns: 1,
intraSub: 1,
intraTrn: 1,
intraDel: 1,
});
function fuzzySearch(haystack: string[], query: string, setter: React.Dispatch<React.SetStateAction<number[]>>) {
const idxs = uf.filter(haystack, query);
idxs && setter(idxs);
}
const debouncedFuzzySearch = debounceLodash(fuzzySearch, 300);
export const MetricEncyclopediaModal = (props: Props) => {
const { datasource, isOpen, onClose, onChange, query } = props;
const [variables, setVariables] = useState<Array<SelectableValue<string>>>([]);
@@ -93,7 +108,7 @@ export const MetricEncyclopediaModal = (props: Props) => {
// metric list
const [metrics, setMetrics] = useState<MetricsData>([]);
const [hasMetadata, setHasMetadata] = useState<boolean>(true);
const [haystack, setHaystack] = useState<string[]>([]);
const [metaHaystack, setMetaHaystack] = useState<string[]>([]);
const [nameHaystack, setNameHaystack] = useState<string[]>([]);
const [openTabs, setOpenTabs] = useState<string[]>([]);
@@ -104,12 +119,14 @@ export const MetricEncyclopediaModal = (props: Props) => {
// filters
const [fuzzySearchQuery, setFuzzySearchQuery] = useState<string>('');
const [fuzzyMetaSearchResults, setFuzzyMetaSearchResults] = useState<number[]>([]);
const [fuzzyNameSearchResults, setNameFuzzySearchResults] = useState<number[]>([]);
const [fuzzyNameSearchResults, setFuzzyNameSearchResults] = useState<number[]>([]);
const [fullMetaSearch, setFullMetaSearch] = useState<boolean>(false);
const [excludeNullMetadata, setExcludeNullMetadata] = useState<boolean>(false);
const [selectedTypes, setSelectedTypes] = useState<Array<SelectableValue<string>>>([]);
const [letterSearch, setLetterSearch] = useState<string | null>(null);
const [totalMetricCount, setTotalMetricCount] = useState<number>(0);
const [filteredMetricCount, setFilteredMetricCount] = useState<number>();
// backend search metric names by text
const [useBackend, setUseBackend] = useState<boolean>(false);
@@ -159,7 +176,7 @@ export const MetricEncyclopediaModal = (props: Props) => {
// setting this by the backend if useBackend is true
setMetrics(metricsData);
setHaystack(haystackData);
setMetaHaystack(haystackData);
setNameHaystack(haystackNameData);
setVariables(
@@ -171,6 +188,8 @@ export const MetricEncyclopediaModal = (props: Props) => {
})
);
setTotalMetricCount(metricsData.length);
setFilteredMetricCount(metricsData.length);
setIsLoading(false);
}, [query, datasource]);
@@ -211,76 +230,45 @@ export const MetricEncyclopediaModal = (props: Props) => {
return selectedTypes.length > 0;
}
function fuzzySearch(query: string) {
// search either the names or all metadata
// fuzzy search go!
if (fullMetaSearch) {
// considered simply filtering indexes with reduce and includes
// Performance comparison with 13,000 metrics searching metadata
// Fuzzy 6326ms
// Reduce & Includes 5541ms
const metaIdxs = uf.filter(haystack, query.toLowerCase());
setFuzzyMetaSearchResults(metaIdxs);
} else {
const nameIdxs = uf.filter(nameHaystack, query.toLowerCase());
setNameFuzzySearchResults(nameIdxs);
}
}
const debouncedFuzzySearch = debounceLodash((query: string) => {
fuzzySearch(query);
}, 300);
/**
* Filter
*
* @param metrics
* @param skipLetterSearch
* @param skipLetterSearch used to show the alphabet letters as clickable before filtering out letters (needs to be refactored)
* @returns
*/
function filterMetrics(metrics: MetricsData, skipLetterSearch?: boolean): MetricsData {
let filteredMetrics: MetricsData = metrics;
if (fuzzySearchQuery || excludeNullMetadata || (letterSearch && !skipLetterSearch) || selectedTypes.length > 0) {
if (fuzzySearchQuery) {
filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
let keepMetric = false;
// search by text
if (fuzzySearchQuery) {
if (useBackend) {
// skip for backend!
keepMetric = true;
} else if (fullMetaSearch) {
keepMetric = fuzzyMetaSearchResults.includes(idx);
} else {
keepMetric = fuzzyNameSearchResults.includes(idx);
}
if (useBackend) {
// skip for backend!
return true;
} else if (fullMetaSearch) {
return fuzzyMetaSearchResults.includes(idx);
} else {
return fuzzyNameSearchResults.includes(idx);
}
});
}
// user clicks the alphabet search
// backend and frontend
if (letterSearch && !skipLetterSearch) {
const letters: string[] = [letterSearch, letterSearch.toLowerCase()];
keepMetric = letters.includes(m.value[0]);
}
if (letterSearch && !skipLetterSearch) {
filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
const letters: string[] = [letterSearch, letterSearch.toLowerCase()];
return letters.includes(m.value[0]);
});
}
// select by type, counter, gauge, etc
// skip for backend because no metadata is returned
if (selectedTypes.length > 0 && !useBackend) {
// return the metric that matches the type
// return the metric if it has no type AND we are NOT excluding metrics without metadata
if (selectedTypes.length > 0 && !useBackend) {
filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
// Matches type
const matchesSelectedType = selectedTypes.some((t) => t.value === m.type);
// Matches type
const matchesSelectedType = selectedTypes.some((t) => t.value === m.type);
// missing type
const hasNoType = !m.type;
// missing type
const hasNoType = !m.type;
return matchesSelectedType || (hasNoType && !excludeNullMetadata);
}
return keepMetric;
return matchesSelectedType || (hasNoType && !excludeNullMetadata);
});
}
@@ -293,6 +281,10 @@ export const MetricEncyclopediaModal = (props: Props) => {
function displayedMetrics(metrics: MetricsData) {
const filteredSorted: MetricsData = filterMetrics(metrics).sort(alphabetically(true, hasMetaDataFilters()));
if (filteredMetricCount !== filteredSorted.length && filteredSorted.length !== 0) {
setFilteredMetricCount(filteredSorted.length);
}
const displayedMetrics: MetricsData = sliceMetrics(filteredSorted, pageNum, resultsPerPage);
return displayedMetrics;
@@ -303,6 +295,7 @@ export const MetricEncyclopediaModal = (props: Props) => {
const debouncedBackendSearch = useMemo(
() =>
debounce(async (metricText: string) => {
setIsLoading(true);
const queryString = regexifyLabelValuesQueryString(metricText);
const labelsParams = query.labels.map((label) => {
@@ -324,11 +317,86 @@ export const MetricEncyclopediaModal = (props: Props) => {
});
setMetrics(metrics);
setFilteredMetricCount(metrics.length);
setIsLoading(false);
}, 300),
[datasource, query.labels]
);
function letterSearchComponent() {
const alphabetCheck: { [char: string]: number } = {
A: 0,
B: 0,
C: 0,
D: 0,
E: 0,
F: 0,
G: 0,
H: 0,
I: 0,
J: 0,
K: 0,
L: 0,
M: 0,
N: 0,
O: 0,
P: 0,
Q: 0,
R: 0,
S: 0,
T: 0,
U: 0,
V: 0,
W: 0,
X: 0,
Y: 0,
Z: 0,
};
filterMetrics(metrics, true).forEach((m: MetricData, idx) => {
const metricFirstLetter = m.value[0].toUpperCase();
if (alphabet.includes(metricFirstLetter) && !alphabetCheck[metricFirstLetter]) {
alphabetCheck[metricFirstLetter] += 1;
}
});
// return the alphabet components with the correct style and behavior
return Object.keys(alphabetCheck).map((letter: string) => {
// const active: boolean = .some((m: MetricData) => {
// return m.value[0] === letter || m.value[0] === letter?.toLowerCase();
// });
const active: boolean = alphabetCheck[letter] > 0;
// starts with letter search
// filter by starts with letter
// if same letter searched null out remove letter search
function updateLetterSearch() {
if (letterSearch === letter) {
setLetterSearch(null);
} else {
setLetterSearch(letter);
}
setPageNum(1);
}
// selected letter to filter by
const selectedClass: string = letterSearch === letter ? styles.selAlpha : '';
// these letters are represented in the list of metrics
const activeClass: string = active ? styles.active : styles.gray;
return (
<span
onClick={active ? updateLetterSearch : () => {}}
className={`${selectedClass} ${activeClass}`}
key={letter}
data-testid={'letter-' + letter}
>
{letter + ' '}
{/* {idx !== coll.length - 1 ? '|': ''} */}
</span>
);
});
}
return (
<Modal
data-testid={testIds.metricModal}
@@ -337,8 +405,9 @@ export const MetricEncyclopediaModal = (props: Props) => {
onDismiss={onClose}
aria-label="Metric Encyclopedia"
>
<FeedbackLink feedbackUrl="https://forms.gle/DEMAJHoAMpe3e54CA" />
<div className={styles.spacing}>
Browse {metrics.length} metric{metrics.length > 1 ? 's' : ''} by text, by type, alphabetically or select a
Browse {totalMetricCount} metric{totalMetricCount > 1 ? 's' : ''} by text, by type, alphabetically or select a
variable.
{isLoading && (
<div className={styles.inlineSpinner}>
@@ -364,11 +433,16 @@ export const MetricEncyclopediaModal = (props: Props) => {
// get all metrics data if a user erases everything in the input
updateMetricsMetadata();
} else if (useBackend) {
setIsLoading(true);
debouncedBackendSearch(value);
} else {
// do the search on the frontend
debouncedFuzzySearch(value);
// search either the names or all metadata
// fuzzy search go!
if (fullMetaSearch) {
debouncedFuzzySearch(metaHaystack, value, setFuzzyMetaSearchResults);
} else {
debouncedFuzzySearch(nameHaystack, value, setFuzzyNameSearchResults);
}
}
setPageNum(1);
@@ -462,69 +536,8 @@ export const MetricEncyclopediaModal = (props: Props) => {
}}
/>
</div>
<h5 className={`${styles.center} ${styles.topPadding}`}>Results</h5>
<div className={`${styles.center} ${styles.bottomPadding}`}>
{[
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
].map((letter, idx, coll) => {
const active: boolean = filterMetrics(metrics, true).some((m: MetricData) => {
return m.value[0] === letter || m.value[0] === letter?.toLowerCase();
});
// starts with letter search
// filter by starts with letter
// if same letter searched null out remove letter search
function updateLetterSearch() {
if (letterSearch === letter) {
setLetterSearch(null);
} else {
setLetterSearch(letter);
}
setPageNum(1);
}
// selected letter to filter by
const selectedClass: string = letterSearch === letter ? styles.selAlpha : '';
// these letters are represented in the list of metrics
const activeClass: string = active ? styles.active : styles.gray;
return (
<span
onClick={active ? updateLetterSearch : () => {}}
className={`${selectedClass} ${activeClass}`}
key={letter}
data-testid={'letter-' + letter}
>
{letter + ' '}
{/* {idx !== coll.length - 1 ? '|': ''} */}
</span>
);
})}
</div>
<h5 className={`${styles.center} ${styles.topPadding}`}>{filteredMetricCount} Results</h5>
<div className={`${styles.center} ${styles.bottomPadding}`}>{letterSearchComponent()}</div>
{metrics &&
displayedMetrics(metrics).map((metric: MetricData, idx) => {
return (
@@ -656,22 +669,6 @@ function alphabetically(ascending: boolean, metadataFilters: boolean) {
};
}
function UseUfuzzy(): uFuzzy {
const ref = useRef<uFuzzy>();
if (!ref.current) {
ref.current = new uFuzzy({
intraMode: 1,
intraIns: 1,
intraSub: 1,
intraTrn: 1,
intraDel: 1,
});
}
return ref.current;
}
const getStyles = (theme: GrafanaTheme2) => {
return {
cardsContainer: css`
@@ -733,3 +730,32 @@ export const testIds = {
resultsPerPage: 'results-per-page',
setUseBackend: 'set-use-backend',
};
const alphabet = [
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];