+
# Results per page
+
{
+ const value = +e.currentTarget.value;
-
);
@@ -399,4 +368,5 @@ export const testIds = {
resultsPerPage: 'results-per-page',
setUseBackend: 'set-use-backend',
showAdditionalSettings: 'show-additional-settings',
+ inferType: 'set-infer-type',
};
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/ResultsTable.tsx b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/ResultsTable.tsx
index 488db534cfa..c2cdbfd2187 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/ResultsTable.tsx
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/ResultsTable.tsx
@@ -1,13 +1,13 @@
import { css } from '@emotion/css';
-import React, { useEffect, useRef } from 'react';
+import React, { ReactElement, useEffect, useRef } from 'react';
import Highlighter from 'react-highlight-words';
import { GrafanaTheme2 } from '@grafana/data';
-import { reportInteraction } from '@grafana/runtime';
-import { useTheme2 } from '@grafana/ui';
+import { Icon, Tooltip, useTheme2 } from '@grafana/ui';
import { PromVisualQuery } from '../../types';
+import { tracking } from './state/helpers';
import { MetricsModalState } from './state/state';
import { MetricData, MetricsData } from './types';
@@ -19,10 +19,11 @@ type ResultsTableProps = {
state: MetricsModalState;
selectedIdx: number;
disableTextWrap: boolean;
+ onFocusRow: (idx: number) => void;
};
export function ResultsTable(props: ResultsTableProps) {
- const { metrics, onChange, onClose, query, state, selectedIdx, disableTextWrap } = props;
+ const { metrics, onChange, onClose, query, state, selectedIdx, disableTextWrap, onFocusRow } = props;
const theme = useTheme2();
const styles = getStyles(theme, disableTextWrap);
@@ -36,15 +37,7 @@ export function ResultsTable(props: ResultsTableProps) {
function selectMetric(metric: MetricData) {
if (metric.value) {
onChange({ ...query, metric: metric.value });
- reportInteraction('grafana_prom_metric_encycopedia_tracking', {
- metric: metric.value,
- hasMetadata: state.hasMetadata,
- totalMetricCount: state.totalMetricCount,
- fuzzySearchQuery: state.fuzzySearchQuery,
- fullMetaSearch: state.fullMetaSearch,
- selectedTypes: state.selectedTypes,
- letterSearch: state.letterSearch,
- });
+ tracking('grafana_prom_metric_encycopedia_tracking', state, metric.value);
onClose();
}
}
@@ -63,8 +56,9 @@ export function ResultsTable(props: ResultsTableProps) {
textToHighlight={metric.type ?? ''}
searchWords={state.metaHaystackMatches}
autoEscape
- highlightClassName={styles.matchHighLight}
- />
+ highlightClassName={`${styles.matchHighLight} ${metric.inferred ? styles.italicized : ''}`}
+ />{' '}
+ {inferredType(metric.inferred ?? false)}
- | {metric.type ?? ''} |
+
+ {metric.type ?? ''} {inferredType(metric.inferred ?? false)}
+ |
{metric.description ?? ''} |
>
);
}
}
+ function inferredType(inferred: boolean): JSX.Element | undefined {
+ if (inferred) {
+ return (
+
+
+
+ );
+ } else {
+ return undefined;
+ }
+ }
+
+ function noMetricsMessages(): ReactElement {
+ let message;
+
+ if (!state.fuzzySearchQuery) {
+ message = 'There are no metrics found in the data source.';
+ }
+
+ if (query.labels.length > 0) {
+ message = 'There are no metrics found. Try to expand your label filters.';
+ }
+
+ if (state.fuzzySearchQuery) {
+ message = 'There are no metrics found. Try to expand your search and filters.';
+ }
+
+ return (
+
+ | {message} |
+
+ );
+ }
+
return (
-
-
- | Name |
+
+
+ | Name |
{state.hasMetadata && (
<>
- Type |
- Description |
+ Type |
+ Description |
>
)}
<>
- {metrics &&
+ {metrics.length > 0 &&
metrics.map((metric: MetricData, idx: number) => {
return (
selectMetric(metric)}
+ tabIndex={0}
+ onFocus={() => onFocusRow(idx)}
+ onKeyDown={(e) => {
+ if (e.code === 'Enter' && e.currentTarget.classList.contains('selected-row')) {
+ selectMetric(metric);
+ }
+ }}
>
- |
+ |
);
})}
+ {metrics.length === 0 && !state.isLoading && noMetricsMessages()}
>
|
@@ -132,6 +170,7 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
return {
table: css`
+ ${disableTextWrap ? '' : 'table-layout: fixed;'}
border-radius: ${theme.shape.borderRadius()};
width: 100%;
white-space: ${disableTextWrap ? 'nowrap' : 'normal'};
@@ -142,11 +181,9 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
td,
th {
min-width: ${theme.spacing(3)};
+ border-bottom: 1px solid ${theme.colors.border.weak};
}
`,
- header: css`
- border-bottom: 1px solid ${theme.colors.border.weak};
- `,
row: css`
label: row;
cursor: pointer;
@@ -158,6 +195,9 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
background-color: ${rowHoverBg};
}
`,
+ tableHeaderPadding: css`
+ padding: 8px;
+ `,
selectedRow: css`
background-color: ${rowHoverBg};
`,
@@ -166,5 +206,26 @@ const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
color: ${theme.components.textHighlight.text};
background-color: ${theme.components.textHighlight.background};
`,
+ nameWidth: css`
+ ${disableTextWrap ? '' : 'width: 40%;'}
+ `,
+ nameOverflow: css`
+ ${disableTextWrap ? '' : 'overflow-wrap: anywhere;'}
+ `,
+ typeWidth: css`
+ ${disableTextWrap ? '' : 'width: 16%;'}
+ `,
+ stickyHeader: css`
+ position: sticky;
+ top: 0;
+ background-color: ${theme.colors.background.primary};
+ `,
+ noResults: css`
+ text-align: center;
+ color: ${theme.colors.text.secondary};
+ `,
+ italicized: css`
+ font-style: italic;
+ `,
};
};
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts
index 91e1d620688..ef6b7108795 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts
@@ -1,5 +1,6 @@
import { AnyAction } from '@reduxjs/toolkit';
+import { reportInteraction } from '@grafana/runtime';
import { PrometheusDatasource } from 'app/plugins/datasource/prometheus/datasource';
import { getMetadataHelp, getMetadataType } from 'app/plugins/datasource/prometheus/language_provider';
@@ -15,6 +16,7 @@ const { setFilteredMetricCount } = stateSlice.actions;
export async function setMetrics(
datasource: PrometheusDatasource,
query: PromVisualQuery,
+ inferType: boolean,
initialMetrics?: string[]
): Promise {
// metadata is set in the metric select now
@@ -32,17 +34,9 @@ export async function setMetrics(
let metricsData: MetricsData | undefined;
metricsData = initialMetrics?.map((m: string) => {
- const type = getMetadataType(m, datasource.languageProvider.metricsMetadata!);
- const description = getMetadataHelp(m, datasource.languageProvider.metricsMetadata!);
+ const metricData = buildMetricData(m, inferType, datasource);
- // possibly remove the type in favor of the type select
- const metaDataString = `${m}¦${type}¦${description}`;
-
- const metricData: MetricData = {
- value: m,
- type: type,
- description: description,
- };
+ const metaDataString = `${m}¦${metricData.type}¦${metricData.description}`;
nameHaystackDictionaryData[m] = metricData;
metaHaystackDictionaryData[metaDataString] = metricData;
@@ -61,6 +55,40 @@ export async function setMetrics(
};
}
+/**
+ * Builds the metric data object with type, description and inferred flag
+ *
+ * @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
+ * @returns A MetricData object.
+ */
+function buildMetricData(metric: string, inferType: boolean, datasource: PrometheusDatasource): MetricData {
+ 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!);
+
+ if (description?.toLowerCase().includes('histogram') && type !== 'histogram') {
+ type += ' (histogram)';
+ }
+
+ const metricData: MetricData = {
+ value: metric,
+ type: type,
+ description: description,
+ inferred: inferredType,
+ };
+
+ return metricData;
+}
+
/**
* The filtered and paginated metrics displayed in the modal
* */
@@ -75,12 +103,9 @@ export function displayedMetrics(state: MetricsModalState, dispatch: React.Dispa
}
/**
- * Filter the metrics with all the options, fuzzy, type, letter
- * @param metrics
- * @param skipLetterSearch used to show the alphabet letters as clickable before filtering out letters (needs to be refactored)
- * @returns
+ * Filter the metrics with all the options, fuzzy, type, null metadata
*/
-export function filterMetrics(state: MetricsModalState, skipLetterSearch?: boolean): MetricsData {
+export function filterMetrics(state: MetricsModalState): MetricsData {
let filteredMetrics: MetricsData = state.metrics;
if (state.fuzzySearchQuery && !state.useBackend) {
@@ -91,27 +116,29 @@ export function filterMetrics(state: MetricsModalState, skipLetterSearch?: boole
}
}
- if (state.letterSearch && !skipLetterSearch) {
- filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
- const letters: string[] = [state.letterSearch, state.letterSearch.toLowerCase()];
- return letters.includes(m.value[0]);
- });
- }
-
- if (state.selectedTypes.length > 0 && !state.useBackend) {
+ if (state.selectedTypes.length > 0) {
filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
// Matches type
- const matchesSelectedType = state.selectedTypes.some((t) => t.value === m.type);
+ const matchesSelectedType = state.selectedTypes.some((t) => {
+ if (m.type && t.value) {
+ return m.type.includes(t.value);
+ }
+ return false;
+ });
// missing type
const hasNoType = !m.type;
- return matchesSelectedType || (hasNoType && !state.excludeNullMetadata);
+ return matchesSelectedType || (hasNoType && state.includeNullMetadata);
});
}
- if (state.excludeNullMetadata) {
+ if (!state.includeNullMetadata) {
filteredMetrics = filteredMetrics.filter((m: MetricData) => {
+ if (state.inferType && m.inferred) {
+ return true;
+ }
+
return m.type !== undefined && m.description !== undefined;
});
}
@@ -152,15 +179,17 @@ export const calculateResultsPerPage = (results: number, defaultResults: number,
/**
* The backend query that replaces the uFuzzy search when the option 'useBackend' has been selected
+ * this is a regex search either to the series or labels Prometheus endpoint
+ * depending on which the Prometheus type or version supports
* @param metricText
* @param labels
* @param datasource
- * @returns
*/
export async function getBackendSearchMetrics(
metricText: string,
labels: QueryBuilderLabelFilter[],
- datasource: PrometheusDatasource
+ datasource: PrometheusDatasource,
+ inferType: boolean
): Promise> {
const queryString = regexifyLabelValuesQueryString(metricText);
@@ -173,14 +202,47 @@ export async function getBackendSearchMetrics(
const results = datasource.metricFindQuery(params);
return await results.then((results) => {
- return results.map((result) => {
- return {
- value: result.text,
- };
- });
+ return results.map((result) => buildMetricData(result.text, inferType, 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) {
+ switch (event) {
+ case 'grafana_prom_metric_encycopedia_tracking':
+ reportInteraction(event, {
+ metric: metric,
+ hasMetadata: state?.hasMetadata,
+ totalMetricCount: state?.totalMetricCount,
+ fuzzySearchQuery: state?.fuzzySearchQuery,
+ fullMetaSearch: state?.fullMetaSearch,
+ selectedTypes: state?.selectedTypes,
+ inferType: state?.inferType,
+ });
+ case 'grafana_prom_metric_encycopedia_disable_text_wrap_interaction':
+ reportInteraction(event, {
+ disableTextWrap: state?.disableTextWrap,
+ });
+ case 'grafana_prometheus_metric_encyclopedia_open':
+ reportInteraction(event, {
+ query: query,
+ });
+ }
+}
+
export const promTypes: PromFilterOption[] = [
{
value: 'counter',
@@ -205,9 +267,9 @@ export const promTypes: PromFilterOption[] = [
export const placeholders = {
browse: 'Search metrics by name',
- metadataSearchSwitch: 'Search by metadata type and description in addition to name',
- type: 'Select...',
- variables: 'Select...',
- excludeNoMetadata: 'Exclude results with no metadata',
- setUseBackend: 'Use the backend to browse metrics',
+ metadataSearchSwitch: 'Include search with type and description',
+ type: 'Filter by type',
+ includeNullMetadata: 'Include results with no metadata',
+ setUseBackend: 'Enable regex search',
+ inferType: 'Infer metric type',
};
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts
index 5e78c63ee0e..7e6749a481a 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts
@@ -48,7 +48,6 @@ export const stateSlice = createSlice({
setFuzzySearchQuery: (state, action: PayloadAction) => {
state.fuzzySearchQuery = action.payload;
state.pageNum = 1;
- state.letterSearch = '';
state.selectedIdx = 0;
},
setNameHaystack: (state, action: PayloadAction) => {
@@ -63,22 +62,17 @@ export const stateSlice = createSlice({
state.fullMetaSearch = action.payload;
state.pageNum = 1;
},
- setExcludeNullMetadata: (state, action: PayloadAction) => {
- state.excludeNullMetadata = action.payload;
+ setIncludeNullMetadata: (state, action: PayloadAction) => {
+ state.includeNullMetadata = action.payload;
state.pageNum = 1;
},
setSelectedTypes: (state, action: PayloadAction>>) => {
state.selectedTypes = action.payload;
state.pageNum = 1;
},
- setLetterSearch: (state, action: PayloadAction) => {
- state.letterSearch = action.payload;
- state.pageNum = 1;
- },
setUseBackend: (state, action: PayloadAction) => {
state.useBackend = action.payload;
state.fullMetaSearch = false;
- state.excludeNullMetadata = false;
state.pageNum = 1;
},
setSelectedIdx: (state, action: PayloadAction) => {
@@ -90,6 +84,9 @@ export const stateSlice = createSlice({
showAdditionalSettings: (state) => {
state.showAdditionalSettings = !state.showAdditionalSettings;
},
+ setInferType: (state, action: PayloadAction) => {
+ state.inferType = action.payload;
+ },
},
});
@@ -114,13 +111,13 @@ export function initialState(query?: PromVisualQuery): MetricsModalState {
pageNum: 1,
fuzzySearchQuery: '',
fullMetaSearch: query?.fullMetaSearch ?? false,
- excludeNullMetadata: query?.excludeNullMetadata ?? false,
+ includeNullMetadata: query?.includeNullMetadata ?? true,
selectedTypes: [],
- letterSearch: '',
useBackend: query?.useBackend ?? false,
disableTextWrap: query?.disableTextWrap ?? false,
selectedIdx: 0,
showAdditionalSettings: false,
+ inferType: true,
};
}
@@ -162,12 +159,10 @@ export interface MetricsModalState {
fuzzySearchQuery: string;
/** Enables the fuzzy meatadata search */
fullMetaSearch: boolean;
- /** Excludes results that are missing type and description */
- excludeNullMetadata: boolean;
+ /** Includes results that are missing type and description */
+ includeNullMetadata: boolean;
/** Filter by prometheus type */
selectedTypes: Array>;
- /** After results are filtered, select a letter to show metrics that start with that letter */
- letterSearch: string;
/** Filter by the series match endpoint instead of the fuzzy search */
useBackend: boolean;
/** Disable text wrap for descriptions in the results table */
@@ -176,6 +171,8 @@ export interface MetricsModalState {
selectedIdx: number;
/** Display toggle switches for settings */
showAdditionalSettings: boolean;
+ /** Check metric to match on substrings to infer prometheus type */
+ inferType: boolean;
}
/**
@@ -197,7 +194,7 @@ export function getSettings(visQuery: PromVisualQuery): MetricsModalSettings {
useBackend: visQuery?.useBackend ?? false,
disableTextWrap: visQuery?.disableTextWrap ?? false,
fullMetaSearch: visQuery?.fullMetaSearch ?? false,
- excludeNullMetadata: visQuery.excludeNullMetadata ?? false,
+ includeNullMetadata: visQuery.includeNullMetadata ?? false,
};
}
@@ -205,5 +202,5 @@ export type MetricsModalSettings = {
useBackend?: boolean;
disableTextWrap?: boolean;
fullMetaSearch?: boolean;
- excludeNullMetadata?: boolean;
+ includeNullMetadata?: boolean;
};
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/styles.ts b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/styles.ts
index 65978dae22f..bd5ad66f45e 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/styles.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/styles.ts
@@ -18,7 +18,6 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
flex-direction: row;
flex-wrap: wrap;
gap: ${theme.spacing(2)};
- margin-bottom: ${theme.spacing(2)};
`,
inputItemFirst: css`
flex-basis: 40%;
@@ -33,51 +32,33 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
selectWrapper: css`
margin-bottom: ${theme.spacing(1)};
`,
- selectItem: css`
- display: flex;
- flex-direction: row;
- align-items: center;
- `,
- selectItemLabel: css`
- margin: 0 0 0 ${theme.spacing(1)};
- align-self: center;
+ resultsAmount: css`
color: ${theme.colors.text.secondary};
- `,
- resultsHeading: css`
- margin: 0 0 0 0;
+ font-size: 0.85rem;
+ padding: 0 0 4px 0;
`,
resultsData: css`
- margin: 0 0 ${theme.spacing(1)} 0;
+ margin: 4px 0 ${theme.spacing(2)} 0;
`,
resultsDataCount: css`
margin: 0;
`,
resultsDataFiltered: css`
- margin: 0;
- color: ${theme.colors.warning.text};
+ color: ${theme.colors.text.secondary};
+ text-align: center;
+ border: solid 1px rgba(204, 204, 220, 0.25);
+ padding: 7px;
`,
- alphabetRow: css`
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- justify-content: space-between;
- align-items: center;
- column-gap: ${theme.spacing(1)};
- margin-bottom: ${theme.spacing(1)};
- `,
- alphabetRowToggles: css`
- display: flex;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
- column-gap: ${theme.spacing(1)};
+ resultsDataFilteredText: css`
+ display: inline;
+ vertical-align: text-top;
`,
results: css`
- height: calc(80vh - 280px);
+ height: calc(80vh - 310px);
overflow-y: scroll;
`,
- pageSettingsWrapper: css`
- padding-top: ${theme.spacing(1.5)};
+ resultsFooter: css`
+ margin-top: 24px;
display: flex;
flex-direction: row;
flex-wrap: wrap;
@@ -85,39 +66,29 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => {
align-items: center;
position: sticky;
`,
- pageSettings: css`
- display: flex;
- flex-direction: row;
- flex-wrap: wrap;
- align-items: center;
- `,
- selAlpha: css`
- cursor: pointer;
- color: #6e9fff;
- `,
- active: css`
- cursor: pointer;
- `,
- gray: css`
+ currentlySelected: css`
color: grey;
- opacity: 50%;
+ opacity: 75%;
+ font-size: 0.75rem;
`,
loadingSpinner: css`
- display: inline-block;
visibility: hidden;
`,
- table: css`
- white-space: ${disableTextWrap ? 'nowrap' : 'normal'};
- td {
- vertical-align: baseline;
- padding: 0;
- }
- `,
- tableDiv: css`
- padding: 8px;
- `,
visible: css`
visibility: visible;
`,
+ settingsBtn: css`
+ float: right;
+ `,
+ resultsPerPageLabel: css`
+ color: ${theme.colors.text.secondary};
+ opacity: 75%;
+ padding-top: 5px;
+ font-size: 0.85rem;
+ margin-right: 8px;
+ `,
+ resultsPerPageWrapper: css`
+ display: flex;
+ `,
};
};
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/types.ts
index ff47b531602..2f25a7d49f5 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/types.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/types.ts
@@ -2,8 +2,9 @@ export type MetricsData = MetricData[];
export type MetricData = {
value: string;
- type?: string;
+ type?: string | null;
description?: string;
+ inferred?: boolean;
};
export type PromFilterOption = {
diff --git a/public/app/plugins/datasource/prometheus/querybuilder/types.ts b/public/app/plugins/datasource/prometheus/querybuilder/types.ts
index c4f8b6b8c2c..1f7b12a32c4 100644
--- a/public/app/plugins/datasource/prometheus/querybuilder/types.ts
+++ b/public/app/plugins/datasource/prometheus/querybuilder/types.ts
@@ -12,7 +12,7 @@ export interface PromVisualQuery {
// metrics modal additional settings
useBackend?: boolean;
disableTextWrap?: boolean;
- excludeNullMetadata?: boolean;
+ includeNullMetadata?: boolean;
fullMetaSearch?: boolean;
}
diff --git a/public/app/plugins/datasource/prometheus/types.ts b/public/app/plugins/datasource/prometheus/types.ts
index 9fca875b939..60af3af315f 100644
--- a/public/app/plugins/datasource/prometheus/types.ts
+++ b/public/app/plugins/datasource/prometheus/types.ts
@@ -20,7 +20,7 @@ export interface PromQuery extends GenPromQuery, DataQuery {
useBackend?: boolean;
disableTextWrap?: boolean;
fullMetaSearch?: boolean;
- excludeNullMetadata?: boolean;
+ includeNullMetadata?: boolean;
}
export enum PrometheusCacheLevel {
|