diff --git a/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts b/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts index 265095ecd65..0102bcec3ab 100644 --- a/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts +++ b/e2e/old-arch/various-suite/helpers/prometheus-helpers.ts @@ -24,11 +24,11 @@ export function createPromDS(dataSourceID: string, name: string): void { } export function getResources() { - cy.intercept(/__name__/g, metricResponse); + cy.intercept(/__name__/g, metricResponse).as('getMetricNames'); - cy.intercept(/metadata/g, metadataResponse); + cy.intercept(/metadata/g, metadataResponse).as('getMetadata'); - cy.intercept(/labels/g, labelsResponse); + cy.intercept(/labels/g, labelsResponse).as('getLabels'); } const metricResponse = { diff --git a/e2e/old-arch/various-suite/prometheus-editor.spec.ts b/e2e/old-arch/various-suite/prometheus-editor.spec.ts index 4a2b5aed7a3..7b3093f4f5d 100644 --- a/e2e/old-arch/various-suite/prometheus-editor.spec.ts +++ b/e2e/old-arch/various-suite/prometheus-editor.spec.ts @@ -138,12 +138,11 @@ describe('Prometheus query editor', () => { it('can select a metric and provide a hint', () => { navigateToEditor('Builder', 'prometheusBuilder'); - getResources(); - - e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist').click().type('metric1'); + e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().should('exist').click(); + cy.wait('@getMetadata'); + e2e.components.DataSource.Prometheus.queryEditor.builder.metricSelect().type('metric1'); selectOption('metric1'); - e2e.components.DataSource.Prometheus.queryEditor.builder.hints().contains('hint: add rate'); }); diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx index 4e4c63352f3..1edbf5ba744 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.test.tsx @@ -3,7 +3,6 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { select } from 'react-select-event'; -import { dateTime, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { PrometheusDatasource } from '../datasource'; @@ -376,24 +375,4 @@ describe('PromVariableQueryEditor', () => { qryType: 5, }); }); - - test('Calls language provider with the time range received in props', async () => { - const now = dateTime('2023-09-16T21:26:00Z'); - const range: TimeRange = { - from: dateTime(now).subtract(2, 'days'), - to: now, - raw: { - from: 'now-2d', - to: 'now', - }, - }; - props.range = range; - - const languageProviderStartMock = jest.fn(); - props.datasource.languageProvider.start = languageProviderStartMock; - - render(); - - expect(languageProviderStartMock).toHaveBeenCalledWith(range); - }); }); diff --git a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx index d8f99a7bc48..2ec81339219 100644 --- a/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx +++ b/packages/grafana-prometheus/src/components/VariableQueryEditor.tsx @@ -77,11 +77,6 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: // label filters have been added as a filter for metrics in label values query type const [labelFilters, setLabelFilters] = useState([]); - useEffect(() => { - datasource.languageProvider.start(range); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - useEffect(() => { if (!query) { return; diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index b94bb4cded3..f67ac0e3e9f 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -311,10 +311,6 @@ } }, "querybuilder": { - "additional-settings": { - "content-filter-metric-names-regex-search-using": "Filter metric names by regex search, using an additional call on the Prometheus API.", - "disable-text-wrap": "Disable text wrap" - }, "feedback-link": { "give-feedback": "Give feedback", "title-give-feedback": "The metrics explorer is new, please let us know how we can improve it" @@ -328,9 +324,6 @@ }, "get-placeholders": { "browse": "Search metrics by name", - "include-null-metadata": "Include results with no metadata", - "metadata-search-switch": "Include description in search", - "set-use-backend": "Enable regex search", "type": "Filter by type" }, "get-prom-types": { @@ -378,15 +371,9 @@ "tooltip-metric": "Optional: returns a list of label values for the label name in the specified metric." }, "metrics-modal": { - "additional-settings": "Additional Settings", - "aria-label-additional-settings": "Additional settings", "aria-label-browse-metrics": "Browse metrics", "currently-selected": "Currently selected: {{selected}}", "metrics-pre-filtered": "These metrics have been pre-filtered by labels chosen in the label filters.", - "placeholder-results-per-page": "results per page", - "results-amount_one": "Showing {{num}} of {{count}} results", - "results-amount_other": "Showing {{num}} of {{count}} results", - "results-per-page": "Results per page", "title-metrics-explorer": "Metrics explorer" }, "nested-query": { @@ -500,7 +487,6 @@ "message-expand-search": "There are no metrics found. Try to expand your search and filters.", "message-no-metrics-found": "There are no metrics found in the data source.", "name": "Name", - "select": "Select", "type": "Type" }, "update-function-args": { diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx index 21a88427159..eaafcb1aa68 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricCombobox.tsx @@ -11,9 +11,8 @@ import { PrometheusDatasource } from '../../datasource'; import { QueryBuilderLabelFilter } from '../shared/types'; import { PromVisualQuery } from '../types'; +import { formatKeyValueStrings } from './formatter'; import { MetricsModal } from './metrics-modal/MetricsModal'; -import { tracking } from './metrics-modal/state/helpers'; -import { formatKeyValueStrings } from './shared/formatter'; export interface MetricComboboxProps { metricLookupDisabled: boolean; @@ -76,18 +75,6 @@ export function MetricCombobox({ [getMetricLabels, onGetMetrics] ); - const loadMetricsExplorerMetrics = useCallback(async () => { - const allMetrics = await onGetMetrics(); - const metrics: string[] = []; - for (const metric of allMetrics) { - if (metric.value) { - metrics.push(metric.value); - } - } - - return metrics; - }, [onGetMetrics]); - const asyncSelect = () => { return ( @@ -115,10 +102,7 @@ export function MetricCombobox({ )} variant="secondary" icon="book-open" - onClick={() => { - tracking('grafana_prometheus_metric_encyclopedia_open', null, '', query); - setMetricsModalOpen(true); - }} + onClick={() => setMetricsModalOpen(true)} /> ); @@ -133,7 +117,6 @@ export function MetricCombobox({ onClose={() => setMetricsModalOpen(false)} query={query} onChange={onChange} - initialMetrics={loadMetricsExplorerMetrics} timeRange={timeRange} /> )} diff --git a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx index 8ac17b1b854..daa84a83c62 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/NestedQuery.tsx @@ -11,7 +11,7 @@ import { PrometheusDatasource } from '../../datasource'; import { binaryScalarDefs } from '../binaryScalarOperations'; import { PromVisualQueryBinary } from '../types'; -import { QueryBuilderContent } from './shared/QueryBuilderContent'; +import { QueryBuilderContent } from './QueryBuilderContent'; interface NestedQueryProps { nestedQuery: PromVisualQueryBinary; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index 03bf97718bc..9504e50b5c0 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -6,7 +6,8 @@ import { PanelData } from '@grafana/data'; import { PrometheusDatasource } from '../../datasource'; import { PromVisualQuery } from '../types'; -import { BaseQueryBuilder } from './shared/BaseQueryBuilder'; +import { NestedQueryList } from './NestedQueryList'; +import { QueryBuilderContent } from './QueryBuilderContent'; interface PromQueryBuilderProps { query: PromVisualQuery; @@ -18,7 +19,22 @@ interface PromQueryBuilderProps { } export const PromQueryBuilder = memo((props) => { - return ; + const { query, datasource, onChange, onRunQuery, showExplain } = props; + + return ( + <> + + {query.binaryQueries && query.binaryQueries.length > 0 && ( + + )} + + ); }); PromQueryBuilder.displayName = 'PromQueryBuilder'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx index b325da19d45..3e400f13a50 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.test.tsx @@ -20,12 +20,8 @@ describe('PromQueryBuilderContainer', () => { await addOperationInQueryBuilder('Range functions', 'Rate'); // extra fields here are for storing metrics explorer settings. Future work: store these in local storage. expect(props.onChange).toHaveBeenCalledWith({ - disableTextWrap: false, expr: 'rate(metric_test{job="testjob"}[$__rate_interval])', - fullMetaSearch: false, - includeNullMetadata: true, refId: 'A', - useBackend: false, }); }); diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx index 318d4a71213..31d3b6a6274 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilderContainer.tsx @@ -1,6 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/PromQueryBuilderContainer.tsx -import { createSlice, PayloadAction } from '@reduxjs/toolkit'; -import { useEffect, useReducer } from 'react'; +import { useEffect, useState } from 'react'; import { PanelData } from '@grafana/data'; @@ -12,7 +11,6 @@ import { PromVisualQuery } from '../types'; import { PromQueryBuilder } from './PromQueryBuilder'; import { QueryPreview } from './QueryPreview'; -import { getSettings, MetricsModalSettings } from './metrics-modal/state/state'; interface PromQueryBuilderContainerProps { query: PromQuery; @@ -23,7 +21,7 @@ interface PromQueryBuilderContainerProps { showExplain: boolean; } -interface State { +interface RenderedQuery { visQuery?: PromVisualQuery; expr: string; } @@ -33,80 +31,38 @@ interface State { */ export function PromQueryBuilderContainer(props: PromQueryBuilderContainerProps) { const { query, onChange, onRunQuery, datasource, data, showExplain } = props; - const [state, dispatch] = useReducer(stateSlice.reducer, { expr: query.expr }); - // Only rebuild visual query if expr changes from outside - useEffect(() => { - dispatch(exprChanged(query.expr)); - dispatch( - setMetricsModalSettings({ - useBackend: query.useBackend ?? false, - disableTextWrap: query.disableTextWrap ?? false, - fullMetaSearch: query.fullMetaSearch ?? false, - includeNullMetadata: query.includeNullMetadata ?? true, - }) - ); - }, [query]); + const [rendered, setRendered] = useState({ expr: query.expr }); useEffect(() => { - datasource.languageProvider.start(data?.timeRange); - }, [data?.timeRange, datasource.languageProvider]); + // Only rebuild visual query if expr changes from outside + if (!rendered.visQuery || rendered.expr !== query.expr) { + const parseResult = buildVisualQueryFromString(query.expr ?? ''); + + setRendered({ expr: query.expr, visQuery: parseResult.query }); + } + }, [query, rendered]); const onVisQueryChange = (visQuery: PromVisualQuery) => { const expr = promQueryModeller.renderQuery(visQuery); - dispatch(visualQueryChange({ visQuery, expr })); - - const metricsModalSettings = getSettings(visQuery); - onChange({ ...props.query, expr: expr, ...metricsModalSettings }); + setRendered({ expr, visQuery }); + onChange({ ...props.query, expr }); }; - if (!state.visQuery) { + if (!rendered.visQuery) { return null; } return ( <> - {} + ); } - -const initialState: State = { - expr: '', -}; - -const stateSlice = createSlice({ - name: 'prom-builder-container', - initialState, - reducers: { - visualQueryChange: (state, action: PayloadAction<{ visQuery: PromVisualQuery; expr: string }>) => { - state.expr = action.payload.expr; - state.visQuery = action.payload.visQuery; - }, - exprChanged: (state, action: PayloadAction) => { - if (!state.visQuery || state.expr !== action.payload) { - state.expr = action.payload; - const parseResult = buildVisualQueryFromString(action.payload ?? ''); - - state.visQuery = parseResult.query; - } - }, - setMetricsModalSettings: (state, action: PayloadAction) => { - if (state.visQuery) { - state.visQuery.useBackend = action.payload.useBackend; - state.visQuery.disableTextWrap = action.payload.disableTextWrap; - state.visQuery.fullMetaSearch = action.payload.fullMetaSearch; - state.visQuery.includeNullMetadata = action.payload.includeNullMetadata; - } - }, - }, -}); - -const { visualQueryChange, exprChanged, setMetricsModalSettings } = stateSlice.actions; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx b/packages/grafana-prometheus/src/querybuilder/components/QueryBuilderContent.tsx similarity index 67% rename from packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx rename to packages/grafana-prometheus/src/querybuilder/components/QueryBuilderContent.tsx index ce00f36cbb9..b04537da808 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/shared/QueryBuilderContent.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/QueryBuilderContent.tsx @@ -1,28 +1,37 @@ import { css } from '@emotion/css'; import { memo, useState } from 'react'; -import { DataSourceApi, getDefaultTimeRange } from '@grafana/data'; +import { DataSourceApi, getDefaultTimeRange, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorRow } from '@grafana/plugin-ui'; -import { promqlGrammar } from '../../../promql'; -import { getInitHints } from '../../../query_hints'; -import { buildVisualQueryFromString } from '../../parsing'; -import { OperationExplainedBox } from '../../shared/OperationExplainedBox'; -import { OperationList } from '../../shared/OperationList'; -import { OperationListExplained } from '../../shared/OperationListExplained'; -import { OperationsEditorRow } from '../../shared/OperationsEditorRow'; -import { QueryBuilderHints } from '../../shared/QueryBuilderHints'; -import { RawQuery } from '../../shared/RawQuery'; -import { promQueryModeller } from '../../shared/modeller_instance'; -import { QueryBuilderOperation } from '../../shared/types'; -import { PromVisualQuery } from '../../types'; -import { MetricsLabelsSection } from '../MetricsLabelsSection'; -import { EXPLAIN_LABEL_FILTER_CONTENT } from '../PromQueryBuilderExplained'; +import { PrometheusDatasource } from '../../datasource'; +import { promqlGrammar } from '../../promql'; +import { getInitHints } from '../../query_hints'; +import { buildVisualQueryFromString } from '../parsing'; +import { OperationExplainedBox } from '../shared/OperationExplainedBox'; +import { OperationList } from '../shared/OperationList'; +import { OperationListExplained } from '../shared/OperationListExplained'; +import { OperationsEditorRow } from '../shared/OperationsEditorRow'; +import { QueryBuilderHints } from '../shared/QueryBuilderHints'; +import { RawQuery } from '../shared/RawQuery'; +import { promQueryModeller } from '../shared/modeller_instance'; +import { QueryBuilderOperation } from '../shared/types'; +import { PromVisualQuery } from '../types'; -import { BaseQueryBuilderProps } from './types'; +import { MetricsLabelsSection } from './MetricsLabelsSection'; +import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained'; -export const QueryBuilderContent = memo((props) => { +interface QueryBuilderContentProps { + query: PromVisualQuery; + datasource: PrometheusDatasource; + onChange: (update: PromVisualQuery) => void; + onRunQuery: () => void; + data?: PanelData; + showExplain: boolean; +} + +export const QueryBuilderContent = memo((props) => { const { datasource, query, onChange, onRunQuery, data, showExplain } = props; const [highlightedOp, setHighlightedOp] = useState(); diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/formatter.ts b/packages/grafana-prometheus/src/querybuilder/components/formatter.ts similarity index 81% rename from packages/grafana-prometheus/src/querybuilder/components/shared/formatter.ts rename to packages/grafana-prometheus/src/querybuilder/components/formatter.ts index baf8228c573..bb795745816 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/shared/formatter.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/formatter.ts @@ -1,7 +1,7 @@ -import { regexifyLabelValuesQueryString } from '../../parsingUtils'; -import { QueryBuilderLabelFilter } from '../../shared/types'; +import { regexifyLabelValuesQueryString } from '../parsingUtils'; +import { QueryBuilderLabelFilter } from '../shared/types'; -export const formatPrometheusLabelFiltersToString = ( +const formatPrometheusLabelFiltersToString = ( queryString: string, labelsFilters: QueryBuilderLabelFilter[] | undefined ): string => { diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx deleted file mode 100644 index 4ce04117dd9..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/AdditionalSettings.tsx +++ /dev/null @@ -1,87 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/AdditionalSettings.tsx -import { css } from '@emotion/css'; - -import { GrafanaTheme2 } from '@grafana/data'; -import { Trans, t } from '@grafana/i18n'; -import { Icon, Switch, Tooltip, useTheme2 } from '@grafana/ui'; - -import { metricsModaltestIds } from './shared/testIds'; -import { AdditionalSettingsProps } from './shared/types'; -import { getPlaceholders } from './state/helpers'; - -export function AdditionalSettings(props: AdditionalSettingsProps) { - const { state, onChangeFullMetaSearch, onChangeIncludeNullMetadata, onChangeDisableTextWrap, onChangeUseBackend } = - props; - - const theme = useTheme2(); - const styles = getStyles(theme); - - const placeholders = getPlaceholders(); - - return ( - <> -
- onChangeFullMetaSearch()} - /> -
{placeholders.metadataSearchSwitch}
-
-
- onChangeIncludeNullMetadata()} - /> -
{placeholders.includeNullMetadata}
-
-
- onChangeDisableTextWrap()} /> -
- - Disable text wrap - -
-
-
- onChangeUseBackend()} - /> -
{placeholders.setUseBackend} 
- - - -
- - ); -} - -function getStyles(theme: GrafanaTheme2) { - return { - settingsIcon: css({ - color: theme.colors.text.secondary, - }), - selectItem: css({ - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - padding: '4px 0', - }), - selectItemLabel: css({ - margin: `0 0 0 ${theme.spacing(1)}`, - alignSelf: 'center', - color: theme.colors.text.secondary, - fontSize: '12px', - }), - }; -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.test.tsx index c85089657cb..f57fbb59bd6 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.test.tsx @@ -12,7 +12,7 @@ import { PromOptions } from '../../../types'; import { PromVisualQuery } from '../../types'; import { MetricsModal } from './MetricsModal'; -import { metricsModaltestIds } from './shared/testIds'; +import { metricsModaltestIds } from './testIds'; // don't care about interaction tracking in our unit tests jest.mock('@grafana/runtime', () => ({ @@ -117,28 +117,6 @@ describe('MetricsModal', () => { }); }); - it('shows results metrics per page chosen by the user', async () => { - setup(defaultQuery, listOfMetrics); - const resultsPerPageInput = screen.getByTestId(metricsModaltestIds.resultsPerPage); - await userEvent.type(resultsPerPageInput, '12'); - const metricInsideRange = screen.getByText('j'); - expect(metricInsideRange).toBeInTheDocument(); - }); - - it('paginates lots of metrics and does not run out of memory', async () => { - const lotsOfMetrics: string[] = [...Array(100000).keys()].map((i) => '' + i); - setup(defaultQuery, lotsOfMetrics); - await waitFor(() => { - // doesn't break on loading - expect(screen.getByText('0')).toBeInTheDocument(); - }); - const resultsPerPageInput = screen.getByTestId(metricsModaltestIds.resultsPerPage); - // doesn't break on changing results per page - await userEvent.type(resultsPerPageInput, '11'); - const metricInsideRange = screen.getByText('9'); - expect(metricInsideRange).toBeInTheDocument(); - }); - // Fuzzy search it('searches and filter by metric name with a fuzzy search', async () => { // search for a_bucket by name @@ -171,14 +149,6 @@ describe('MetricsModal', () => { expect(metricABucket).toBeInTheDocument(); }); - const showSettingsButton = screen.getByTestId(metricsModaltestIds.showAdditionalSettings); - expect(showSettingsButton).toBeInTheDocument(); - await userEvent.click(showSettingsButton); - - const metadataSwitch = screen.getByTestId(metricsModaltestIds.searchWithMetadata); - expect(metadataSwitch).toBeInTheDocument(); - await userEvent.click(metadataSwitch); - const searchMetric = screen.getByTestId(metricsModaltestIds.searchMetric); expect(searchMetric).toBeInTheDocument(); await userEvent.type(searchMetric, 'functions'); @@ -248,6 +218,34 @@ function createDatasource(withLabels?: boolean) { const languageProvider = new EmptyLanguageProviderMock() as unknown as PrometheusLanguageProviderInterface; // display different results if their labels are selected in the PromVisualQuery + if (withLabels) { + languageProvider.queryMetricsMetadata = jest.fn().mockResolvedValue({ + 'with-labels': { + type: 'with-labels-type', + help: 'with-labels-help', + }, + }); + } else { + // all metrics - create metadata for all metrics in listOfMetrics + const mockMetadata: Record = {}; + listOfMetrics.forEach((metric) => { + if (metric === 'all-metrics') { + mockMetadata[metric] = { type: 'all-metrics-type', help: 'all-metrics-help' }; + } else if (metric === 'a_bucket') { + mockMetadata[metric] = { type: 'histogram', help: 'for functions' }; + } else if (metric === 'new_histogram') { + mockMetadata[metric] = { type: 'histogram', help: 'a native histogram' }; + } else if (metric === 'a') { + mockMetadata[metric] = { type: 'counter', help: 'a-metric-help' }; + } else { + mockMetadata[metric] = { type: 'counter', help: `${metric} metric help` }; + } + }); + + languageProvider.queryMetricsMetadata = jest.fn().mockResolvedValue(mockMetadata); + } + + // Also mock the retrieveMetricsMetadata method that might be used elsewhere if (withLabels) { languageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({ 'with-labels': { @@ -256,26 +254,23 @@ function createDatasource(withLabels?: boolean) { }, }); } else { - // all metrics - languageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({ - 'all-metrics': { - type: 'all-metrics-type', - help: 'all-metrics-help', - }, - a: { - type: 'counter', - help: 'a-metric-help', - }, - a_bucket: { - type: 'histogram', - help: 'for functions', - }, - new_histogram: { - type: 'histogram', - help: 'a native histogram', - }, - // missing metadata for other metrics is tested for, see below + // Create the same metadata structure for retrieveMetricsMetadata + const mockMetadata: Record = {}; + listOfMetrics.forEach((metric) => { + if (metric === 'all-metrics') { + mockMetadata[metric] = { type: 'all-metrics-type', help: 'all-metrics-help' }; + } else if (metric === 'a_bucket') { + mockMetadata[metric] = { type: 'histogram', help: 'for functions' }; + } else if (metric === 'new_histogram') { + mockMetadata[metric] = { type: 'histogram', help: 'a native histogram' }; + } else if (metric === 'a') { + mockMetadata[metric] = { type: 'counter', help: 'a-metric-help' }; + } else { + mockMetadata[metric] = { type: 'counter', help: `${metric} metric help` }; + } }); + + languageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue(mockMetadata); } const datasource = new PrometheusDatasource( diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx index 0722489b771..59c4c703ccf 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModal.tsx @@ -1,107 +1,49 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/MetricsModal.tsx import { cx } from '@emotion/css'; -import debounce from 'debounce-promise'; -import { useCallback, useEffect, useMemo, useReducer } from 'react'; -import { SelectableValue } from '@grafana/data'; +import { SelectableValue, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { t, Trans } from '@grafana/i18n'; -import { - Button, - ButtonGroup, - Icon, - Input, - Modal, - MultiSelect, - Pagination, - Spinner, - Toggletip, - useTheme2, -} from '@grafana/ui'; +import { Icon, Input, Modal, MultiSelect, Pagination, Spinner, useStyles2 } from '@grafana/ui'; -import { getDebounceTimeInMilliseconds } from '../../../caching'; -import { METRIC_LABEL } from '../../../constants'; -import { regexifyLabelValuesQueryString } from '../../parsingUtils'; -import { formatPrometheusLabelFilters } from '../shared/formatter'; +import { PrometheusDatasource } from '../../../datasource'; +import { PromVisualQuery } from '../../types'; -import { AdditionalSettings } from './AdditionalSettings'; import { FeedbackLink } from './FeedbackLink'; +import { MetricsModalContextProvider, useMetricsModal } from './MetricsModalContext'; import { ResultsTable } from './ResultsTable'; -import { metricsModaltestIds } from './shared/testIds'; -import { MetricsModalProps } from './shared/types'; -import { - calculatePageList, - calculateResultsPerPage, - displayedMetrics, - getPlaceholders, - getPromTypes, - setMetrics, - tracking, -} from './state/helpers'; -import { - buildMetrics, - DEFAULT_RESULTS_PER_PAGE, - filterMetricsBackend, - initialState, - MAXIMUM_RESULTS_PER_PAGE, - MetricsModalMetadata, - setDisableTextWrap, - setFullMetaSearch, - setFuzzySearchQuery, - setIncludeNullMetadata, - setIsLoading, - setMetaHaystack, - setNameHaystack, - setPageNum, - setResultsPerPage, - setSelectedTypes, - setUseBackend, - showAdditionalSettings, - stateSlice, -} from './state/state'; -import { getStyles } from './styles'; +import { getPlaceholders, getPromTypes } from './helpers'; +import { getMetricsModalStyles } from './styles'; +import { metricsModaltestIds } from './testIds'; import { PromFilterOption } from './types'; -import { debouncedFuzzySearch } from './uFuzzy'; -export const MetricsModal = (props: MetricsModalProps) => { - const { datasource, isOpen, onClose, onChange, query, initialMetrics, timeRange } = props; +interface MetricsModalProps { + datasource: PrometheusDatasource; + timeRange: TimeRange; + isOpen: boolean; + query: PromVisualQuery; + onClose: () => void; + onChange: (query: PromVisualQuery) => void; +} - const [state, dispatch] = useReducer(stateSlice.reducer, initialState(query)); +const MetricsModalContent = (props: MetricsModalProps) => { + const { isOpen, onClose, onChange, query, timeRange } = props; - const theme = useTheme2(); - const styles = getStyles(theme, state.disableTextWrap); + const { + isLoading, + filteredMetricsData, + debouncedBackendSearch, + pagination, + setPagination, + selectedTypes, + setSelectedTypes, + searchedText, + setSearchedText, + } = useMetricsModal(); + const styles = useStyles2(getMetricsModalStyles); const placeholders = getPlaceholders(); const promTypes = getPromTypes(); - /** - * loads metrics and metadata on opening modal and switching off useBackend - */ - const updateMetricsMetadata = useCallback(async () => { - // *** Loading Gif - dispatch(setIsLoading(true)); - - // Because Combobox in MetricsCombobox doesn't use the same lifecycle as Select to open the Metrics Explorer - // it might not have loaded any metrics yet, so it instead passes in an async function to get the metrics - const metrics = typeof initialMetrics === 'function' ? await initialMetrics() : initialMetrics; - - const data: MetricsModalMetadata = await setMetrics(datasource, query, metrics); - 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]); - const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => { return { value: t.value, @@ -110,96 +52,10 @@ export const MetricsModal = (props: MetricsModalProps) => { }; }); - /** - * The backend debounced search - */ - const debouncedBackendSearch = useMemo( - () => - debounce(async (metricText: string) => { - dispatch(setIsLoading(true)); - - const queryString = regexifyLabelValuesQueryString(metricText); - const filterArray = query.labels ? formatPrometheusLabelFilters(query.labels) : []; - const match = `{__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}}`; - - const results = await datasource.languageProvider.queryLabelValues(timeRange, METRIC_LABEL, match); - - const resultsOptions = results.map((result) => ({ - value: result, - })); - - dispatch( - filterMetricsBackend({ - metrics: resultsOptions, - filteredMetricCount: resultsOptions.length, - isLoading: false, - }) - ); - }, getDebounceTimeInMilliseconds(datasource.cacheLevel)), - [datasource.cacheLevel, datasource.languageProvider, query.labels, timeRange] - ); - - function fuzzyNameDispatch(haystackData: string[][]) { - dispatch(setNameHaystack(haystackData)); - } - - function fuzzyMetaDispatch(haystackData: string[][]) { - dispatch(setMetaHaystack(haystackData)); - } - - function searchCallback(query: string, fullMetaSearchVal: boolean) { - if (state.useBackend && query === '') { - // get all metrics data if a user erases everything in the input - updateMetricsMetadata(); - } else if (state.useBackend) { - debouncedBackendSearch(query); - } else { - // search either the names or all metadata - // fuzzy search go! - if (fullMetaSearchVal) { - debouncedFuzzySearch(Object.keys(state.metaHaystackDictionary), query, fuzzyMetaDispatch); - } else { - debouncedFuzzySearch(Object.keys(state.nameHaystackDictionary), query, fuzzyNameDispatch); - } - } - } - - /* Settings switches */ - const additionalSettings = ( - { - 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, ''); - }} - 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(); - } else { - // check if there is text in the browse search and update - if (state.fuzzySearchQuery !== '') { - debouncedBackendSearch(state.fuzzySearchQuery); - } - // otherwise wait for user typing - } - }} - /> - ); + const searchCallback = (query: string, fullMetaSearchVal?: boolean) => { + setSearchedText(query); + debouncedBackendSearch(timeRange, query); + }; return ( { autoFocus={true} data-testid={metricsModaltestIds.searchMetric} placeholder={placeholders.browse} - value={state.fuzzySearchQuery} + value={searchedText} onInput={(e) => { const value = e.currentTarget.value ?? ''; - dispatch(setFuzzySearchQuery(value)); - searchCallback(value, state.fullMetaSearch); + setSearchedText(value); + setPagination({ ...pagination, pageNum: 1 }); + searchCallback(value); }} /> - {state.hasMetadata && ( -
- dispatch(setSelectedTypes(v))} - /> -
- )} -
- -
- - - -
+
+
@@ -298,61 +123,23 @@ export const MetricsModal = (props: MetricsModalProps) => { )}
- {state.metrics && ( - - )} + {filteredMetricsData && }
-
- - Showing {'{{num}}'} of {'{{count}}'} results - -
{ - const page = val ?? 1; - dispatch(setPageNum(page)); - }} + currentPage={pagination.pageNum > pagination.totalPageNum ? 1 : pagination.pageNum} + numberOfPages={pagination.totalPageNum} + onNavigate={(val: number) => setPagination({ ...pagination, pageNum: val ?? 1 })} /> -
-

- Results per page -

- { - const value = +e.currentTarget.value; - - if (isNaN(value) || value >= MAXIMUM_RESULTS_PER_PAGE) { - return; - } - - dispatch(setResultsPerPage(value)); - }} - /> -
); }; + +export const MetricsModal = (props: MetricsModalProps) => { + return ( + + + + ); +}; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.test.tsx new file mode 100644 index 00000000000..955b2c1b585 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.test.tsx @@ -0,0 +1,329 @@ +import { act, render, renderHook, waitFor } from '@testing-library/react'; +import { ReactNode } from 'react'; + +import { TimeRange } from '@grafana/data'; + +import { PrometheusLanguageProviderInterface } from '../../../language_provider'; + +import { DEFAULT_RESULTS_PER_PAGE, MetricsModalContextProvider, useMetricsModal } from './MetricsModalContext'; +import { generateMetricData } from './helpers'; + +// Mock dependencies +jest.mock('./helpers', () => ({ + generateMetricData: jest.fn(), +})); + +const mockGenerateMetricData = generateMetricData as jest.MockedFunction; + +// Mock language provider +const mockLanguageProvider: PrometheusLanguageProviderInterface = { + queryMetricsMetadata: jest.fn(), + queryLabelValues: jest.fn(), + retrieveMetricsMetadata: jest.fn(), +} as unknown as PrometheusLanguageProviderInterface; + +// Helper to create wrapper component +const createWrapper = (languageProvider = mockLanguageProvider) => { + return ({ children }: { children: ReactNode }) => ( + {children} + ); +}; + +// Sample time range for tests +const defaultTimeRange: TimeRange = { + from: 'now-1h' as unknown as TimeRange['from'], + to: 'now' as unknown as TimeRange['to'], + raw: { + from: 'now-1h', + to: 'now', + }, +}; + +describe('MetricsModalContext', () => { + let consoleSpy: jest.SpyInstance; + + beforeEach(() => { + jest.clearAllMocks(); + // Mock console.error to suppress React act() warnings + consoleSpy = jest.spyOn(console, 'error').mockImplementation(() => {}); + + // Default mock implementations + mockGenerateMetricData.mockImplementation((metric) => ({ + value: metric, + type: 'counter', + description: 'Test metric', + })); + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({ + test_metric: { type: 'counter', help: 'Test metric' }, + }); + }); + + afterEach(() => { + consoleSpy.mockRestore(); + }); + + describe('useMetricsModal hook', () => { + it('should throw error when used outside provider', () => { + expect(() => { + renderHook(() => useMetricsModal()); + }).toThrow('useMetricsModal must be used within a MetricsModalContextProvider'); + }); + + it('should provide context value when used within provider', () => { + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + expect(result.current).toBeDefined(); + expect(result.current.isLoading).toBe(true); // Initially loading + expect(result.current.filteredMetricsData).toEqual([]); + expect(result.current.pagination).toEqual({ + pageNum: 1, + totalPageNum: 1, + resultsPerPage: DEFAULT_RESULTS_PER_PAGE, + }); + expect(result.current.selectedTypes).toEqual([]); + expect(result.current.searchedText).toBe(''); + }); + }); + + describe('State management', () => { + it('should update pagination', () => { + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + const expectedPagination = { pageNum: 1, resultsPerPage: 50, totalPageNum: 1 }; + + act(() => { + result.current.setPagination({ pageNum: 2, resultsPerPage: 50, totalPageNum: 3 }); + }); + + expect(result.current.pagination).toEqual(expectedPagination); + }); + + it('should update selected types', () => { + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + const newTypes = [{ value: 'counter', label: 'Counter' }]; + + act(() => { + result.current.setSelectedTypes(newTypes); + }); + + expect(result.current.selectedTypes).toEqual(newTypes); + }); + + it('should update searched text', () => { + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.setSearchedText('test_metric'); + }); + + expect(result.current.searchedText).toBe('test_metric'); + }); + + it('should update loading state', () => { + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + act(() => { + result.current.setIsLoading(false); + }); + + expect(result.current.isLoading).toBe(false); + }); + }); + + describe('Metadata fetching', () => { + it('should load initial metadata on mount', async () => { + const mockMetadata = { + cpu_usage: { type: 'gauge', help: 'CPU usage percentage' }, + memory_usage: { type: 'gauge', help: 'Memory usage bytes' }, + }; + + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue(mockMetadata); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + // Wait for metadata to load + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(mockLanguageProvider.queryMetricsMetadata).toHaveBeenCalledWith(1000); + expect(mockGenerateMetricData).toHaveBeenCalledWith('cpu_usage', mockLanguageProvider); + expect(mockGenerateMetricData).toHaveBeenCalledWith('memory_usage', mockLanguageProvider); + expect(result.current.filteredMetricsData).toHaveLength(2); + }); + + it('should handle empty metadata response', async () => { + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({}); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.filteredMetricsData).toEqual([]); + }); + + it('should handle metadata fetch error', async () => { + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockRejectedValue(new Error('Network error')); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.filteredMetricsData).toEqual([]); + }); + }); + + describe('Backend search', () => { + it('should perform backend search with results', async () => { + (mockLanguageProvider.queryLabelValues as jest.Mock).mockResolvedValue(['test_metric', 'other_metric']); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await act(async () => { + await result.current.debouncedBackendSearch(defaultTimeRange, 'test'); + }); + + expect(mockLanguageProvider.queryLabelValues).toHaveBeenCalledWith( + defaultTimeRange, + '__name__', + '{__name__=~"(?i).*test.*"}' + ); + expect(result.current.filteredMetricsData).toHaveLength(1); + }); + + it('should handle backend search error', async () => { + (mockLanguageProvider.queryLabelValues as jest.Mock).mockRejectedValue(new Error('Search failed')); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await act(async () => { + await result.current.debouncedBackendSearch(defaultTimeRange, 'test'); + }); + + expect(result.current.filteredMetricsData).toEqual([]); + expect(result.current.isLoading).toBe(false); + }); + }); + + describe('Filtering logic', () => { + it('should return all metrics when no types are selected', async () => { + mockGenerateMetricData.mockImplementation((metric) => ({ + value: metric, + type: 'counter', + description: 'Test metric', + })); + + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({ + test_metric: { type: 'counter', help: 'Test metric' }, + }); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + expect(result.current.filteredMetricsData).toHaveLength(1); + expect(result.current.selectedTypes).toEqual([]); + }); + + it('should filter metrics by selected type', async () => { + mockGenerateMetricData.mockImplementation((metric) => ({ + value: metric, + type: metric === 'counter_metric' ? 'counter' : 'gauge', + description: 'Test metric', + })); + + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({ + counter_metric: { type: 'counter', help: 'Counter metric' }, + gauge_metric: { type: 'gauge', help: 'Gauge metric' }, + }); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + act(() => { + result.current.setSelectedTypes([{ value: 'counter', label: 'Counter' }]); + }); + + expect(result.current.filteredMetricsData).toHaveLength(1); + expect(result.current.filteredMetricsData[0].value).toBe('counter_metric'); + }); + + it('should handle metrics without type when "no type" is selected', async () => { + mockGenerateMetricData.mockImplementation((metric) => ({ + value: metric, + type: metric === 'no_type_metric' ? undefined : 'counter', + description: 'Test metric', + })); + + (mockLanguageProvider.queryMetricsMetadata as jest.Mock).mockResolvedValue({ + counter_metric: { type: 'counter', help: 'Counter metric' }, + no_type_metric: { help: 'Metric without type' }, + }); + + const { result } = renderHook(() => useMetricsModal(), { + wrapper: createWrapper(), + }); + + await waitFor(() => { + expect(result.current.isLoading).toBe(false); + }); + + act(() => { + result.current.setSelectedTypes([{ value: 'no type', label: 'No Type' }]); + }); + + expect(result.current.filteredMetricsData).toHaveLength(1); + expect(result.current.filteredMetricsData[0].value).toBe('no_type_metric'); + }); + }); + + describe('Component integration', () => { + it('should render provider without errors', () => { + const TestComponent = () => { + return
frontend
; + }; + + const { getByTestId } = render( + + + + ); + + expect(getByTestId('test')).toHaveTextContent('frontend'); + }); + }); +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.tsx new file mode 100644 index 00000000000..3361b448547 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/MetricsModalContext.tsx @@ -0,0 +1,201 @@ +import debounce from 'debounce-promise'; +import { + createContext, + FC, + PropsWithChildren, + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; + +import { SelectableValue, TimeRange } from '@grafana/data'; + +import { METRIC_LABEL, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from '../../../constants'; +import { PrometheusLanguageProviderInterface } from '../../../language_provider'; +import { regexifyLabelValuesQueryString } from '../../parsingUtils'; +import { QueryBuilderLabelFilter } from '../../shared/types'; +import { formatPrometheusLabelFilters } from '../formatter'; + +import { generateMetricData } from './helpers'; +import { MetricData, MetricsData } from './types'; +import { fuzzySearch } from './uFuzzy'; + +export const DEFAULT_RESULTS_PER_PAGE = 25; + +type Pagination = { + pageNum: number; + resultsPerPage: number; + totalPageNum: number; +}; + +type MetricsModalContextValue = { + isLoading: boolean; + setIsLoading: (val: boolean) => void; + filteredMetricsData: MetricData[]; + debouncedBackendSearch: ( + timeRange: TimeRange, + metricText: string, + queryLabels?: QueryBuilderLabelFilter[] + ) => Promise; + pagination: Pagination; + setPagination: (val: Pagination) => void; + selectedTypes: Array>; + setSelectedTypes: (val: Array>) => void; + searchedText: string; + setSearchedText: (val: string) => void; +}; + +const MetricsModalContext = createContext(undefined); + +type MetricsModalContextProviderProps = { + languageProvider: PrometheusLanguageProviderInterface; +}; + +export const MetricsModalContextProvider: FC> = ({ + children, + languageProvider, +}) => { + const [isLoading, setIsLoading] = useState(true); + const [metricsData, setMetricsData] = useState([]); + const [pagination, setPagination] = useState({ + pageNum: 1, + totalPageNum: 1, + resultsPerPage: DEFAULT_RESULTS_PER_PAGE, + }); + const [selectedTypes, setSelectedTypes] = useState>>([]); + const [searchedText, setSearchedText] = useState(''); + + const filteredMetricsData = useMemo(() => { + if (selectedTypes.length === 0) { + return metricsData; + } + + // Filter metrics based on selected types + return metricsData.filter((metric: MetricData) => { + return selectedTypes.some((selectedType) => { + // Handle metrics with defined types + if (metric.type && selectedType.value) { + return metric.type.includes(selectedType.value); + } + + // Handle metrics without type when "no type" is selected + if (!metric.type && selectedType.value === 'no type') { + return true; + } + + return false; + }); + }); + }, [metricsData, selectedTypes]); + + useEffect(() => { + const totalPageNum = + filteredMetricsData.length === 0 ? 1 : Math.ceil(filteredMetricsData.length / pagination.resultsPerPage); + const pageNum = pagination.pageNum > totalPageNum ? 1 : pagination.pageNum; + + setPagination((prevPagination) => ({ + ...prevPagination, + totalPageNum, + pageNum, + })); + }, [filteredMetricsData.length, pagination.resultsPerPage, pagination.pageNum]); + + // Track the latest search ID to handle race conditions + const latestSearchIdRef = useRef(0); + + const fetchMetadata = useCallback(async () => { + try { + setIsLoading(true); + const metadata = await languageProvider.queryMetricsMetadata(PROMETHEUS_QUERY_BUILDER_MAX_RESULTS); + + if (Object.keys(metadata).length === 0) { + setMetricsData([]); + } else { + const processedData = Object.keys(metadata).map((m) => generateMetricData(m, languageProvider)); + setMetricsData(processedData); + } + } catch (error) { + setMetricsData([]); + } finally { + setIsLoading(false); + } + }, [languageProvider]); + + const debouncedBackendSearch = useMemo( + () => + debounce(async (timeRange: TimeRange, metricText: string, queryLabels?: QueryBuilderLabelFilter[]) => { + // Generate unique search ID to handle race conditions + const searchId = ++latestSearchIdRef.current; + + try { + if (metricText === '') { + await fetchMetadata(); + return; + } + + setIsLoading(true); + + const queryString = regexifyLabelValuesQueryString(metricText); + const filterArray = queryLabels ? formatPrometheusLabelFilters(queryLabels) : []; + const match = `{__name__=~"(?i).*${queryString}"${filterArray ? filterArray.join('') : ''}}`; + + const results = await languageProvider.queryLabelValues(timeRange, METRIC_LABEL, match); + + // Check if this is still the most recent search + if (searchId !== latestSearchIdRef.current) { + return; // Ignore outdated results + } + + const [fuzzyOrderedMetrics] = fuzzySearch(results, queryString); + const resultsOptions: MetricsData = fuzzyOrderedMetrics.map((m) => generateMetricData(m, languageProvider)); + + setMetricsData(resultsOptions); + setIsLoading(false); + } catch (error) { + // Only update state if this is still the latest search + if (searchId === latestSearchIdRef.current) { + console.error('Backend search failed:', error); + setMetricsData([]); // Clear results on error + setIsLoading(false); + } + } + }, 300), + [fetchMetadata, languageProvider] + ); + + useEffect(() => { + fetchMetadata(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {children} + + ); +}; + +export function useMetricsModal() { + const context = useContext(MetricsModalContext); + if (context === undefined) { + throw new Error('useMetricsModal must be used within a MetricsModalContextProvider'); + } + return context; +} diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx index 1e179d336ee..3f3a1bf5afc 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/ResultsTable.tsx @@ -1,65 +1,62 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/ResultsTable.tsx -import { css } from '@emotion/css'; -import { ReactElement } from 'react'; +import { ReactElement, useMemo } from 'react'; import Highlighter from 'react-highlight-words'; -import { GrafanaTheme2 } from '@grafana/data'; import { t, Trans } from '@grafana/i18n'; -import { Button, Icon, Tooltip, useTheme2 } from '@grafana/ui'; +import { Icon, Tooltip, useStyles2 } from '@grafana/ui'; import { docsTip } from '../../../configuration/shared/utils'; import { PromVisualQuery } from '../../types'; -import { tracking } from './state/helpers'; -import { MetricsModalState } from './state/state'; -import { MetricData, MetricsData } from './types'; +import { useMetricsModal } from './MetricsModalContext'; +import { getResultsTableStyles } from './styles'; +import { MetricData } from './types'; type ResultsTableProps = { - metrics: MetricsData; onChange: (query: PromVisualQuery) => void; onClose: () => void; query: PromVisualQuery; - state: MetricsModalState; - disableTextWrap: boolean; }; export function ResultsTable(props: ResultsTableProps) { - const { metrics, onChange, onClose, query, state, disableTextWrap } = props; + const { onChange, onClose, query } = props; + const { + isLoading, + filteredMetricsData, + pagination: { pageNum, resultsPerPage }, + selectedTypes, + searchedText, + } = useMetricsModal(); - const theme = useTheme2(); - const styles = getStyles(theme, disableTextWrap); + const slicedMetrics = useMemo(() => { + const startIndex = (pageNum - 1) * resultsPerPage; + const endIndex = startIndex + resultsPerPage; + return filteredMetricsData.slice(startIndex, endIndex); + }, [filteredMetricsData, pageNum, resultsPerPage]); + + const styles = useStyles2(getResultsTableStyles); function selectMetric(metric: MetricData) { if (metric.value) { onChange({ ...query, metric: metric.value }); - tracking('grafana_prom_metric_encycopedia_tracking', state, metric.value); onClose(); } } function metaRows(metric: MetricData) { - if (state.fullMetaSearch && metric) { - return ( - <> - {displayType(metric.type ?? '')} - - - - - ); - } else { - return ( - <> - {displayType(metric.type ?? '')} - {metric.description ?? ''} - - ); - } + return ( + <> + {displayType(metric.type ?? '')} + + + + + ); } function addHelpIcon(fullType: string, descriptiveType: string, link: string) { @@ -105,7 +102,7 @@ export function ResultsTable(props: ResultsTableProps) { function noMetricsMessages(): ReactElement { let message; - if (!state.fuzzySearchQuery) { + if (!searchedText) { message = t( 'grafana-prometheus.querybuilder.results-table.message-no-metrics-found', 'There are no metrics found in the data source.' @@ -119,7 +116,7 @@ export function ResultsTable(props: ResultsTableProps) { ); } - if (state.fuzzySearchQuery || state.selectedTypes.length > 0) { + if (searchedText || selectedTypes.length > 0) { message = t( 'grafana-prometheus.querybuilder.results-table.message-expand-search', 'There are no metrics found. Try to expand your search and filters.' @@ -133,21 +130,6 @@ 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 ( @@ -155,115 +137,35 @@ export function ResultsTable(props: ResultsTableProps) { - {state.hasMetadata && ( - <> - - - - )} - + + <> - {metrics.length > 0 && - metrics.map((metric: MetricData, idx: number) => { + {slicedMetrics.length > 0 && + slicedMetrics.map((metric: MetricData, idx: number) => { return ( - + selectMetric(metric)}> - {state.hasMetadata && metaRows(metric)} - + {metaRows(metric)} ); })} - {metrics.length === 0 && !state.isLoading && noMetricsMessages()} + {slicedMetrics.length === 0 && !isLoading && noMetricsMessages()}
Name - Type - - Description - + Type + + Description +
- -
); } - -const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => { - return { - table: css({ - tableLayout: disableTextWrap ? undefined : 'fixed', - borderRadius: theme.shape.radius.default, - width: '100%', - whiteSpace: disableTextWrap ? 'nowrap' : 'normal', - td: { - padding: theme.spacing(1), - }, - 'td,th': { - minWidth: theme.spacing(3), - borderBottom: `1px solid ${theme.colors.border.weak}`, - }, - }), - row: css({ - label: 'row', - borderBottom: `1px solid ${theme.colors.border.weak}`, - '&:last-child': { - borderBottom: 0, - }, - }), - tableHeaderPadding: css({ - padding: '8px', - }), - matchHighLight: css({ - background: 'inherit', - color: theme.components.textHighlight.text, - backgroundColor: theme.components.textHighlight.background, - }), - nameWidth: css({ - width: disableTextWrap ? undefined : '37.5%', - }), - nameOverflow: css({ - overflowWrap: disableTextWrap ? undefined : 'anywhere', - }), - typeWidth: css({ - width: disableTextWrap ? undefined : '15%', - }), - descriptionWidth: css({ - width: disableTextWrap ? undefined : '35%', - }), - selectButtonWidth: css({ - width: disableTextWrap ? undefined : '12.5%', - }), - stickyHeader: css({ - position: 'sticky', - top: 0, - backgroundColor: theme.colors.background.primary, - }), - noResults: css({ - textAlign: 'center', - color: theme.colors.text.secondary, - }), - tooltipSpace: css({ - marginLeft: '4px', - }), - centerButton: css({ - display: 'block', - margin: 'auto', - border: 'none', - }), - }; -}; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.test.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.test.ts new file mode 100644 index 00000000000..d3998e46de0 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.test.ts @@ -0,0 +1,313 @@ +import { PrometheusLanguageProviderInterface } from '../../../language_provider'; +import { PromMetricsMetadata } from '../../../types'; + +import { calculatePageList, generateMetricData, getPlaceholders, getPromTypes } from './helpers'; +import { MetricsData } from './types'; + +// Mock the language provider +const createMockLanguageProvider = (metadata: PromMetricsMetadata = {}): PrometheusLanguageProviderInterface => + ({ + retrieveMetricsMetadata: jest.fn().mockReturnValue(metadata), + }) as unknown as PrometheusLanguageProviderInterface; + +describe('helpers.ts', () => { + describe('generateMetricData', () => { + it('should generate basic metric data', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'counter', + help: 'Test counter metric', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + + expect(result).toEqual({ + value: 'test_metric', + type: 'counter', + description: 'Test counter metric', + }); + }); + + it('should handle metric with no metadata', () => { + const mockProvider = createMockLanguageProvider({}); + + const result = generateMetricData('unknown_metric', mockProvider); + + expect(result).toEqual({ + value: 'unknown_metric', + type: undefined, + description: undefined, + }); + }); + + it('should enhance type based on description for histogram', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'gauge', + help: 'This is a histogram metric for testing', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + + expect(result.type).toBe('gauge (histogram)'); + }); + + it('should enhance type based on description for summary', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'counter', + help: 'This is a summary metric for testing', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + + expect(result.type).toBe('counter (summary)'); + }); + + it('should not enhance type if already matches description', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'histogram', + help: 'This is a histogram metric for testing', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + + expect(result.type).toBe('native histogram'); // Should be native histogram, not enhanced + }); + + it('should detect native histogram vs classic histogram', () => { + const mockProvider = createMockLanguageProvider({ + native_histogram: { + type: 'histogram', + help: 'Native histogram', + }, + classic_bucket: { + type: 'histogram', + help: 'Classic histogram', + }, + classic_bucket_with_labels: { + type: 'histogram', + help: 'Classic histogram with labels', + }, + }); + + // Native histogram (no _bucket suffix) + const nativeResult = generateMetricData('native_histogram', mockProvider); + expect(nativeResult.type).toBe('native histogram'); + + // Classic histogram (with _bucket suffix) + const classicResult = generateMetricData('classic_bucket', mockProvider); + expect(classicResult.type).toBe('histogram'); + + // Classic histogram with labels + const classicWithLabelsResult = generateMetricData('classic_bucket_with_labels', mockProvider); + expect(classicWithLabelsResult.type).toBe('native histogram'); // No _bucket pattern match + }); + + it('should handle old histogram pattern matching', () => { + const mockProvider = createMockLanguageProvider({ + test_bucket: { type: 'histogram', help: 'Test bucket metric' }, + 'test_bucket{le="0.1"}': { type: 'histogram', help: 'Test bucket with labels' }, + test_histogram: { type: 'histogram', help: 'Test histogram metric' }, + }); + + // Should be classic histogram (matches pattern) + expect(generateMetricData('test_bucket', mockProvider).type).toBe('histogram'); + expect(generateMetricData('test_bucket{le="0.1"}', mockProvider).type).toBe('histogram'); + + // Should be native histogram (doesn't match pattern) + expect(generateMetricData('test_histogram', mockProvider).type).toBe('native histogram'); + }); + + it('should handle case-insensitive description matching', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'gauge', + help: 'This is a HISTOGRAM metric for testing', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + expect(result.type).toBe('gauge (histogram)'); + }); + + it('should handle empty type with description enhancement', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: '', + help: 'This is a histogram metric for testing', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + expect(result.type).toBe('native histogram'); + }); + + it('should handle empty description', () => { + const mockProvider = createMockLanguageProvider({ + test_metric: { + type: 'gauge', + help: '', + }, + }); + + const result = generateMetricData('test_metric', mockProvider); + expect(result.type).toBe('gauge'); + }); + }); + + describe('calculatePageList', () => { + const createMetricsData = (length: number): MetricsData => + Array.from({ length }, (_, i) => ({ value: `metric_${i}` })); + + it('should return empty array for empty metrics data', () => { + expect(calculatePageList([], 10)).toEqual([]); + }); + + it('should return [1] for zero or negative results per page', () => { + const metricsData = createMetricsData(5); + expect(calculatePageList(metricsData, 0)).toEqual([1]); + expect(calculatePageList(metricsData, -5)).toEqual([1]); + }); + + it('should calculate correct page list for exact division', () => { + const metricsData = createMetricsData(20); + const result = calculatePageList(metricsData, 10); + expect(result).toEqual([1, 2]); + }); + + it('should calculate correct page list for non-exact division', () => { + const metricsData = createMetricsData(23); + const result = calculatePageList(metricsData, 10); + expect(result).toEqual([1, 2, 3]); + }); + + it('should handle single page scenario', () => { + const metricsData = createMetricsData(5); + const result = calculatePageList(metricsData, 10); + expect(result).toEqual([1]); + }); + + it('should handle very large datasets', () => { + const metricsData = createMetricsData(1000); + const result = calculatePageList(metricsData, 25); + expect(result).toHaveLength(40); + expect(result[0]).toBe(1); + expect(result[result.length - 1]).toBe(40); + }); + + it('should handle single item per page', () => { + const metricsData = createMetricsData(3); + const result = calculatePageList(metricsData, 1); + expect(result).toEqual([1, 2, 3]); + }); + + it('should handle fractional results per page', () => { + const metricsData = createMetricsData(10); + expect(calculatePageList(metricsData, 3.5)).toEqual([1, 2, 3]); + }); + }); + + describe('getPromTypes', () => { + it('should return array of Prometheus types', () => { + const types = getPromTypes(); + + expect(Array.isArray(types)).toBe(true); + expect(types.length).toBe(7); + + const expectedValues = ['counter', 'gauge', 'histogram', 'native histogram', 'summary', 'unknown', 'no type']; + const actualValues = types.map((type) => type.value); + + expect(actualValues).toEqual(expectedValues); + }); + + it('should have correct structure for each type', () => { + const types = getPromTypes(); + + types.forEach((type) => { + expect(type).toHaveProperty('value'); + expect(type).toHaveProperty('label'); + expect(type).toHaveProperty('description'); + + expect(typeof type.value).toBe('string'); + expect(typeof type.label).toBe('string'); + expect(typeof type.description).toBe('string'); + + expect(type.value.length).toBeGreaterThan(0); + expect(type.label.length).toBeGreaterThan(0); + expect(type.description.length).toBeGreaterThan(0); + }); + }); + + it('should return consistent results on multiple calls', () => { + const types1 = getPromTypes(); + const types2 = getPromTypes(); + + expect(types1).toEqual(types2); + }); + + it('should have unique values', () => { + const types = getPromTypes(); + const values = types.map((type) => type.value); + const uniqueValues = [...new Set(values)]; + + expect(values.length).toBe(uniqueValues.length); + }); + + it('should have descriptive labels', () => { + const types = getPromTypes(); + + types.forEach((type) => { + expect(type.label).not.toBe(type.value); + expect(type.label.length).toBeGreaterThanOrEqual(type.value.length); + }); + }); + }); + + describe('getPlaceholders', () => { + it('should return object with all required placeholders', () => { + const placeholders = getPlaceholders(); + + const expectedKeys = ['browse', 'filterType']; + + expect(Object.keys(placeholders)).toEqual(expectedKeys); + }); + + it('should have string values for all placeholders', () => { + const placeholders = getPlaceholders(); + + Object.values(placeholders).forEach((placeholder) => { + expect(typeof placeholder).toBe('string'); + expect(placeholder.length).toBeGreaterThan(0); + }); + }); + + it('should return consistent results on multiple calls', () => { + const placeholders1 = getPlaceholders(); + const placeholders2 = getPlaceholders(); + + expect(placeholders1).toEqual(placeholders2); + }); + + it('should contain expected placeholder content', () => { + const placeholders = getPlaceholders(); + + expect(placeholders.browse).toMatch(/search/i); + expect(placeholders.filterType).toMatch(/type/i); + }); + + it('should have descriptive placeholder text', () => { + const placeholders = getPlaceholders(); + + Object.values(placeholders).forEach((placeholder) => { + expect(placeholder.length).toBeGreaterThan(5); + expect(placeholder).toMatch(/[a-zA-Z]/); + }); + }); + }); +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.ts new file mode 100644 index 00000000000..9377a22e5e0 --- /dev/null +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/helpers.ts @@ -0,0 +1,123 @@ +// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts +import { t } from '@grafana/i18n'; + +import { PrometheusLanguageProviderInterface } from '../../../language_provider'; + +import { MetricData, MetricsData, PromFilterOption } from './types'; + +// Constants +const HISTOGRAM_TYPES = ['histogram', 'summary'] as const; +const OLD_HISTOGRAM_PATTERN = /^\w+_bucket$|^\w+_bucket{.*}$/; +const HISTOGRAM_TYPE = 'histogram'; +const NATIVE_HISTOGRAM_TYPE = 'native histogram'; + +/** + * Builds the metric data object with type and description + * @param metric - The metric name + * @param languageProvider - The Prometheus language provider interface + * @returns MetricData object with value, type, and description + */ +export const generateMetricData = ( + metric: string, + languageProvider: PrometheusLanguageProviderInterface +): MetricData => { + const metadata = languageProvider.retrieveMetricsMetadata(); + + let type = metadata[metric]?.type; + const description = metadata[metric]?.help; + + HISTOGRAM_TYPES.forEach((t) => { + if (description?.toLowerCase().includes(t) && type !== t) { + type = type ? `${type} (${t})` : t; + } + }); + + const oldHistogramMatch = metric.match(OLD_HISTOGRAM_PATTERN); + + if (type === HISTOGRAM_TYPE && !oldHistogramMatch) { + type = NATIVE_HISTOGRAM_TYPE; + } + + return { + value: metric, + type: type, + description: description, + }; +}; + +export function calculatePageList(metricsData: MetricsData, resultsPerPage: number): number[] { + if (!Array.isArray(metricsData) || metricsData.length === 0) { + return []; + } + + if (resultsPerPage <= 0) { + return [1]; + } + + const totalPages = Math.ceil(metricsData.length / resultsPerPage); + return Array.from({ length: totalPages }, (_, i) => i + 1); +} + +export const getPromTypes: () => PromFilterOption[] = () => [ + { + value: 'counter', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-counter', 'Counter'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-counter', + 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.' + ), + }, + { + value: 'gauge', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-gauge', 'Gauge'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-gauge', + 'A metric that represents a single numerical value that can arbitrarily go up and down.' + ), + }, + { + value: 'histogram', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-histogram', 'Histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-histogram', + 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.' + ), + }, + { + value: 'native histogram', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-native-histogram', 'Native histogram'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-native-histogram', + 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.' + ), + }, + { + value: 'summary', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-summary', 'Summary'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-summary', + 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.' + ), + }, + { + value: 'unknown', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-unknown', 'Unknown'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-unknown', + 'These metrics have been given the type unknown in the metadata.' + ), + }, + { + value: 'no type', + label: t('grafana-prometheus.querybuilder.get-prom-types.label-no-type', 'No type'), + description: t( + 'grafana-prometheus.querybuilder.get-prom-types.description-no-type', + 'These metrics have no defined type in the metadata.' + ), + }, +]; + +export const getPlaceholders = () => ({ + browse: t('grafana-prometheus.querybuilder.get-placeholders.browse', 'Search metrics by name'), + filterType: t('grafana-prometheus.querybuilder.get-placeholders.type', 'Filter by type'), +}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts deleted file mode 100644 index 76ee015c1e9..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/types.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { TimeRange } from '@grafana/data'; - -import { PrometheusDatasource } from '../../../../datasource'; -import { PromVisualQuery } from '../../../types'; - -interface MetricsModalState { - useBackend: boolean; - disableTextWrap: boolean; - includeNullMetadata: boolean; - fullMetaSearch: boolean; - hasMetadata: boolean; -} - -export interface MetricsModalProps { - datasource: PrometheusDatasource; - isOpen: boolean; - query: PromVisualQuery; - onClose: () => void; - onChange: (query: PromVisualQuery) => void; - initialMetrics: string[] | (() => Promise); - timeRange: TimeRange; -} - -export interface AdditionalSettingsProps { - state: MetricsModalState; - onChangeFullMetaSearch: () => void; - onChangeIncludeNullMetadata: () => void; - onChangeDisableTextWrap: () => void; - onChangeUseBackend: () => void; -} diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts deleted file mode 100644 index 79d41287197..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/helpers.ts +++ /dev/null @@ -1,278 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/helpers.ts -import { AnyAction } from '@reduxjs/toolkit'; - -import { t } from '@grafana/i18n'; -import { reportInteraction } from '@grafana/runtime'; - -import { PrometheusDatasource } from '../../../../datasource'; -import { PromMetricsMetadata } from '../../../../types'; -import { PromVisualQuery } from '../../../types'; -import { HaystackDictionary, MetricData, MetricsData, PromFilterOption } from '../types'; - -import { MetricsModalMetadata, MetricsModalState, setFilteredMetricCount } from './state'; - -export async function setMetrics( - datasource: PrometheusDatasource, - query: PromVisualQuery, - initialMetrics?: string[] -): Promise { - // metadata is set in the metric select now - // use this to disable metadata search and display - let hasMetadata = true; - const metadata = datasource.languageProvider.retrieveMetricsMetadata(); - if (metadata && Object.keys(metadata).length === 0) { - hasMetadata = false; - } - - let nameHaystackDictionaryData: HaystackDictionary = {}; - let metaHaystackDictionaryData: HaystackDictionary = {}; - - // pass in metrics from getMetrics in the query builder, reduced in the metric select - let metricsData: MetricsData | undefined; - - metricsData = initialMetrics?.map((m: string) => { - const metricData = buildMetricData(m, datasource); - - const metaDataString = `${m}¦${metricData.description}`; - - nameHaystackDictionaryData[m] = metricData; - metaHaystackDictionaryData[metaDataString] = metricData; - - return metricData; - }); - - return { - isLoading: false, - hasMetadata: hasMetadata, - metrics: metricsData ?? [], - metaHaystackDictionary: metaHaystackDictionaryData, - nameHaystackDictionary: nameHaystackDictionaryData, - totalMetricCount: metricsData?.length ?? 0, - filteredMetricCount: metricsData?.length ?? 0, - }; -} - -/** - * Builds the metric data object with type and description - * - * @param metric The metric name - * @param datasource The Prometheus datasource for mapping metradata to the metric name - * @returns A MetricData object. - */ -function buildMetricData(metric: string, datasource: PrometheusDatasource): MetricData { - let type = getMetadataType(metric, datasource.languageProvider.retrieveMetricsMetadata()); - - const description = getMetadataHelp(metric, datasource.languageProvider.retrieveMetricsMetadata()); - - ['histogram', 'summary'].forEach((t) => { - if (description?.toLowerCase().includes(t) && type !== t) { - type += ` (${t})`; - } - }); - - const oldHistogramMatch = metric.match(/^\w+_bucket$|^\w+_bucket{.*}$/); - - if (type === 'histogram' && !oldHistogramMatch) { - type = 'native histogram'; - } - - const metricData: MetricData = { - value: metric, - type: type, - description: description, - }; - - return metricData; -} - -function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined { - return metadata[metric]?.help; -} - -function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined { - return metadata[metric]?.type; -} - -/** - * The filtered and paginated metrics displayed in the modal - * */ -export function displayedMetrics(state: MetricsModalState, dispatch: React.Dispatch) { - const filteredSorted: MetricsData = filterMetrics(state); - - if (!state.isLoading && state.filteredMetricCount !== filteredSorted.length) { - dispatch(setFilteredMetricCount(filteredSorted.length)); - } - - return sliceMetrics(filteredSorted, state.pageNum, state.resultsPerPage); -} - -/** - * Filter the metrics with all the options, fuzzy, type, null metadata - */ -function filterMetrics(state: MetricsModalState): MetricsData { - let filteredMetrics: MetricsData = state.metrics; - - if (state.fuzzySearchQuery && !state.useBackend) { - if (state.fullMetaSearch) { - filteredMetrics = state.metaHaystackOrder.map((needle: string) => state.metaHaystackDictionary[needle]); - } else { - filteredMetrics = state.nameHaystackOrder.map((needle: string) => state.nameHaystackDictionary[needle]); - } - } - - if (state.selectedTypes.length > 0) { - filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => { - // Matches type - const matchesSelectedType = state.selectedTypes.some((t) => { - if (m.type && t.value) { - return m.type.includes(t.value); - } - - if (!m.type && t.value === 'no type') { - return true; - } - - return false; - }); - - // when a user filters for type, only return metrics with defined types - return matchesSelectedType; - }); - } - - if (!state.includeNullMetadata) { - filteredMetrics = filteredMetrics.filter((m: MetricData) => { - return m.type !== undefined && m.description !== undefined; - }); - } - - return filteredMetrics; -} - -export function calculatePageList(state: MetricsModalState) { - if (!state.metrics.length) { - return []; - } - - const calcResultsPerPage: number = state.resultsPerPage === 0 ? 1 : state.resultsPerPage; - - const pages = Math.floor(filterMetrics(state).length / calcResultsPerPage) + 1; - - return [...Array(pages).keys()].map((i) => i + 1); -} - -function sliceMetrics(metrics: MetricsData, pageNum: number, resultsPerPage: number) { - const calcResultsPerPage: number = resultsPerPage === 0 ? 1 : resultsPerPage; - const start: number = pageNum === 1 ? 0 : (pageNum - 1) * calcResultsPerPage; - const end: number = start + calcResultsPerPage; - return metrics.slice(start, end); -} - -export const calculateResultsPerPage = (results: number, defaultResults: number, max: number) => { - if (results < 1) { - return 1; - } - - if (results > max) { - return max; - } - - return results ?? defaultResults; -}; - -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, - useRegexSearch: state?.useBackend, - includeResultsWithoutMetadata: state?.includeNullMetadata, - }); - 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 getPromTypes: () => PromFilterOption[] = () => [ - { - value: 'counter', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-counter', 'Counter'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-counter', - 'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.' - ), - }, - { - value: 'gauge', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-gauge', 'Gauge'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-gauge', - 'A metric that represents a single numerical value that can arbitrarily go up and down.' - ), - }, - { - value: 'histogram', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-histogram', 'Histogram'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-histogram', - 'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.' - ), - }, - { - value: 'native histogram', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-native-histogram', 'Native histogram'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-native-histogram', - 'Native histograms are different from classic Prometheus histograms in a number of ways: Native histogram bucket boundaries are calculated by a formula that depends on the scale (resolution) of the native histogram, and are not user defined.' - ), - }, - { - value: 'summary', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-summary', 'Summary'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-summary', - 'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.' - ), - }, - { - value: 'unknown', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-unknown', 'Unknown'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-unknown', - 'These metrics have been given the type unknown in the metadata.' - ), - }, - { - value: 'no type', - label: t('grafana-prometheus.querybuilder.get-prom-types.label-no-type', 'No type'), - description: t( - 'grafana-prometheus.querybuilder.get-prom-types.description-no-type', - 'These metrics have no defined type in the metadata.' - ), - }, -]; - -export const getPlaceholders = () => ({ - browse: t('grafana-prometheus.querybuilder.get-placeholders.browse', 'Search metrics by name'), - metadataSearchSwitch: t( - 'grafana-prometheus.querybuilder.get-placeholders.metadata-search-switch', - 'Include description in search' - ), - type: t('grafana-prometheus.querybuilder.get-placeholders.type', 'Filter by type'), - includeNullMetadata: t( - 'grafana-prometheus.querybuilder.get-placeholders.include-null-metadata', - 'Include results with no metadata' - ), - setUseBackend: t('grafana-prometheus.querybuilder.get-placeholders.set-use-backend', 'Enable regex search'), -}); diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts deleted file mode 100644 index 05031eee4db..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/state/state.ts +++ /dev/null @@ -1,212 +0,0 @@ -// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/state/state.ts -import { PayloadAction, createSlice } from '@reduxjs/toolkit'; - -import { SelectableValue } from '@grafana/data'; - -import { PromVisualQuery } from '../../../types'; -import { HaystackDictionary, MetricsData } from '../types'; - -export const DEFAULT_RESULTS_PER_PAGE = 100; -export const MAXIMUM_RESULTS_PER_PAGE = 1000; - -/** - * Initial state for the metrics explorer - * @returns - */ -export function initialState(query?: PromVisualQuery): MetricsModalState { - return { - isLoading: true, - metrics: [], - hasMetadata: true, - metaHaystackDictionary: {}, - metaHaystackMatches: [], - metaHaystackOrder: [], - nameHaystackDictionary: {}, - nameHaystackOrder: [], - nameHaystackMatches: [], - totalMetricCount: 0, - filteredMetricCount: null, - resultsPerPage: DEFAULT_RESULTS_PER_PAGE, - pageNum: 1, - fuzzySearchQuery: '', - fullMetaSearch: query?.fullMetaSearch ?? false, - includeNullMetadata: query?.includeNullMetadata ?? true, - selectedTypes: [], - useBackend: query?.useBackend ?? false, - disableTextWrap: query?.disableTextWrap ?? false, - showAdditionalSettings: false, - }; -} - -/** - * The metrics explorer state object - */ -export interface MetricsModalState { - /** Used for the loading spinner */ - isLoading: boolean; - /** - * Initial collection of metrics. - * The frontend filters do not impact this, but - * it is reduced by the backend search. - */ - metrics: MetricsData; - /** Field for disabling type select and switches that rely on metadata */ - hasMetadata: boolean; - /** Used to display metrics and help with fuzzy order */ - nameHaystackDictionary: HaystackDictionary; - /** Used to sort name fuzzy search by relevance */ - nameHaystackOrder: string[]; - /** Used to highlight text in fuzzy matches */ - nameHaystackMatches: string[]; - /** Used to display metrics and help with fuzzy order for search across all metadata */ - metaHaystackDictionary: HaystackDictionary; - /** Used to sort meta fuzzy search by relevance */ - metaHaystackOrder: string[]; - /** Used to highlight text in fuzzy matches */ - metaHaystackMatches: string[]; - /** Total results computed on initialization */ - totalMetricCount: number; - /** Set after filtering metrics */ - filteredMetricCount: number | null; - /** Pagination field for showing results in table */ - resultsPerPage: number; - /** Pagination field */ - pageNum: number; - /** The text query used to match metrics */ - fuzzySearchQuery: string; - /** Enables the fuzzy meatadata search */ - fullMetaSearch: boolean; - /** Includes results that are missing type and description */ - includeNullMetadata: boolean; - /** Filter by prometheus type */ - selectedTypes: Array>; - /** Filter by the series match endpoint instead of the fuzzy search */ - useBackend: boolean; - /** Disable text wrap for descriptions in the results table */ - disableTextWrap: boolean; - /** Display toggle switches for settings */ - showAdditionalSettings: boolean; -} - -/** - * Type for the useEffect get metadata function - */ -export type MetricsModalMetadata = { - isLoading: boolean; - metrics: MetricsData; - hasMetadata: boolean; - metaHaystackDictionary: HaystackDictionary; - nameHaystackDictionary: HaystackDictionary; - totalMetricCount: number; - filteredMetricCount: number | null; -}; - -// for updating the settings in the PromQuery model -export function getSettings(visQuery: PromVisualQuery): MetricsModalSettings { - return { - useBackend: visQuery?.useBackend ?? false, - disableTextWrap: visQuery?.disableTextWrap ?? false, - fullMetaSearch: visQuery?.fullMetaSearch ?? false, - includeNullMetadata: visQuery.includeNullMetadata ?? false, - }; -} - -export type MetricsModalSettings = { - useBackend?: boolean; - disableTextWrap?: boolean; - fullMetaSearch?: boolean; - includeNullMetadata?: boolean; -}; - -export const stateSlice = createSlice({ - name: 'metrics-modal-state', - initialState: initialState(), - reducers: { - filterMetricsBackend: ( - state, - action: PayloadAction<{ - metrics: MetricsData; - filteredMetricCount: number; - isLoading: boolean; - }> - ) => { - state.metrics = action.payload.metrics; - state.filteredMetricCount = action.payload.filteredMetricCount; - state.isLoading = action.payload.isLoading; - }, - buildMetrics: (state, action: PayloadAction) => { - state.isLoading = action.payload.isLoading; - state.metrics = action.payload.metrics; - state.hasMetadata = action.payload.hasMetadata; - state.metaHaystackDictionary = action.payload.metaHaystackDictionary; - state.nameHaystackDictionary = action.payload.nameHaystackDictionary; - state.totalMetricCount = action.payload.totalMetricCount; - state.filteredMetricCount = action.payload.filteredMetricCount; - }, - setIsLoading: (state, action: PayloadAction) => { - state.isLoading = action.payload; - }, - setFilteredMetricCount: (state, action: PayloadAction) => { - state.filteredMetricCount = action.payload; - }, - setResultsPerPage: (state, action: PayloadAction) => { - state.resultsPerPage = action.payload; - }, - setPageNum: (state, action: PayloadAction) => { - state.pageNum = action.payload; - }, - setFuzzySearchQuery: (state, action: PayloadAction) => { - state.fuzzySearchQuery = action.payload; - state.pageNum = 1; - }, - setNameHaystack: (state, action: PayloadAction) => { - state.nameHaystackOrder = action.payload[0]; - state.nameHaystackMatches = action.payload[1]; - }, - setMetaHaystack: (state, action: PayloadAction) => { - state.metaHaystackOrder = action.payload[0]; - state.metaHaystackMatches = action.payload[1]; - }, - setFullMetaSearch: (state, action: PayloadAction) => { - state.fullMetaSearch = action.payload; - state.pageNum = 1; - }, - setIncludeNullMetadata: (state, action: PayloadAction) => { - state.includeNullMetadata = action.payload; - state.pageNum = 1; - }, - setSelectedTypes: (state, action: PayloadAction>>) => { - state.selectedTypes = action.payload; - state.pageNum = 1; - }, - setUseBackend: (state, action: PayloadAction) => { - state.useBackend = action.payload; - state.fullMetaSearch = false; - state.pageNum = 1; - }, - setDisableTextWrap: (state) => { - state.disableTextWrap = !state.disableTextWrap; - }, - showAdditionalSettings: (state) => { - state.showAdditionalSettings = !state.showAdditionalSettings; - }, - }, -}); - -export const { - setIsLoading, - buildMetrics, - filterMetricsBackend, - setResultsPerPage, - setPageNum, - setFuzzySearchQuery, - setNameHaystack, - setMetaHaystack, - setFullMetaSearch, - setIncludeNullMetadata, - setSelectedTypes, - setUseBackend, - setDisableTextWrap, - showAdditionalSettings, - setFilteredMetricCount, -} = stateSlice.actions; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/styles.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/styles.ts index bcc6fa425c7..64e52600055 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/styles.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/styles.ts @@ -3,7 +3,7 @@ import { css } from '@emotion/css'; import { GrafanaTheme2 } from '@grafana/data'; -export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => { +export const getMetricsModalStyles = (theme: GrafanaTheme2) => { return { modal: css({ width: '85vw', @@ -34,20 +34,9 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => { minWidth: '100%', }, }), - selectWrapper: css({ - marginBottom: theme.spacing(1), - }), - resultsAmount: css({ - color: theme.colors.text.secondary, - fontSize: '0.85rem', - padding: '0 0 4px 0', - }), resultsData: css({ margin: `4px 0 ${theme.spacing(2)} 0`, }), - resultsDataCount: css({ - margin: 0, - }), resultsDataFiltered: css({ color: theme.colors.text.secondary, textAlign: 'center', @@ -67,9 +56,9 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => { display: 'flex', flexDirection: 'row', flexWrap: 'wrap', - justifyContent: 'space-between', alignItems: 'center', position: 'sticky', + justifyContent: 'center', }), currentlySelected: css({ color: 'grey', @@ -82,21 +71,69 @@ export const getStyles = (theme: GrafanaTheme2, disableTextWrap: boolean) => { visible: css({ visibility: 'visible', }), - settingsBtn: css({ - float: 'right', - }), noBorder: css({ border: 'none', }), - resultsPerPageLabel: css({ - color: theme.colors.text.secondary, - opacity: '75%', - paddingTop: '5px', - fontSize: '0.85rem', - marginRight: '8px', + }; +}; + +export const getResultsTableStyles = (theme: GrafanaTheme2) => { + return { + table: css({ + tableLayout: 'fixed', + borderRadius: theme.shape.radius.default, + width: '100%', + whiteSpace: 'normal', + td: { + padding: theme.spacing(1), + }, + 'td,th': { + minWidth: theme.spacing(3), + borderBottom: `1px solid ${theme.colors.border.weak}`, + }, }), - resultsPerPageWrapper: css({ - display: 'flex', + row: css({ + label: 'row', + borderBottom: `1px solid ${theme.colors.border.weak}`, + cursor: 'pointer', + '&:last-child': { + borderBottom: 0, + }, + '&:hover': { + backgroundColor: theme.colors.background.secondary, + }, + }), + tableHeaderPadding: css({ + padding: '8px', + }), + matchHighLight: css({ + background: 'inherit', + color: theme.components.textHighlight.text, + backgroundColor: theme.components.textHighlight.background, + }), + nameWidth: css({ + width: '37.5%', + }), + nameOverflow: css({ + overflowWrap: 'anywhere', + }), + typeWidth: css({ + width: '15%', + }), + descriptionWidth: css({ + width: '35%', + }), + stickyHeader: css({ + position: 'sticky', + top: 0, + backgroundColor: theme.colors.background.primary, + }), + noResults: css({ + textAlign: 'center', + color: theme.colors.text.secondary, + }), + tooltipSpace: css({ + marginLeft: '4px', }), }; }; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/testIds.ts similarity index 65% rename from packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts rename to packages/grafana-prometheus/src/querybuilder/components/metrics-modal/testIds.ts index 275394dd348..f1e5c7709e8 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/shared/testIds.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/testIds.ts @@ -1,12 +1,9 @@ export const metricsModaltestIds = { metricModal: 'metric-modal', searchMetric: 'search-metric', - searchWithMetadata: 'search-with-metadata', selectType: 'select-type', metricCard: 'metric-card', useMetric: 'use-metric', searchPage: 'search-page', resultsPerPage: 'results-per-page', - setUseBackend: 'set-use-backend', - showAdditionalSettings: 'show-additional-settings', }; diff --git a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts index 297e216b560..3de9b6f1828 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts +++ b/packages/grafana-prometheus/src/querybuilder/components/metrics-modal/uFuzzy.ts @@ -1,6 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/metrics-modal/uFuzzy.ts import uFuzzy from '@leeoniya/ufuzzy'; -import { debounce as debounceLodash } from 'lodash'; const uf = new uFuzzy({ intraMode: 1, @@ -10,37 +9,34 @@ const uf = new uFuzzy({ intraDel: 1, }); -function fuzzySearch(haystack: string[], query: string, dispatcher: (data: string[][]) => void) { +export const fuzzySearch = (haystack: string[], query: string) => { const [idxs, info, order] = uf.search(haystack, query, 0, 1e5); let haystackOrder: string[] = []; let matchesSet: Set = new Set(); - if (idxs && order) { - /** - * get the fuzzy matches for hilighting - * @param part - * @param matched - */ - const mark = (part: string, matched: boolean) => { - if (matched) { - matchesSet.add(part); - } - }; - - // Iterate to create the order of needles(queries) and the matches - for (let i = 0; i < order.length; i++) { - let infoIdx = order[i]; - - /** Evaluate the match, get the matches for highlighting */ - uFuzzy.highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx], mark); - /** Get the order */ - haystackOrder.push(haystack[info.idx[infoIdx]]); - } - - dispatcher([haystackOrder, [...matchesSet]]); - } else if (!query) { - dispatcher([[], []]); + if (!(idxs && order)) { + return [[], []]; } -} + /** + * get the fuzzy matches for highlighting + * @param part + * @param matched + */ + const mark = (part: string, matched: boolean) => { + if (matched) { + matchesSet.add(part); + } + }; -export const debouncedFuzzySearch = debounceLodash(fuzzySearch, 300); + // Iterate to create the order of needles(queries) and the matches + for (let i = 0; i < order.length; i++) { + let infoIdx = order[i]; + + /** Evaluate the match, get the matches for highlighting */ + uFuzzy.highlight(haystack[info.idx[infoIdx]], info.ranges[infoIdx], mark); + /** Get the order */ + haystackOrder.push(haystack[info.idx[infoIdx]]); + } + + return [haystackOrder, [...matchesSet]]; +}; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx deleted file mode 100644 index 03787aad88a..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/shared/BaseQueryBuilder.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { memo } from 'react'; - -import { NestedQueryList } from '../NestedQueryList'; - -import { QueryBuilderContent } from './QueryBuilderContent'; -import { BaseQueryBuilderProps } from './types'; - -export const BaseQueryBuilder = memo((props) => { - const { query, datasource, onChange, onRunQuery, showExplain } = props; - - return ( - <> - - {query.binaryQueries && query.binaryQueries.length > 0 && ( - - )} - - ); -}); - -BaseQueryBuilder.displayName = 'BaseQueryBuilder'; diff --git a/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts b/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts deleted file mode 100644 index 626e6606259..00000000000 --- a/packages/grafana-prometheus/src/querybuilder/components/shared/types.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { PanelData } from '@grafana/data'; - -import { PrometheusDatasource } from '../../../datasource'; -import { PromVisualQuery } from '../../types'; - -export interface BaseQueryBuilderProps { - query: PromVisualQuery; - datasource: PrometheusDatasource; - onChange: (update: PromVisualQuery) => void; - onRunQuery: () => void; - data?: PanelData; - showExplain: boolean; -} diff --git a/packages/grafana-prometheus/src/querybuilder/types.ts b/packages/grafana-prometheus/src/querybuilder/types.ts index 991cfec8969..c2bc18bc2e5 100644 --- a/packages/grafana-prometheus/src/querybuilder/types.ts +++ b/packages/grafana-prometheus/src/querybuilder/types.ts @@ -14,11 +14,6 @@ export interface PromVisualQuery { labels: QueryBuilderLabelFilter[]; operations: QueryBuilderOperation[]; binaryQueries?: PromVisualQueryBinary[]; - // metrics explorer additional settings - useBackend?: boolean; - disableTextWrap?: boolean; - includeNullMetadata?: boolean; - fullMetaSearch?: boolean; } export interface PromQueryModellerInterface { diff --git a/packages/grafana-prometheus/src/types.ts b/packages/grafana-prometheus/src/types.ts index acd5b803ee2..98730018bf3 100644 --- a/packages/grafana-prometheus/src/types.ts +++ b/packages/grafana-prometheus/src/types.ts @@ -15,11 +15,6 @@ export interface PromQuery extends GenPromQuery, DataQuery { showingTable?: boolean; hinting?: boolean; interval?: string; - // store the metrics explorer additional settings - useBackend?: boolean; - disableTextWrap?: boolean; - fullMetaSearch?: boolean; - includeNullMetadata?: boolean; fromExploreMetrics?: boolean; }