mirror of
https://github.com/grafana/grafana.git
synced 2024-12-01 21:19:28 -06:00
Prometheus: Metric encyclopedia ux collab design (#68421)
* add class for full story click event on open modal * move feedback link to modal top under header * move results amount to bottom left * move settings into modal, change language from exclude to include * add metadata to backend search, use toggletip for settings, clean code * style input row, remove labels and update settings button design * remove alphabet search as requested by design * display selected metric * update style warning message for labels filtered metrics * organize results footer * update table design w fixed width and sticky header * allow focus row on tab and use key Enter to select metric on keydown * add rudderstack event for disable text wrap * add messages for no metrics found, labels, search and none in data source. * filter by type placeholder * add min width to custom select option * add text wrap for long metric names * Have 4px margin b/w the search row and the 'currently selected' text. 16px between 'currently selected text' and the table * Add some padding inside the first table header row (8 pixels on all sides) * font-size of 12px for additional settings text * 4px padding between additional settings text * 24px margin between the last table cell and the pagination row * # of Results per page font size 0.85rem * 8px margin b/w the '# of results per page' and the dropdown * fix test * add infer type setting for testing * use title on icon instead of wrapping in tooltip to fix test * fix icon issue * italicize inferred types, update setting text and add icon * add space for label filters alert message * make open button style consistent with advanced datasource picker * keep copy for open modal button * refactor rudderstack interactions and add inferType * add event tracking for opening the modal * galen's feedback, fix select horizontal scroll and results perpg bug * ismail's feedback for metric types * revert button in option for accessibility(galen) and style button with ghost mode * change name to Metrics explorer * fix hover/focus styles * ismail's feedbcak, refactor hints, return empty string, remove @return * Fix icon hovering: put tooltips back in over titles on icon * make results not expand to fill table space and fix width for modal open option button
This commit is contained in:
parent
effe21fb65
commit
2e6c71fd39
@ -6,7 +6,7 @@ import Highlighter from 'react-highlight-words';
|
||||
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
|
||||
import { EditorField, EditorFieldGroup } from '@grafana/experimental';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { AsyncSelect, Button, FormatOptionLabelMeta, useStyles2 } from '@grafana/ui';
|
||||
import { AsyncSelect, Button, FormatOptionLabelMeta, Icon, useStyles2 } from '@grafana/ui';
|
||||
import { SelectMenuOptions } from '@grafana/ui/src/components/Select/SelectMenu';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
@ -15,6 +15,7 @@ import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
import { PromVisualQuery } from '../types';
|
||||
|
||||
import { MetricsModal } from './metrics-modal/MetricsModal';
|
||||
import { tracking } from './metrics-modal/state/helpers';
|
||||
|
||||
// We are matching words split with space
|
||||
const splitSeparator = ' ';
|
||||
@ -119,6 +120,7 @@ export function MetricSelect({
|
||||
(query: string) => getMetricLabels(query),
|
||||
datasource.getDebounceTimeInMilliseconds()
|
||||
);
|
||||
|
||||
// No type found for the common select props so typing as any
|
||||
// https://github.com/grafana/grafana/blob/main/packages/grafana-ui/src/components/Select/SelectBase.tsx/#L212-L263
|
||||
// eslint-disable-next-line
|
||||
@ -131,7 +133,8 @@ export function MetricSelect({
|
||||
return (
|
||||
<div
|
||||
{...props.innerProps}
|
||||
className="metric-encyclopedia-open"
|
||||
ref={props.innerRef}
|
||||
className={`${styles.customOptionWidth} metric-encyclopedia-open`}
|
||||
onKeyDown={(e) => {
|
||||
// if there is no metric and the m.e. is enabled, open the modal
|
||||
if (e.code === 'Enter') {
|
||||
@ -146,14 +149,14 @@ export function MetricSelect({
|
||||
<div className={`${styles.customOptionDesc} metric-encyclopedia-open`}>{option.description}</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
fill="outline"
|
||||
fill="text"
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
onClick={() => setState({ ...state, metricsModalOpen: true })}
|
||||
icon="book"
|
||||
className="metric-encyclopedia-open"
|
||||
>
|
||||
Open
|
||||
<Icon name="arrow-right" />
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
@ -197,15 +200,16 @@ export function MetricSelect({
|
||||
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
|
||||
}
|
||||
|
||||
if (config.featureToggles.prometheusMetricEncyclopedia) {
|
||||
if (prometheusMetricEncyclopedia) {
|
||||
// pass the initial metrics, possibly filtered by labels into the Metrics Modal
|
||||
const metricsModalOption: SelectableValue[] = [
|
||||
{
|
||||
value: 'BrowseMetrics',
|
||||
label: 'Browse metrics',
|
||||
label: 'Metrics explorer',
|
||||
description: 'Browse and filter metrics and metadata with a fuzzy search',
|
||||
},
|
||||
];
|
||||
// pass the initial metrics into the Metrics Modal
|
||||
setState({
|
||||
metrics: [...metricsModalOption, ...metrics],
|
||||
isLoading: undefined,
|
||||
@ -222,13 +226,14 @@ export function MetricSelect({
|
||||
if (value) {
|
||||
// if there is no metric and the m.e. is enabled, open the modal
|
||||
if (prometheusMetricEncyclopedia && value === 'BrowseMetrics') {
|
||||
tracking('grafana_prometheus_metric_encyclopedia_open', null, '', query);
|
||||
setState({ ...state, metricsModalOpen: true });
|
||||
} else {
|
||||
onChange({ ...query, metric: value });
|
||||
}
|
||||
}
|
||||
}}
|
||||
components={{ Option: CustomOption }}
|
||||
components={prometheusMetricEncyclopedia ? { Option: CustomOption } : {}}
|
||||
/>
|
||||
</EditorField>
|
||||
</EditorFieldGroup>
|
||||
@ -253,7 +258,7 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
:hover {
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.1)};
|
||||
}
|
||||
`,
|
||||
customOptionlabel: css`
|
||||
@ -265,7 +270,10 @@ const getStyles = (theme: GrafanaTheme2) => ({
|
||||
opacity: 50%;
|
||||
`,
|
||||
focus: css`
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.03)};
|
||||
background-color: ${theme.colors.emphasize(theme.colors.background.primary, 0.1)};
|
||||
`,
|
||||
customOptionWidth: css`
|
||||
min-width: 400px;
|
||||
`,
|
||||
});
|
||||
|
||||
|
@ -45,7 +45,7 @@ export function PromQueryBuilderContainer(props: Props) {
|
||||
useBackend: query.useBackend ?? false,
|
||||
disableTextWrap: query.disableTextWrap ?? false,
|
||||
fullMetaSearch: query.fullMetaSearch ?? false,
|
||||
excludeNullMetadata: query.excludeNullMetadata ?? false,
|
||||
includeNullMetadata: query.includeNullMetadata ?? true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@ -103,7 +103,7 @@ const stateSlice = createSlice({
|
||||
state.visQuery.useBackend = action.payload.useBackend;
|
||||
state.visQuery.disableTextWrap = action.payload.disableTextWrap;
|
||||
state.visQuery.fullMetaSearch = action.payload.fullMetaSearch;
|
||||
state.visQuery.excludeNullMetadata = action.payload.excludeNullMetadata;
|
||||
state.visQuery.includeNullMetadata = action.payload.includeNullMetadata;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -0,0 +1,100 @@
|
||||
import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Icon, Switch, Tooltip, useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { testIds } from './MetricsModal';
|
||||
import { placeholders } from './state/helpers';
|
||||
import { MetricsModalState } from './state/state';
|
||||
|
||||
type AdditionalSettingsProps = {
|
||||
state: MetricsModalState;
|
||||
onChangeFullMetaSearch: () => void;
|
||||
onChangeIncludeNullMetadata: () => void;
|
||||
onChangeDisableTextWrap: () => void;
|
||||
onChangeUseBackend: () => void;
|
||||
onChangeInferType: () => void;
|
||||
};
|
||||
|
||||
export function AdditionalSettings(props: AdditionalSettingsProps) {
|
||||
const {
|
||||
state,
|
||||
onChangeFullMetaSearch,
|
||||
onChangeIncludeNullMetadata,
|
||||
onChangeDisableTextWrap,
|
||||
onChangeUseBackend,
|
||||
onChangeInferType,
|
||||
} = props;
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
data-testid={testIds.searchWithMetadata}
|
||||
value={state.fullMetaSearch}
|
||||
disabled={state.useBackend || !state.hasMetadata}
|
||||
onChange={() => onChangeFullMetaSearch()}
|
||||
/>
|
||||
<div className={styles.selectItemLabel}>{placeholders.metadataSearchSwitch}</div>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
value={state.includeNullMetadata}
|
||||
disabled={!state.hasMetadata}
|
||||
onChange={() => onChangeIncludeNullMetadata()}
|
||||
/>
|
||||
<div className={styles.selectItemLabel}>{placeholders.includeNullMetadata}</div>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch value={state.disableTextWrap} onChange={() => onChangeDisableTextWrap()} />
|
||||
<div className={styles.selectItemLabel}>Disable text wrap</div>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch data-testid={testIds.setUseBackend} value={state.useBackend} onChange={() => onChangeUseBackend()} />
|
||||
<div className={styles.selectItemLabel}>{placeholders.setUseBackend} </div>
|
||||
<Tooltip
|
||||
content={'Filter metric names by regex search, using an additional call on the Prometheus API.'}
|
||||
placement="bottom-end"
|
||||
>
|
||||
<Icon name="info-circle" size="xs" className={styles.settingsIcon} />
|
||||
</Tooltip>
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
settingsIcon: css`
|
||||
color: ${theme.colors.text.secondary};
|
||||
`,
|
||||
selectItem: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 4px 0;
|
||||
`,
|
||||
selectItemLabel: css`
|
||||
margin: 0 0 0 ${theme.spacing(1)};
|
||||
align-self: center;
|
||||
color: ${theme.colors.text.secondary};
|
||||
font-size: 12px;
|
||||
`,
|
||||
};
|
||||
}
|
@ -13,7 +13,7 @@ export function FeedbackLink({ feedbackUrl }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<Stack gap={1}>
|
||||
<Stack>
|
||||
<a
|
||||
href={feedbackUrl}
|
||||
className={styles.link}
|
||||
@ -35,6 +35,7 @@ function getStyles(theme: GrafanaTheme2) {
|
||||
':hover': {
|
||||
color: theme.colors.text.link,
|
||||
},
|
||||
margin: `-25px 0 30px 0`,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
@ -1,71 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { useTheme2 } from '@grafana/ui';
|
||||
|
||||
import { getStyles } from './styles';
|
||||
import { MetricData, MetricsData } from './types';
|
||||
|
||||
export type LetterSearchProps = {
|
||||
filteredMetrics: MetricsData;
|
||||
disableTextWrap: boolean;
|
||||
updateLetterSearch: (letter: string) => void;
|
||||
letterSearch: string | null;
|
||||
};
|
||||
|
||||
export function LetterSearch(props: LetterSearchProps) {
|
||||
const { filteredMetrics, disableTextWrap, updateLetterSearch, letterSearch } = props;
|
||||
|
||||
const alphabetDictionary = alphabetCheck();
|
||||
|
||||
const theme = useTheme2();
|
||||
const styles = getStyles(theme, disableTextWrap);
|
||||
|
||||
filteredMetrics.forEach((m: MetricData, idx: number) => {
|
||||
const metricFirstLetter = m.value[0].toUpperCase();
|
||||
|
||||
if (alphabet.includes(metricFirstLetter) && !alphabetDictionary[metricFirstLetter]) {
|
||||
alphabetDictionary[metricFirstLetter] += 1;
|
||||
}
|
||||
});
|
||||
|
||||
// return the alphabet components with the correct style and behavior
|
||||
return (
|
||||
<div>
|
||||
{Object.keys(alphabetDictionary).map((letter: string) => {
|
||||
const active: boolean = alphabetDictionary[letter] > 0;
|
||||
// starts with letter search
|
||||
// filter by starts with letter
|
||||
// if same letter searched null out remove letter search
|
||||
function setLetterSearch() {
|
||||
updateLetterSearch(letter);
|
||||
}
|
||||
// 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 ? setLetterSearch : () => {}}
|
||||
className={`${selectedClass} ${activeClass}`}
|
||||
key={letter}
|
||||
data-testid={'letter-' + letter}
|
||||
>
|
||||
{letter + ' '}
|
||||
{/* {idx !== coll.length - 1 ? '|': ''} */}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const alphabet = [...'ABCDEFGHIJKLMNOPQRSTUVWXYZ'];
|
||||
|
||||
function alphabetCheck(): { [char: string]: number } {
|
||||
const check: { [char: string]: number } = {};
|
||||
|
||||
alphabet.forEach((char) => (check[char] = 0));
|
||||
|
||||
return check;
|
||||
}
|
@ -22,7 +22,7 @@ describe('MetricsModal', () => {
|
||||
it('renders the modal', async () => {
|
||||
setup(defaultQuery, listOfMetrics);
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Browse metrics')).toBeInTheDocument();
|
||||
expect(screen.getByText('Metrics explorer')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@ -88,20 +88,6 @@ describe('MetricsModal', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('filters by alphebetical letter choice', async () => {
|
||||
setup(defaultQuery, listOfMetrics);
|
||||
// pick the letter J
|
||||
const letterJ = screen.getByTestId('letter-J');
|
||||
await userEvent.click(letterJ);
|
||||
|
||||
// check metrics that start with J
|
||||
const metricStartingWithJ = screen.getByText('j');
|
||||
expect(metricStartingWithJ).toBeInTheDocument();
|
||||
// check metrics that don't start with J
|
||||
const metricStartingWithSomethingElse = screen.queryByText('a');
|
||||
expect(metricStartingWithSomethingElse).toBeNull();
|
||||
});
|
||||
|
||||
// Pagination
|
||||
it('shows metrics within a range by pagination', async () => {
|
||||
// default resultsPerPage is 100
|
||||
|
@ -3,31 +3,39 @@ import debounce from 'debounce-promise';
|
||||
import React, { useCallback, useEffect, useMemo, useReducer } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditorField } from '@grafana/experimental';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { InlineField, Switch, Input, Modal, MultiSelect, Spinner, useTheme2, Pagination, Button } from '@grafana/ui';
|
||||
import {
|
||||
Input,
|
||||
Modal,
|
||||
MultiSelect,
|
||||
Spinner,
|
||||
useTheme2,
|
||||
Pagination,
|
||||
Button,
|
||||
Toggletip,
|
||||
ButtonGroup,
|
||||
Icon,
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { PrometheusDatasource } from '../../../datasource';
|
||||
import { PromVisualQuery } from '../../types';
|
||||
|
||||
import { AdditionalSettings } from './AdditionalSettings';
|
||||
import { FeedbackLink } from './FeedbackLink';
|
||||
import { LetterSearch } from './LetterSearch';
|
||||
import { ResultsTable } from './ResultsTable';
|
||||
import {
|
||||
calculatePageList,
|
||||
calculateResultsPerPage,
|
||||
displayedMetrics,
|
||||
filterMetrics,
|
||||
getBackendSearchMetrics,
|
||||
setMetrics,
|
||||
placeholders,
|
||||
promTypes,
|
||||
tracking,
|
||||
} from './state/helpers';
|
||||
import {
|
||||
DEFAULT_RESULTS_PER_PAGE,
|
||||
initialState,
|
||||
MAXIMUM_RESULTS_PER_PAGE,
|
||||
// MetricsModalReducer,
|
||||
MetricsModalMetadata,
|
||||
stateSlice,
|
||||
} from './state/state';
|
||||
@ -44,7 +52,7 @@ export type MetricsModalProps = {
|
||||
initialMetrics: string[];
|
||||
};
|
||||
|
||||
// actions
|
||||
// actions to update the state
|
||||
const {
|
||||
setIsLoading,
|
||||
buildMetrics,
|
||||
@ -55,13 +63,13 @@ const {
|
||||
setNameHaystack,
|
||||
setMetaHaystack,
|
||||
setFullMetaSearch,
|
||||
setExcludeNullMetadata,
|
||||
setIncludeNullMetadata,
|
||||
setSelectedTypes,
|
||||
setLetterSearch,
|
||||
setUseBackend,
|
||||
setSelectedIdx,
|
||||
setDisableTextWrap,
|
||||
showAdditionalSettings,
|
||||
setInferType,
|
||||
} = stateSlice.actions;
|
||||
|
||||
export const MetricsModal = (props: MetricsModalProps) => {
|
||||
@ -75,27 +83,31 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
/**
|
||||
* loads metrics and metadata on opening modal and switching off useBackend
|
||||
*/
|
||||
const updateMetricsMetadata = useCallback(async () => {
|
||||
// *** Loading Gif
|
||||
dispatch(setIsLoading(true));
|
||||
const updateMetricsMetadata = useCallback(
|
||||
async (inferType: boolean) => {
|
||||
// *** Loading Gif
|
||||
dispatch(setIsLoading(true));
|
||||
|
||||
const data: MetricsModalMetadata = await setMetrics(datasource, query, initialMetrics);
|
||||
const data: MetricsModalMetadata = await setMetrics(datasource, query, inferType, initialMetrics);
|
||||
|
||||
dispatch(
|
||||
buildMetrics({
|
||||
isLoading: false,
|
||||
hasMetadata: data.hasMetadata,
|
||||
metrics: data.metrics,
|
||||
metaHaystackDictionary: data.metaHaystackDictionary,
|
||||
nameHaystackDictionary: data.nameHaystackDictionary,
|
||||
totalMetricCount: data.metrics.length,
|
||||
filteredMetricCount: data.metrics.length,
|
||||
})
|
||||
);
|
||||
}, [query, datasource, initialMetrics]);
|
||||
dispatch(
|
||||
buildMetrics({
|
||||
isLoading: false,
|
||||
hasMetadata: data.hasMetadata,
|
||||
metrics: data.metrics,
|
||||
metaHaystackDictionary: data.metaHaystackDictionary,
|
||||
nameHaystackDictionary: data.nameHaystackDictionary,
|
||||
totalMetricCount: data.metrics.length,
|
||||
filteredMetricCount: data.metrics.length,
|
||||
})
|
||||
);
|
||||
},
|
||||
[query, datasource, initialMetrics]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
updateMetricsMetadata();
|
||||
updateMetricsMetadata(state.inferType);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [updateMetricsMetadata]);
|
||||
|
||||
const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => {
|
||||
@ -111,10 +123,10 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
*/
|
||||
const debouncedBackendSearch = useMemo(
|
||||
() =>
|
||||
debounce(async (metricText: string) => {
|
||||
debounce(async (metricText: string, inferType: boolean) => {
|
||||
dispatch(setIsLoading(true));
|
||||
|
||||
const metrics = await getBackendSearchMetrics(metricText, query.labels, datasource);
|
||||
const metrics = await getBackendSearchMetrics(metricText, query.labels, datasource, inferType);
|
||||
|
||||
dispatch(
|
||||
filterMetricsBackend({
|
||||
@ -135,12 +147,12 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
dispatch(setMetaHaystack(haystackData));
|
||||
}
|
||||
|
||||
function fuzzySearchCallback(query: string, fullMetaSearchVal: boolean) {
|
||||
function searchCallback(query: string, fullMetaSearchVal: boolean) {
|
||||
if (state.useBackend && query === '') {
|
||||
// get all metrics data if a user erases everything in the input
|
||||
updateMetricsMetadata();
|
||||
updateMetricsMetadata(state.inferType);
|
||||
} else if (state.useBackend) {
|
||||
debouncedBackendSearch(query);
|
||||
debouncedBackendSearch(query, state.inferType);
|
||||
} else {
|
||||
// search either the names or all metadata
|
||||
// fuzzy search go!
|
||||
@ -161,179 +173,137 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
const metric = displayedMetrics(state, dispatch)[state.selectedIdx];
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings switches */
|
||||
const additionalSettings = (
|
||||
<AdditionalSettings
|
||||
state={state}
|
||||
onChangeFullMetaSearch={() => {
|
||||
const newVal = !state.fullMetaSearch;
|
||||
dispatch(setFullMetaSearch(newVal));
|
||||
onChange({ ...query, fullMetaSearch: newVal });
|
||||
|
||||
searchCallback(state.fuzzySearchQuery, newVal);
|
||||
}}
|
||||
onChangeIncludeNullMetadata={() => {
|
||||
dispatch(setIncludeNullMetadata(!state.includeNullMetadata));
|
||||
onChange({ ...query, includeNullMetadata: !state.includeNullMetadata });
|
||||
}}
|
||||
onChangeDisableTextWrap={() => {
|
||||
dispatch(setDisableTextWrap());
|
||||
onChange({ ...query, disableTextWrap: !state.disableTextWrap });
|
||||
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={() => {
|
||||
const newVal = !state.useBackend;
|
||||
dispatch(setUseBackend(newVal));
|
||||
onChange({ ...query, useBackend: newVal });
|
||||
if (newVal === false) {
|
||||
// rebuild the metrics metadata if we turn off useBackend
|
||||
updateMetricsMetadata(state.inferType);
|
||||
} else {
|
||||
// check if there is text in the browse search and update
|
||||
if (state.fuzzySearchQuery !== '') {
|
||||
debouncedBackendSearch(state.fuzzySearchQuery, state.inferType);
|
||||
}
|
||||
// otherwise wait for user typing
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
data-testid={testIds.metricModal}
|
||||
isOpen={isOpen}
|
||||
title="Browse metrics"
|
||||
title="Metrics explorer"
|
||||
onDismiss={onClose}
|
||||
aria-label="Browse metrics"
|
||||
className={styles.modal}
|
||||
>
|
||||
<FeedbackLink feedbackUrl="https://forms.gle/DEMAJHoAMpe3e54CA" />
|
||||
<div className={styles.inputWrapper}>
|
||||
<div className={cx(styles.inputItem, styles.inputItemFirst)}>
|
||||
<EditorField label="Search metrics">
|
||||
<Input
|
||||
autoFocus={true}
|
||||
data-testid={testIds.searchMetric}
|
||||
placeholder={placeholders.browse}
|
||||
value={state.fuzzySearchQuery}
|
||||
onInput={(e) => {
|
||||
const value = e.currentTarget.value ?? '';
|
||||
dispatch(setFuzzySearchQuery(value));
|
||||
|
||||
fuzzySearchCallback(value, state.fullMetaSearch);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
</EditorField>
|
||||
<Input
|
||||
autoFocus={true}
|
||||
data-testid={testIds.searchMetric}
|
||||
placeholder={placeholders.browse}
|
||||
value={state.fuzzySearchQuery}
|
||||
onInput={(e) => {
|
||||
const value = e.currentTarget.value ?? '';
|
||||
dispatch(setFuzzySearchQuery(value));
|
||||
searchCallback(value, state.fullMetaSearch);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.inputItem}>
|
||||
<EditorField label="Filter by type">
|
||||
<div>
|
||||
<Spinner className={`${styles.loadingSpinner} ${state.isLoading ? styles.visible : ''}`} />
|
||||
</div>
|
||||
{state.hasMetadata && (
|
||||
<div className={styles.inputItem}>
|
||||
<MultiSelect
|
||||
data-testid={testIds.selectType}
|
||||
inputId="my-select"
|
||||
options={typeOptions}
|
||||
value={state.selectedTypes}
|
||||
disabled={!state.hasMetadata || state.useBackend}
|
||||
placeholder={placeholders.type}
|
||||
onChange={(v) => {
|
||||
// *** Filter by type
|
||||
// *** always include metrics without metadata but label it as unknown type
|
||||
// Consider tabs select instead of actual select or multi select
|
||||
dispatch(setSelectedTypes(v));
|
||||
}}
|
||||
onChange={(v) => dispatch(setSelectedTypes(v))}
|
||||
/>
|
||||
</EditorField>
|
||||
</div>
|
||||
</div>
|
||||
{/* <h4 className={styles.resultsHeading}>Results</h4> */}
|
||||
<div className={styles.resultsData}>
|
||||
<div className={styles.resultsDataCount}>
|
||||
Showing {state.filteredMetricCount} of {state.totalMetricCount} results.{' '}
|
||||
<Spinner className={`${styles.loadingSpinner} ${state.isLoading ? styles.visible : ''}`} />
|
||||
<div className={styles.selectWrapper}>
|
||||
<div className={styles.alphabetRow}>
|
||||
<LetterSearch
|
||||
filteredMetrics={filterMetrics(state, true)}
|
||||
disableTextWrap={state.disableTextWrap}
|
||||
updateLetterSearch={(letter: string) => {
|
||||
if (state.letterSearch === letter) {
|
||||
dispatch(setLetterSearch(''));
|
||||
} else {
|
||||
dispatch(setLetterSearch(letter));
|
||||
}
|
||||
}}
|
||||
letterSearch={state.letterSearch}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.inputItem}>
|
||||
<Toggletip
|
||||
aria-label="Additional settings"
|
||||
content={additionalSettings}
|
||||
placement="bottom-end"
|
||||
closeButton={false}
|
||||
>
|
||||
<ButtonGroup className={styles.settingsBtn}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
fill="text"
|
||||
size="sm"
|
||||
size="md"
|
||||
onClick={() => dispatch(showAdditionalSettings())}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
data-testid={testIds.showAdditionalSettings}
|
||||
>
|
||||
Additional Settings
|
||||
</Button>
|
||||
</div>
|
||||
{state.showAdditionalSettings && (
|
||||
<>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
data-testid={testIds.searchWithMetadata}
|
||||
value={state.fullMetaSearch}
|
||||
disabled={state.useBackend || !state.hasMetadata}
|
||||
onChange={() => {
|
||||
const newVal = !state.fullMetaSearch;
|
||||
dispatch(setFullMetaSearch(newVal));
|
||||
onChange({ ...query, fullMetaSearch: newVal });
|
||||
|
||||
fuzzySearchCallback(state.fuzzySearchQuery, newVal);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
<p className={styles.selectItemLabel}>{placeholders.metadataSearchSwitch}</p>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
value={state.excludeNullMetadata}
|
||||
disabled={state.useBackend || !state.hasMetadata}
|
||||
onChange={() => {
|
||||
dispatch(setExcludeNullMetadata(!state.excludeNullMetadata));
|
||||
onChange({ ...query, excludeNullMetadata: !state.excludeNullMetadata });
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
<p className={styles.selectItemLabel}>{placeholders.excludeNoMetadata}</p>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
value={state.disableTextWrap}
|
||||
onChange={() => {
|
||||
dispatch(setDisableTextWrap());
|
||||
onChange({ ...query, disableTextWrap: !state.disableTextWrap });
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
<p className={styles.selectItemLabel}>Disable text wrap</p>
|
||||
</div>
|
||||
<div className={styles.selectItem}>
|
||||
<Switch
|
||||
data-testid={testIds.setUseBackend}
|
||||
value={state.useBackend}
|
||||
onChange={() => {
|
||||
const newVal = !state.useBackend;
|
||||
dispatch(setUseBackend(newVal));
|
||||
onChange({ ...query, useBackend: newVal });
|
||||
if (newVal === false) {
|
||||
// rebuild the metrics metadata if we turn off useBackend
|
||||
updateMetricsMetadata();
|
||||
} else {
|
||||
// check if there is text in the browse search and update
|
||||
if (state.fuzzySearchQuery !== '') {
|
||||
debouncedBackendSearch(state.fuzzySearchQuery);
|
||||
}
|
||||
// otherwise wait for user typing
|
||||
}
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
keyFunction(e);
|
||||
}}
|
||||
/>
|
||||
<p className={styles.selectItemLabel}>{placeholders.setUseBackend}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<Button variant="secondary" icon={state.showAdditionalSettings ? 'angle-up' : 'angle-down'} />
|
||||
</ButtonGroup>
|
||||
</Toggletip>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.resultsData}>
|
||||
{query.metric && <i className={styles.currentlySelected}>Currently selected: {query.metric}</i>}
|
||||
{query.labels.length > 0 && (
|
||||
<p className={styles.resultsDataFiltered}>
|
||||
These metrics have been pre-filtered by labels chosen in the label filters.
|
||||
</p>
|
||||
<div className={styles.resultsDataFiltered}>
|
||||
<Icon name="info-circle" size="sm" />
|
||||
<div className={styles.resultsDataFilteredText}>
|
||||
These metrics have been pre-filtered by labels chosen in the label filters.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.results}>
|
||||
@ -346,43 +316,42 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
state={state}
|
||||
selectedIdx={state.selectedIdx}
|
||||
disableTextWrap={state.disableTextWrap}
|
||||
onFocusRow={(idx: number) => dispatch(setSelectedIdx(idx))}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.resultsFooter}>
|
||||
<div className={styles.resultsAmount}>
|
||||
Showing {state.filteredMetricCount} of {state.totalMetricCount} results
|
||||
</div>
|
||||
<Pagination
|
||||
currentPage={state.pageNum ?? 1}
|
||||
numberOfPages={calculatePageList(state).length}
|
||||
onNavigate={(val: number) => {
|
||||
const page = val ?? 1;
|
||||
dispatch(setPageNum(page));
|
||||
}}
|
||||
/>
|
||||
<div className={styles.resultsPerPageWrapper}>
|
||||
<p className={styles.resultsPerPageLabel}># Results per page </p>
|
||||
<Input
|
||||
data-testid={testIds.resultsPerPage}
|
||||
value={calculateResultsPerPage(state.resultsPerPage, DEFAULT_RESULTS_PER_PAGE, MAXIMUM_RESULTS_PER_PAGE)}
|
||||
placeholder="results per page"
|
||||
width={10}
|
||||
title={'The maximum results per page is ' + MAXIMUM_RESULTS_PER_PAGE}
|
||||
type="number"
|
||||
onInput={(e) => {
|
||||
const value = +e.currentTarget.value;
|
||||
|
||||
<div className={styles.pageSettingsWrapper}>
|
||||
<div className={styles.pageSettings}>
|
||||
<InlineField
|
||||
label="# results per page"
|
||||
tooltip={'The maximum results per page is ' + MAXIMUM_RESULTS_PER_PAGE}
|
||||
labelWidth={20}
|
||||
>
|
||||
<Input
|
||||
data-testid={testIds.resultsPerPage}
|
||||
value={calculateResultsPerPage(state.resultsPerPage, DEFAULT_RESULTS_PER_PAGE, MAXIMUM_RESULTS_PER_PAGE)}
|
||||
placeholder="results per page"
|
||||
width={20}
|
||||
onInput={(e) => {
|
||||
const value = +e.currentTarget.value;
|
||||
if (isNaN(value) || value >= MAXIMUM_RESULTS_PER_PAGE) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (isNaN(value)) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch(setResultsPerPage(value));
|
||||
}}
|
||||
/>
|
||||
</InlineField>
|
||||
<Pagination
|
||||
currentPage={state.pageNum ?? 1}
|
||||
numberOfPages={calculatePageList(state).length}
|
||||
onNavigate={(val: number) => {
|
||||
const page = val ?? 1;
|
||||
dispatch(setPageNum(page));
|
||||
dispatch(setResultsPerPage(value));
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<FeedbackLink feedbackUrl="https://forms.gle/DEMAJHoAMpe3e54CA" />
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
@ -399,4 +368,5 @@ export const testIds = {
|
||||
resultsPerPage: 'results-per-page',
|
||||
setUseBackend: 'set-use-backend',
|
||||
showAdditionalSettings: 'show-additional-settings',
|
||||
inferType: 'set-infer-type',
|
||||
};
|
||||
|
@ -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)}
|
||||
</td>
|
||||
<td>
|
||||
<Highlighter
|
||||
@ -79,37 +73,80 @@ export function ResultsTable(props: ResultsTableProps) {
|
||||
} else {
|
||||
return (
|
||||
<>
|
||||
<td>{metric.type ?? ''}</td>
|
||||
<td className={metric.inferred ? styles.italicized : ''}>
|
||||
{metric.type ?? ''} {inferredType(metric.inferred ?? false)}
|
||||
</td>
|
||||
<td>{metric.description ?? ''}</td>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function inferredType(inferred: boolean): JSX.Element | undefined {
|
||||
if (inferred) {
|
||||
return (
|
||||
<Tooltip content={'This metric type has been inferred'} placement="bottom-end">
|
||||
<Icon name="info-circle" size="xs" />
|
||||
</Tooltip>
|
||||
);
|
||||
} 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 (
|
||||
<tr className={styles.noResults}>
|
||||
<td colSpan={3}>{message}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<table className={styles.table} ref={tableRef}>
|
||||
<thead>
|
||||
<tr className={styles.header}>
|
||||
<th>Name</th>
|
||||
<thead className={styles.stickyHeader}>
|
||||
<tr>
|
||||
<th className={`${styles.nameWidth} ${styles.tableHeaderPadding}`}>Name</th>
|
||||
{state.hasMetadata && (
|
||||
<>
|
||||
<th>Type</th>
|
||||
<th>Description</th>
|
||||
<th className={`${styles.typeWidth} ${styles.tableHeaderPadding}`}>Type</th>
|
||||
<th className={styles.tableHeaderPadding}>Description</th>
|
||||
</>
|
||||
)}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<>
|
||||
{metrics &&
|
||||
{metrics.length > 0 &&
|
||||
metrics.map((metric: MetricData, idx: number) => {
|
||||
return (
|
||||
<tr
|
||||
key={metric?.value ?? idx}
|
||||
className={`${styles.row} ${isSelectedRow(idx) ? `${styles.selectedRow} selected-row` : ''}`}
|
||||
onClick={() => selectMetric(metric)}
|
||||
tabIndex={0}
|
||||
onFocus={() => onFocusRow(idx)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.code === 'Enter' && e.currentTarget.classList.contains('selected-row')) {
|
||||
selectMetric(metric);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td>
|
||||
<td className={styles.nameOverflow}>
|
||||
<Highlighter
|
||||
textToHighlight={metric?.value ?? ''}
|
||||
searchWords={state.fullMetaSearch ? state.metaHaystackMatches : state.nameHaystackMatches}
|
||||
@ -121,6 +158,7 @@ export function ResultsTable(props: ResultsTableProps) {
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
{metrics.length === 0 && !state.isLoading && noMetricsMessages()}
|
||||
</>
|
||||
</tbody>
|
||||
</table>
|
||||
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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<MetricsModalMetadata> {
|
||||
// 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<Array<{ value: string }>> {
|
||||
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',
|
||||
};
|
||||
|
@ -48,7 +48,6 @@ export const stateSlice = createSlice({
|
||||
setFuzzySearchQuery: (state, action: PayloadAction<string>) => {
|
||||
state.fuzzySearchQuery = action.payload;
|
||||
state.pageNum = 1;
|
||||
state.letterSearch = '';
|
||||
state.selectedIdx = 0;
|
||||
},
|
||||
setNameHaystack: (state, action: PayloadAction<string[][]>) => {
|
||||
@ -63,22 +62,17 @@ export const stateSlice = createSlice({
|
||||
state.fullMetaSearch = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setExcludeNullMetadata: (state, action: PayloadAction<boolean>) => {
|
||||
state.excludeNullMetadata = action.payload;
|
||||
setIncludeNullMetadata: (state, action: PayloadAction<boolean>) => {
|
||||
state.includeNullMetadata = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setSelectedTypes: (state, action: PayloadAction<Array<SelectableValue<string>>>) => {
|
||||
state.selectedTypes = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setLetterSearch: (state, action: PayloadAction<string>) => {
|
||||
state.letterSearch = action.payload;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setUseBackend: (state, action: PayloadAction<boolean>) => {
|
||||
state.useBackend = action.payload;
|
||||
state.fullMetaSearch = false;
|
||||
state.excludeNullMetadata = false;
|
||||
state.pageNum = 1;
|
||||
},
|
||||
setSelectedIdx: (state, action: PayloadAction<number>) => {
|
||||
@ -90,6 +84,9 @@ export const stateSlice = createSlice({
|
||||
showAdditionalSettings: (state) => {
|
||||
state.showAdditionalSettings = !state.showAdditionalSettings;
|
||||
},
|
||||
setInferType: (state, action: PayloadAction<boolean>) => {
|
||||
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<SelectableValue<string>>;
|
||||
/** 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;
|
||||
};
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -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 = {
|
||||
|
@ -12,7 +12,7 @@ export interface PromVisualQuery {
|
||||
// metrics modal additional settings
|
||||
useBackend?: boolean;
|
||||
disableTextWrap?: boolean;
|
||||
excludeNullMetadata?: boolean;
|
||||
includeNullMetadata?: boolean;
|
||||
fullMetaSearch?: boolean;
|
||||
}
|
||||
|
||||
|
@ -20,7 +20,7 @@ export interface PromQuery extends GenPromQuery, DataQuery {
|
||||
useBackend?: boolean;
|
||||
disableTextWrap?: boolean;
|
||||
fullMetaSearch?: boolean;
|
||||
excludeNullMetadata?: boolean;
|
||||
includeNullMetadata?: boolean;
|
||||
}
|
||||
|
||||
export enum PrometheusCacheLevel {
|
||||
|
Loading…
Reference in New Issue
Block a user