mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Metrics explorer usability test improvements (#69528)
* remove infer type functionality because usability tests confirmed it was confusing/not helpful * persist button option to open modal when typing in metric select * update copy desc for setting that includes type and description in search * when filtering by type, only return metrics with defined type * give focused metric row more contrast, consistent with metric select focused option * allow selection of metrics with unknown types and undefined types * add highlighting to backend search * augment counters created from summaries with (summary) * remove type from search input setting and only search by name and description * fix test to reflect that type has been removed from the metadata input search as duplicated by the filter * add button to select metric, change wording, make table hover row consistent with grafana table * add tooltip icon with docs link for metric types that are augmented, histogram and summary * remove files slated for future refactoring * style changes based on catherine's review * remove border from settings btn, select btn increase to md, change col size in table, fix responsive inputs ui for sm screens
This commit is contained in:
committed by
GitHub
parent
cae3b4c6e6
commit
ba97c492f9
@@ -49,6 +49,14 @@ export function MetricSelect({
|
|||||||
initialMetrics?: string[];
|
initialMetrics?: string[];
|
||||||
}>({});
|
}>({});
|
||||||
|
|
||||||
|
const metricsModalOption: SelectableValue[] = [
|
||||||
|
{
|
||||||
|
value: 'BrowseMetrics',
|
||||||
|
label: 'Metrics explorer',
|
||||||
|
description: 'Browse and filter metrics and metadata with a fuzzy search',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const customFilterOption = useCallback((option: SelectableValue<any>, searchQuery: string) => {
|
const customFilterOption = useCallback((option: SelectableValue<any>, searchQuery: string) => {
|
||||||
const label = option.label ?? option.value;
|
const label = option.label ?? option.value;
|
||||||
if (!label) {
|
if (!label) {
|
||||||
@@ -61,7 +69,16 @@ export function MetricSelect({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const searchWords = searchQuery.split(splitSeparator);
|
const searchWords = searchQuery.split(splitSeparator);
|
||||||
return searchWords.reduce((acc, cur) => acc && label.toLowerCase().includes(cur.toLowerCase()), true);
|
return searchWords.reduce((acc, cur) => {
|
||||||
|
const matcheSearch = label.toLowerCase().includes(cur.toLowerCase());
|
||||||
|
|
||||||
|
let browseOption = false;
|
||||||
|
if (prometheusMetricEncyclopedia) {
|
||||||
|
browseOption = label === 'Metrics explorer';
|
||||||
|
}
|
||||||
|
|
||||||
|
return acc && (matcheSearch || browseOption);
|
||||||
|
}, true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const formatOptionLabel = useCallback(
|
const formatOptionLabel = useCallback(
|
||||||
@@ -70,7 +87,8 @@ export function MetricSelect({
|
|||||||
if (option['__isNew__']) {
|
if (option['__isNew__']) {
|
||||||
return option.label;
|
return option.label;
|
||||||
}
|
}
|
||||||
|
// only matches on input, does not match on regex
|
||||||
|
// look into matching for regex input
|
||||||
return (
|
return (
|
||||||
<Highlighter
|
<Highlighter
|
||||||
searchWords={meta.inputValue.split(splitSeparator)}
|
searchWords={meta.inputValue.split(splitSeparator)}
|
||||||
@@ -104,12 +122,19 @@ export function MetricSelect({
|
|||||||
if (results.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
if (results.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
|
||||||
results.splice(0, results.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
results.splice(0, results.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||||
}
|
}
|
||||||
return results.map((result) => {
|
|
||||||
|
const resultsOptions = results.map((result) => {
|
||||||
return {
|
return {
|
||||||
label: result.text,
|
label: result.text,
|
||||||
value: result.text,
|
value: result.text,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (prometheusMetricEncyclopedia) {
|
||||||
|
return [...metricsModalOption, ...resultsOptions];
|
||||||
|
} else {
|
||||||
|
return resultsOptions;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -201,18 +226,11 @@ export function MetricSelect({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prometheusMetricEncyclopedia) {
|
if (prometheusMetricEncyclopedia) {
|
||||||
// pass the initial metrics, possibly filtered by labels into the Metrics Modal
|
|
||||||
const metricsModalOption: SelectableValue[] = [
|
|
||||||
{
|
|
||||||
value: 'BrowseMetrics',
|
|
||||||
label: 'Metrics explorer',
|
|
||||||
description: 'Browse and filter metrics and metadata with a fuzzy search',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
// pass the initial metrics into the Metrics Modal
|
|
||||||
setState({
|
setState({
|
||||||
|
// add the modal butoon option to the options
|
||||||
metrics: [...metricsModalOption, ...metrics],
|
metrics: [...metricsModalOption, ...metrics],
|
||||||
isLoading: undefined,
|
isLoading: undefined,
|
||||||
|
// pass the initial metrics into the Metrics Modal
|
||||||
initialMetrics: initialMetrics,
|
initialMetrics: initialMetrics,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -14,18 +14,11 @@ type AdditionalSettingsProps = {
|
|||||||
onChangeIncludeNullMetadata: () => void;
|
onChangeIncludeNullMetadata: () => void;
|
||||||
onChangeDisableTextWrap: () => void;
|
onChangeDisableTextWrap: () => void;
|
||||||
onChangeUseBackend: () => void;
|
onChangeUseBackend: () => void;
|
||||||
onChangeInferType: () => void;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export function AdditionalSettings(props: AdditionalSettingsProps) {
|
export function AdditionalSettings(props: AdditionalSettingsProps) {
|
||||||
const {
|
const { state, onChangeFullMetaSearch, onChangeIncludeNullMetadata, onChangeDisableTextWrap, onChangeUseBackend } =
|
||||||
state,
|
props;
|
||||||
onChangeFullMetaSearch,
|
|
||||||
onChangeIncludeNullMetadata,
|
|
||||||
onChangeDisableTextWrap,
|
|
||||||
onChangeUseBackend,
|
|
||||||
onChangeInferType,
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const theme = useTheme2();
|
const theme = useTheme2();
|
||||||
const styles = getStyles(theme);
|
const styles = getStyles(theme);
|
||||||
@@ -63,18 +56,6 @@ export function AdditionalSettings(props: AdditionalSettingsProps) {
|
|||||||
<Icon name="info-circle" size="xs" className={styles.settingsIcon} />
|
<Icon name="info-circle" size="xs" className={styles.settingsIcon} />
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.selectItem}>
|
|
||||||
<Switch data-testid={testIds.inferType} value={state.inferType} onChange={() => onChangeInferType()} />
|
|
||||||
<div className={styles.selectItemLabel}>{placeholders.inferType} </div>
|
|
||||||
<Tooltip
|
|
||||||
content={
|
|
||||||
'For example, metrics ending in _sum, _count, will be given an inferred type of counter. Metrics ending in _bucket with be given a type of histogram.'
|
|
||||||
}
|
|
||||||
placement="bottom-end"
|
|
||||||
>
|
|
||||||
<Icon name="info-circle" size="xs" className={styles.settingsIcon} />
|
|
||||||
</Tooltip>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ describe('MetricsModal', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('searches by all metric metadata with a fuzzy search', async () => {
|
it('searches by name and description with a fuzzy search when setting is turned on', async () => {
|
||||||
// search for a_bucket by metadata type counter but only type countt
|
// search for a_bucket by metadata type counter but only type countt
|
||||||
setup(defaultQuery, listOfMetrics);
|
setup(defaultQuery, listOfMetrics);
|
||||||
let metricABucket: HTMLElement | null;
|
let metricABucket: HTMLElement | null;
|
||||||
@@ -179,7 +179,7 @@ describe('MetricsModal', () => {
|
|||||||
|
|
||||||
const searchMetric = screen.getByTestId(testIds.searchMetric);
|
const searchMetric = screen.getByTestId(testIds.searchMetric);
|
||||||
expect(searchMetric).toBeInTheDocument();
|
expect(searchMetric).toBeInTheDocument();
|
||||||
await userEvent.type(searchMetric, 'countt');
|
await userEvent.type(searchMetric, 'functions');
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
metricABucket = screen.getByText('a_bucket');
|
metricABucket = screen.getByText('a_bucket');
|
||||||
|
|||||||
@@ -69,7 +69,6 @@ const {
|
|||||||
setSelectedIdx,
|
setSelectedIdx,
|
||||||
setDisableTextWrap,
|
setDisableTextWrap,
|
||||||
showAdditionalSettings,
|
showAdditionalSettings,
|
||||||
setInferType,
|
|
||||||
} = stateSlice.actions;
|
} = stateSlice.actions;
|
||||||
|
|
||||||
export const MetricsModal = (props: MetricsModalProps) => {
|
export const MetricsModal = (props: MetricsModalProps) => {
|
||||||
@@ -83,31 +82,26 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
/**
|
/**
|
||||||
* loads metrics and metadata on opening modal and switching off useBackend
|
* loads metrics and metadata on opening modal and switching off useBackend
|
||||||
*/
|
*/
|
||||||
const updateMetricsMetadata = useCallback(
|
const updateMetricsMetadata = useCallback(async () => {
|
||||||
async (inferType: boolean) => {
|
// *** Loading Gif
|
||||||
// *** Loading Gif
|
dispatch(setIsLoading(true));
|
||||||
dispatch(setIsLoading(true));
|
|
||||||
|
|
||||||
const data: MetricsModalMetadata = await setMetrics(datasource, query, inferType, initialMetrics);
|
const data: MetricsModalMetadata = await setMetrics(datasource, query, initialMetrics);
|
||||||
|
dispatch(
|
||||||
dispatch(
|
buildMetrics({
|
||||||
buildMetrics({
|
isLoading: false,
|
||||||
isLoading: false,
|
hasMetadata: data.hasMetadata,
|
||||||
hasMetadata: data.hasMetadata,
|
metrics: data.metrics,
|
||||||
metrics: data.metrics,
|
metaHaystackDictionary: data.metaHaystackDictionary,
|
||||||
metaHaystackDictionary: data.metaHaystackDictionary,
|
nameHaystackDictionary: data.nameHaystackDictionary,
|
||||||
nameHaystackDictionary: data.nameHaystackDictionary,
|
totalMetricCount: data.metrics.length,
|
||||||
totalMetricCount: data.metrics.length,
|
filteredMetricCount: data.metrics.length,
|
||||||
filteredMetricCount: data.metrics.length,
|
})
|
||||||
})
|
);
|
||||||
);
|
}, [query, datasource, initialMetrics]);
|
||||||
},
|
|
||||||
[query, datasource, initialMetrics]
|
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
updateMetricsMetadata(state.inferType);
|
updateMetricsMetadata();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, [updateMetricsMetadata]);
|
}, [updateMetricsMetadata]);
|
||||||
|
|
||||||
const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => {
|
const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => {
|
||||||
@@ -123,10 +117,10 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
*/
|
*/
|
||||||
const debouncedBackendSearch = useMemo(
|
const debouncedBackendSearch = useMemo(
|
||||||
() =>
|
() =>
|
||||||
debounce(async (metricText: string, inferType: boolean) => {
|
debounce(async (metricText: string) => {
|
||||||
dispatch(setIsLoading(true));
|
dispatch(setIsLoading(true));
|
||||||
|
|
||||||
const metrics = await getBackendSearchMetrics(metricText, query.labels, datasource, inferType);
|
const metrics = await getBackendSearchMetrics(metricText, query.labels, datasource);
|
||||||
|
|
||||||
dispatch(
|
dispatch(
|
||||||
filterMetricsBackend({
|
filterMetricsBackend({
|
||||||
@@ -150,9 +144,9 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
function searchCallback(query: string, fullMetaSearchVal: boolean) {
|
function searchCallback(query: string, fullMetaSearchVal: boolean) {
|
||||||
if (state.useBackend && query === '') {
|
if (state.useBackend && query === '') {
|
||||||
// get all metrics data if a user erases everything in the input
|
// get all metrics data if a user erases everything in the input
|
||||||
updateMetricsMetadata(state.inferType);
|
updateMetricsMetadata();
|
||||||
} else if (state.useBackend) {
|
} else if (state.useBackend) {
|
||||||
debouncedBackendSearch(query, state.inferType);
|
debouncedBackendSearch(query);
|
||||||
} else {
|
} else {
|
||||||
// search either the names or all metadata
|
// search either the names or all metadata
|
||||||
// fuzzy search go!
|
// fuzzy search go!
|
||||||
@@ -200,31 +194,17 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
onChange({ ...query, disableTextWrap: !state.disableTextWrap });
|
onChange({ ...query, disableTextWrap: !state.disableTextWrap });
|
||||||
tracking('grafana_prom_metric_encycopedia_disable_text_wrap_interaction', state, '');
|
tracking('grafana_prom_metric_encycopedia_disable_text_wrap_interaction', state, '');
|
||||||
}}
|
}}
|
||||||
onChangeInferType={() => {
|
|
||||||
const inferType = !state.inferType;
|
|
||||||
dispatch(setInferType(inferType));
|
|
||||||
// update the type
|
|
||||||
if (state.useBackend) {
|
|
||||||
// if there is no query yet, it will infer the type on the api call
|
|
||||||
if (state.fuzzySearchQuery !== '') {
|
|
||||||
debouncedBackendSearch(state.fuzzySearchQuery, inferType);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// updates the metadata with the inferred type
|
|
||||||
updateMetricsMetadata(inferType);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onChangeUseBackend={() => {
|
onChangeUseBackend={() => {
|
||||||
const newVal = !state.useBackend;
|
const newVal = !state.useBackend;
|
||||||
dispatch(setUseBackend(newVal));
|
dispatch(setUseBackend(newVal));
|
||||||
onChange({ ...query, useBackend: newVal });
|
onChange({ ...query, useBackend: newVal });
|
||||||
if (newVal === false) {
|
if (newVal === false) {
|
||||||
// rebuild the metrics metadata if we turn off useBackend
|
// rebuild the metrics metadata if we turn off useBackend
|
||||||
updateMetricsMetadata(state.inferType);
|
updateMetricsMetadata();
|
||||||
} else {
|
} else {
|
||||||
// check if there is text in the browse search and update
|
// check if there is text in the browse search and update
|
||||||
if (state.fuzzySearchQuery !== '') {
|
if (state.fuzzySearchQuery !== '') {
|
||||||
debouncedBackendSearch(state.fuzzySearchQuery, state.inferType);
|
debouncedBackendSearch(state.fuzzySearchQuery);
|
||||||
}
|
}
|
||||||
// otherwise wait for user typing
|
// otherwise wait for user typing
|
||||||
}
|
}
|
||||||
@@ -259,9 +239,6 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<Spinner className={`${styles.loadingSpinner} ${state.isLoading ? styles.visible : ''}`} />
|
|
||||||
</div>
|
|
||||||
{state.hasMetadata && (
|
{state.hasMetadata && (
|
||||||
<div className={styles.inputItem}>
|
<div className={styles.inputItem}>
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
@@ -274,6 +251,9 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
<div>
|
||||||
|
<Spinner className={`${styles.loadingSpinner} ${state.isLoading ? styles.visible : ''}`} />
|
||||||
|
</div>
|
||||||
<div className={styles.inputItem}>
|
<div className={styles.inputItem}>
|
||||||
<Toggletip
|
<Toggletip
|
||||||
aria-label="Additional settings"
|
aria-label="Additional settings"
|
||||||
@@ -287,10 +267,15 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
|||||||
size="md"
|
size="md"
|
||||||
onClick={() => dispatch(showAdditionalSettings())}
|
onClick={() => dispatch(showAdditionalSettings())}
|
||||||
data-testid={testIds.showAdditionalSettings}
|
data-testid={testIds.showAdditionalSettings}
|
||||||
|
className={styles.noBorder}
|
||||||
>
|
>
|
||||||
Additional Settings
|
Additional Settings
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="secondary" icon={state.showAdditionalSettings ? 'angle-up' : 'angle-down'} />
|
<Button
|
||||||
|
className={styles.noBorder}
|
||||||
|
variant="secondary"
|
||||||
|
icon={state.showAdditionalSettings ? 'angle-up' : 'angle-down'}
|
||||||
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</Toggletip>
|
</Toggletip>
|
||||||
</div>
|
</div>
|
||||||
@@ -368,5 +353,4 @@ export const testIds = {
|
|||||||
resultsPerPage: 'results-per-page',
|
resultsPerPage: 'results-per-page',
|
||||||
setUseBackend: 'set-use-backend',
|
setUseBackend: 'set-use-backend',
|
||||||
showAdditionalSettings: 'show-additional-settings',
|
showAdditionalSettings: 'show-additional-settings',
|
||||||
inferType: 'set-infer-type',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import React, { ReactElement, useEffect, useRef } from 'react';
|
|||||||
import Highlighter from 'react-highlight-words';
|
import Highlighter from 'react-highlight-words';
|
||||||
|
|
||||||
import { GrafanaTheme2 } from '@grafana/data';
|
import { GrafanaTheme2 } from '@grafana/data';
|
||||||
import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { docsTip } from '../../../configuration/ConfigEditor';
|
||||||
import { PromVisualQuery } from '../../types';
|
import { PromVisualQuery } from '../../types';
|
||||||
|
|
||||||
import { tracking } from './state/helpers';
|
import { tracking } from './state/helpers';
|
||||||
@@ -51,15 +52,7 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
if (state.fullMetaSearch && metric) {
|
if (state.fullMetaSearch && metric) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td>
|
<td>{displayType(metric.type ?? '')}</td>
|
||||||
<Highlighter
|
|
||||||
textToHighlight={metric.type ?? ''}
|
|
||||||
searchWords={state.metaHaystackMatches}
|
|
||||||
autoEscape
|
|
||||||
highlightClassName={`${styles.matchHighLight} ${metric.inferred ? styles.italicized : ''}`}
|
|
||||||
/>{' '}
|
|
||||||
{inferredType(metric.inferred ?? false)}
|
|
||||||
</td>
|
|
||||||
<td>
|
<td>
|
||||||
<Highlighter
|
<Highlighter
|
||||||
textToHighlight={metric.description ?? ''}
|
textToHighlight={metric.description ?? ''}
|
||||||
@@ -73,25 +66,49 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
} else {
|
} else {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td className={metric.inferred ? styles.italicized : ''}>
|
<td>{displayType(metric.type ?? '')}</td>
|
||||||
{metric.type ?? ''} {inferredType(metric.inferred ?? false)}
|
|
||||||
</td>
|
|
||||||
<td>{metric.description ?? ''}</td>
|
<td>{metric.description ?? ''}</td>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function inferredType(inferred: boolean): JSX.Element | undefined {
|
function addHelpIcon(fullType: string, descriptiveType: string, link: string) {
|
||||||
if (inferred) {
|
return (
|
||||||
return (
|
<>
|
||||||
<Tooltip content={'This metric type has been inferred'} placement="bottom-end">
|
{fullType}
|
||||||
<Icon name="info-circle" size="xs" />
|
<span className={styles.tooltipSpace}>
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
);
|
content={
|
||||||
} else {
|
<>
|
||||||
return undefined;
|
When creating a {descriptiveType}, Prometheus exposes multiple series with the type counter.{' '}
|
||||||
|
{docsTip(link)}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
placement="bottom-start"
|
||||||
|
interactive={true}
|
||||||
|
>
|
||||||
|
<Icon name="info-circle" size="xs" />
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function displayType(type: string | null) {
|
||||||
|
if (!type) {
|
||||||
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (type.includes('(summary)')) {
|
||||||
|
return addHelpIcon(type, 'summary', 'https://prometheus.io/docs/concepts/metric_types/#summary');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type.includes('(histogram)')) {
|
||||||
|
return addHelpIcon(type, 'histogram', 'https://prometheus.io/docs/concepts/metric_types/#histogram');
|
||||||
|
}
|
||||||
|
|
||||||
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
function noMetricsMessages(): ReactElement {
|
function noMetricsMessages(): ReactElement {
|
||||||
@@ -105,7 +122,7 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
message = 'There are no metrics found. Try to expand your label filters.';
|
message = 'There are no metrics found. Try to expand your label filters.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.fuzzySearchQuery) {
|
if (state.fuzzySearchQuery || state.selectedTypes.length > 0) {
|
||||||
message = 'There are no metrics found. Try to expand your search and filters.';
|
message = 'There are no metrics found. Try to expand your search and filters.';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +133,21 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function textHighlight(state: MetricsModalState) {
|
||||||
|
if (state.useBackend) {
|
||||||
|
// highlight the input only for the backend search
|
||||||
|
// this highlight is equivalent to how the metric select highlights
|
||||||
|
// look into matching on regex input
|
||||||
|
return [state.fuzzySearchQuery];
|
||||||
|
} else if (state.fullMetaSearch) {
|
||||||
|
// highlight the matches in the ufuzzy metaHaystack
|
||||||
|
return state.metaHaystackMatches;
|
||||||
|
} else {
|
||||||
|
// highlight the ufuzzy name matches
|
||||||
|
return state.nameHaystackMatches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<table className={styles.table} ref={tableRef}>
|
<table className={styles.table} ref={tableRef}>
|
||||||
<thead className={styles.stickyHeader}>
|
<thead className={styles.stickyHeader}>
|
||||||
@@ -124,9 +156,10 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
{state.hasMetadata && (
|
{state.hasMetadata && (
|
||||||
<>
|
<>
|
||||||
<th className={`${styles.typeWidth} ${styles.tableHeaderPadding}`}>Type</th>
|
<th className={`${styles.typeWidth} ${styles.tableHeaderPadding}`}>Type</th>
|
||||||
<th className={styles.tableHeaderPadding}>Description</th>
|
<th className={`${styles.descriptionWidth} ${styles.tableHeaderPadding}`}>Description</th>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<th className={styles.selectButtonWidth}> </th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -137,8 +170,6 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
<tr
|
<tr
|
||||||
key={metric?.value ?? idx}
|
key={metric?.value ?? idx}
|
||||||
className={`${styles.row} ${isSelectedRow(idx) ? `${styles.selectedRow} selected-row` : ''}`}
|
className={`${styles.row} ${isSelectedRow(idx) ? `${styles.selectedRow} selected-row` : ''}`}
|
||||||
onClick={() => selectMetric(metric)}
|
|
||||||
tabIndex={0}
|
|
||||||
onFocus={() => onFocusRow(idx)}
|
onFocus={() => onFocusRow(idx)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.code === 'Enter' && e.currentTarget.classList.contains('selected-row')) {
|
if (e.code === 'Enter' && e.currentTarget.classList.contains('selected-row')) {
|
||||||
@@ -149,12 +180,22 @@ export function ResultsTable(props: ResultsTableProps) {
|
|||||||
<td className={styles.nameOverflow}>
|
<td className={styles.nameOverflow}>
|
||||||
<Highlighter
|
<Highlighter
|
||||||
textToHighlight={metric?.value ?? ''}
|
textToHighlight={metric?.value ?? ''}
|
||||||
searchWords={state.fullMetaSearch ? state.metaHaystackMatches : state.nameHaystackMatches}
|
searchWords={textHighlight(state)}
|
||||||
autoEscape
|
autoEscape
|
||||||
highlightClassName={styles.matchHighLight}
|
highlightClassName={styles.matchHighLight}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
{state.hasMetadata && metaRows(metric)}
|
{state.hasMetadata && metaRows(metric)}
|
||||||
|
<td>
|
||||||
|
<Button
|
||||||
|
size="md"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => selectMetric(metric)}
|
||||||
|
className={styles.centerButton}
|
||||||
|
>
|
||||||
|
Select
|
||||||
|
</Button>
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -186,7 +227,6 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
|
|||||||
`,
|
`,
|
||||||
row: css`
|
row: css`
|
||||||
label: row;
|
label: row;
|
||||||
cursor: pointer;
|
|
||||||
border-bottom: 1px solid ${theme.colors.border.weak}
|
border-bottom: 1px solid ${theme.colors.border.weak}
|
||||||
&:last-child {
|
&:last-child {
|
||||||
border-bottom: 0;
|
border-bottom: 0;
|
||||||
@@ -207,13 +247,19 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
|
|||||||
background-color: ${theme.components.textHighlight.background};
|
background-color: ${theme.components.textHighlight.background};
|
||||||
`,
|
`,
|
||||||
nameWidth: css`
|
nameWidth: css`
|
||||||
${disableTextWrap ? '' : 'width: 40%;'}
|
${disableTextWrap ? '' : 'width: 37.5%;'}
|
||||||
`,
|
`,
|
||||||
nameOverflow: css`
|
nameOverflow: css`
|
||||||
${disableTextWrap ? '' : 'overflow-wrap: anywhere;'}
|
${disableTextWrap ? '' : 'overflow-wrap: anywhere;'}
|
||||||
`,
|
`,
|
||||||
typeWidth: css`
|
typeWidth: css`
|
||||||
${disableTextWrap ? '' : 'width: 16%;'}
|
${disableTextWrap ? '' : 'width: 15%;'}
|
||||||
|
`,
|
||||||
|
descriptionWidth: css`
|
||||||
|
${disableTextWrap ? '' : 'width: 35%;'}
|
||||||
|
`,
|
||||||
|
selectButtonWidth: css`
|
||||||
|
${disableTextWrap ? '' : 'width: 12.5%;'}
|
||||||
`,
|
`,
|
||||||
stickyHeader: css`
|
stickyHeader: css`
|
||||||
position: sticky;
|
position: sticky;
|
||||||
@@ -224,8 +270,13 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
`,
|
`,
|
||||||
italicized: css`
|
tooltipSpace: css`
|
||||||
font-style: italic;
|
margin-left: 4px;
|
||||||
|
`,
|
||||||
|
centerButton: css`
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
border: none;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ const { setFilteredMetricCount } = stateSlice.actions;
|
|||||||
export async function setMetrics(
|
export async function setMetrics(
|
||||||
datasource: PrometheusDatasource,
|
datasource: PrometheusDatasource,
|
||||||
query: PromVisualQuery,
|
query: PromVisualQuery,
|
||||||
inferType: boolean,
|
|
||||||
initialMetrics?: string[]
|
initialMetrics?: string[]
|
||||||
): Promise<MetricsModalMetadata> {
|
): Promise<MetricsModalMetadata> {
|
||||||
// metadata is set in the metric select now
|
// metadata is set in the metric select now
|
||||||
@@ -34,9 +33,9 @@ export async function setMetrics(
|
|||||||
let metricsData: MetricsData | undefined;
|
let metricsData: MetricsData | undefined;
|
||||||
|
|
||||||
metricsData = initialMetrics?.map((m: string) => {
|
metricsData = initialMetrics?.map((m: string) => {
|
||||||
const metricData = buildMetricData(m, inferType, datasource);
|
const metricData = buildMetricData(m, datasource);
|
||||||
|
|
||||||
const metaDataString = `${m}¦${metricData.type}¦${metricData.description}`;
|
const metaDataString = `${m}¦${metricData.description}`;
|
||||||
|
|
||||||
nameHaystackDictionaryData[m] = metricData;
|
nameHaystackDictionaryData[m] = metricData;
|
||||||
metaHaystackDictionaryData[metaDataString] = metricData;
|
metaHaystackDictionaryData[metaDataString] = metricData;
|
||||||
@@ -56,34 +55,27 @@ export async function setMetrics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Builds the metric data object with type, description and inferred flag
|
* Builds the metric data object with type and description
|
||||||
*
|
*
|
||||||
* @param metric The metric name
|
* @param metric The metric name
|
||||||
* @param inferType state attribute that the infer type setting is on or off
|
|
||||||
* @param datasource The Prometheus datasource for mapping metradata to the metric name
|
* @param datasource The Prometheus datasource for mapping metradata to the metric name
|
||||||
* @returns A MetricData object.
|
* @returns A MetricData object.
|
||||||
*/
|
*/
|
||||||
function buildMetricData(metric: string, inferType: boolean, datasource: PrometheusDatasource): MetricData {
|
function buildMetricData(metric: string, datasource: PrometheusDatasource): MetricData {
|
||||||
let type = getMetadataType(metric, datasource.languageProvider.metricsMetadata!);
|
let type = getMetadataType(metric, datasource.languageProvider.metricsMetadata!);
|
||||||
let inferredType;
|
|
||||||
if (!type && inferType) {
|
|
||||||
type = metricTypeHints(metric);
|
|
||||||
|
|
||||||
if (type) {
|
|
||||||
inferredType = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const description = getMetadataHelp(metric, datasource.languageProvider.metricsMetadata!);
|
const description = getMetadataHelp(metric, datasource.languageProvider.metricsMetadata!);
|
||||||
|
|
||||||
if (description?.toLowerCase().includes('histogram') && type !== 'histogram') {
|
['histogram', 'summary'].forEach((t) => {
|
||||||
type += ' (histogram)';
|
if (description?.toLowerCase().includes(t) && type !== t) {
|
||||||
}
|
type += ` (${t})`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const metricData: MetricData = {
|
const metricData: MetricData = {
|
||||||
value: metric,
|
value: metric,
|
||||||
type: type,
|
type: type,
|
||||||
description: description,
|
description: description,
|
||||||
inferred: inferredType,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return metricData;
|
return metricData;
|
||||||
@@ -123,22 +115,21 @@ export function filterMetrics(state: MetricsModalState): MetricsData {
|
|||||||
if (m.type && t.value) {
|
if (m.type && t.value) {
|
||||||
return m.type.includes(t.value);
|
return m.type.includes(t.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!m.type && t.value === 'no type') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
// missing type
|
// when a user filters for type, only return metrics with defined types
|
||||||
const hasNoType = !m.type;
|
return matchesSelectedType;
|
||||||
|
|
||||||
return matchesSelectedType || (hasNoType && state.includeNullMetadata);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!state.includeNullMetadata) {
|
if (!state.includeNullMetadata) {
|
||||||
filteredMetrics = filteredMetrics.filter((m: MetricData) => {
|
filteredMetrics = filteredMetrics.filter((m: MetricData) => {
|
||||||
if (state.inferType && m.inferred) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return m.type !== undefined && m.description !== undefined;
|
return m.type !== undefined && m.description !== undefined;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -188,8 +179,7 @@ export const calculateResultsPerPage = (results: number, defaultResults: number,
|
|||||||
export async function getBackendSearchMetrics(
|
export async function getBackendSearchMetrics(
|
||||||
metricText: string,
|
metricText: string,
|
||||||
labels: QueryBuilderLabelFilter[],
|
labels: QueryBuilderLabelFilter[],
|
||||||
datasource: PrometheusDatasource,
|
datasource: PrometheusDatasource
|
||||||
inferType: boolean
|
|
||||||
): Promise<Array<{ value: string }>> {
|
): Promise<Array<{ value: string }>> {
|
||||||
const queryString = regexifyLabelValuesQueryString(metricText);
|
const queryString = regexifyLabelValuesQueryString(metricText);
|
||||||
|
|
||||||
@@ -202,24 +192,10 @@ export async function getBackendSearchMetrics(
|
|||||||
const results = datasource.metricFindQuery(params);
|
const results = datasource.metricFindQuery(params);
|
||||||
|
|
||||||
return await results.then((results) => {
|
return await results.then((results) => {
|
||||||
return results.map((result) => buildMetricData(result.text, inferType, datasource));
|
return results.map((result) => buildMetricData(result.text, datasource));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function metricTypeHints(metric: string): string {
|
|
||||||
const histogramMetric = metric.match(/^\w+_bucket$|^\w+_bucket{.*}$/);
|
|
||||||
if (histogramMetric) {
|
|
||||||
return 'counter (histogram)';
|
|
||||||
}
|
|
||||||
|
|
||||||
const counterMatch = metric.match(/\b(\w+_(total|sum|count))\b/);
|
|
||||||
if (counterMatch) {
|
|
||||||
return 'counter';
|
|
||||||
}
|
|
||||||
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function tracking(event: string, state?: MetricsModalState | null, metric?: string, query?: PromVisualQuery) {
|
export function tracking(event: string, state?: MetricsModalState | null, metric?: string, query?: PromVisualQuery) {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
case 'grafana_prom_metric_encycopedia_tracking':
|
case 'grafana_prom_metric_encycopedia_tracking':
|
||||||
@@ -230,7 +206,6 @@ export function tracking(event: string, state?: MetricsModalState | null, metric
|
|||||||
fuzzySearchQuery: state?.fuzzySearchQuery,
|
fuzzySearchQuery: state?.fuzzySearchQuery,
|
||||||
fullMetaSearch: state?.fullMetaSearch,
|
fullMetaSearch: state?.fullMetaSearch,
|
||||||
selectedTypes: state?.selectedTypes,
|
selectedTypes: state?.selectedTypes,
|
||||||
inferType: state?.inferType,
|
|
||||||
});
|
});
|
||||||
case 'grafana_prom_metric_encycopedia_disable_text_wrap_interaction':
|
case 'grafana_prom_metric_encycopedia_disable_text_wrap_interaction':
|
||||||
reportInteraction(event, {
|
reportInteraction(event, {
|
||||||
@@ -263,13 +238,20 @@ export const promTypes: PromFilterOption[] = [
|
|||||||
description:
|
description:
|
||||||
'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.',
|
'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'unknown',
|
||||||
|
description: 'These metrics have been given the type unknown in the metadata.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'no type',
|
||||||
|
description: 'These metrics have no defined type in the metadata.',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const placeholders = {
|
export const placeholders = {
|
||||||
browse: 'Search metrics by name',
|
browse: 'Search metrics by name',
|
||||||
metadataSearchSwitch: 'Include search with type and description',
|
metadataSearchSwitch: 'Include description in search',
|
||||||
type: 'Filter by type',
|
type: 'Filter by type',
|
||||||
includeNullMetadata: 'Include results with no metadata',
|
includeNullMetadata: 'Include results with no metadata',
|
||||||
setUseBackend: 'Enable regex search',
|
setUseBackend: 'Enable regex search',
|
||||||
inferType: 'Infer metric type',
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -84,9 +84,6 @@ export const stateSlice = createSlice({
|
|||||||
showAdditionalSettings: (state) => {
|
showAdditionalSettings: (state) => {
|
||||||
state.showAdditionalSettings = !state.showAdditionalSettings;
|
state.showAdditionalSettings = !state.showAdditionalSettings;
|
||||||
},
|
},
|
||||||
setInferType: (state, action: PayloadAction<boolean>) => {
|
|
||||||
state.inferType = action.payload;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -117,7 +114,6 @@ export function initialState(query?: PromVisualQuery): MetricsModalState {
|
|||||||
disableTextWrap: query?.disableTextWrap ?? false,
|
disableTextWrap: query?.disableTextWrap ?? false,
|
||||||
selectedIdx: 0,
|
selectedIdx: 0,
|
||||||
showAdditionalSettings: false,
|
showAdditionalSettings: false,
|
||||||
inferType: true,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,8 +167,6 @@ export interface MetricsModalState {
|
|||||||
selectedIdx: number;
|
selectedIdx: number;
|
||||||
/** Display toggle switches for settings */
|
/** Display toggle switches for settings */
|
||||||
showAdditionalSettings: boolean;
|
showAdditionalSettings: boolean;
|
||||||
/** Check metric to match on substrings to infer prometheus type */
|
|
||||||
inferType: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -17,10 +17,14 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: ${theme.spacing(2)};
|
|
||||||
`,
|
`,
|
||||||
inputItemFirst: css`
|
inputItemFirst: css`
|
||||||
flex-basis: 40%;
|
flex-basis: 40%;
|
||||||
|
padding-right: 16px;
|
||||||
|
${theme.breakpoints.down('md')} {
|
||||||
|
padding-right: 0px;
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
inputItem: css`
|
inputItem: css`
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
@@ -80,6 +84,9 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
|
|||||||
settingsBtn: css`
|
settingsBtn: css`
|
||||||
float: right;
|
float: right;
|
||||||
`,
|
`,
|
||||||
|
noBorder: css`
|
||||||
|
border: none;
|
||||||
|
`,
|
||||||
resultsPerPageLabel: css`
|
resultsPerPageLabel: css`
|
||||||
color: ${theme.colors.text.secondary};
|
color: ${theme.colors.text.secondary};
|
||||||
opacity: 75%;
|
opacity: 75%;
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ export type MetricData = {
|
|||||||
value: string;
|
value: string;
|
||||||
type?: string | null;
|
type?: string | null;
|
||||||
description?: string;
|
description?: string;
|
||||||
inferred?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type PromFilterOption = {
|
export type PromFilterOption = {
|
||||||
|
|||||||
Reference in New Issue
Block a user