mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Prometheus: Metric encyclopedia (#63423)
* add metric encyclopedia feature toggle and component * remove unused button * move file, add test file * add tests * add pagination and tests * test with 10,000,000 metrics * remove unused import * add filter by type * search alphabetically and add switch to exclude metrics with no metadata * add suggested functions and filter for functions * allow user to select variables in encyclopedia * fix style and tests * add fuzzy search by either metric name or all metadata * if missing metadata, remove metadata fuzzy search option, exclude metadata, and filter by type * add encyclopedia feature tracking * indicate that metrics are filtered by labels * handle metric singular or plural * add tooltips and fix language * add filtering tests * change 'search' to 'browse' * remove functions filter and tests as not part of work flow * add m.e. button and selected metric is a tag * fix hanging search and update styles, padding, labels, and groupings * small performance improvements * fix tests * add backend metrics query option * add loading spinner for start load and backend search * autofocus search input * Update docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com> * run prettier * run prettier * fix text for feature toggle * for license check since https://cla-assistant.io/check/grafana/grafana?pullRequest=<PR#> is not working * fixing tests * fix feature toggle docs * fix feature toggle * fix feature toggle * add owner to feature toggle --------- Co-authored-by: Christopher Moyer <35463610+chri2547@users.noreply.github.com>
This commit is contained in:
parent
94f39e69a3
commit
9b6e531549
@ -92,6 +92,7 @@ Alpha features might be changed or removed without prior notice.
|
|||||||
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
| `individualCookiePreferences` | Support overriding cookie preferences per user |
|
||||||
| `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. |
|
| `drawerDataSourcePicker` | Changes the user experience for data source selection to a drawer. |
|
||||||
| `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries |
|
| `traceqlSearch` | Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries |
|
||||||
|
| `prometheusMetricEncyclopedia` | Replaces the Prometheus query builder metric select option with a paginated and filterable component |
|
||||||
|
|
||||||
## Development feature toggles
|
## Development feature toggles
|
||||||
|
|
||||||
|
@ -81,4 +81,5 @@ export interface FeatureToggles {
|
|||||||
individualCookiePreferences?: boolean;
|
individualCookiePreferences?: boolean;
|
||||||
drawerDataSourcePicker?: boolean;
|
drawerDataSourcePicker?: boolean;
|
||||||
traceqlSearch?: boolean;
|
traceqlSearch?: boolean;
|
||||||
|
prometheusMetricEncyclopedia?: boolean;
|
||||||
}
|
}
|
||||||
|
@ -393,5 +393,12 @@ var (
|
|||||||
State: FeatureStateAlpha,
|
State: FeatureStateAlpha,
|
||||||
FrontendOnly: true,
|
FrontendOnly: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "prometheusMetricEncyclopedia",
|
||||||
|
Description: "Replaces the Prometheus query builder metric select option with a paginated and filterable component",
|
||||||
|
State: FeatureStateAlpha,
|
||||||
|
FrontendOnly: true,
|
||||||
|
Owner: "O11y-metrics",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -266,4 +266,8 @@ const (
|
|||||||
// FlagTraceqlSearch
|
// FlagTraceqlSearch
|
||||||
// Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries
|
// Enables the 'TraceQL Search' tab for the Tempo datasource which provides a UI to generate TraceQL queries
|
||||||
FlagTraceqlSearch = "traceqlSearch"
|
FlagTraceqlSearch = "traceqlSearch"
|
||||||
|
|
||||||
|
// FlagPrometheusMetricEncyclopedia
|
||||||
|
// Replaces the Prometheus query builder metric select option with a paginated and filterable component
|
||||||
|
FlagPrometheusMetricEncyclopedia = "prometheusMetricEncyclopedia"
|
||||||
)
|
)
|
||||||
|
@ -77,6 +77,20 @@ export function getMetadataString(metric: string, metadata: PromMetricsMetadata)
|
|||||||
return `${type.toUpperCase()}: ${help}`;
|
return `${type.toUpperCase()}: ${help}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getMetadataHelp(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||||
|
if (!metadata[metric]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return metadata[metric].help;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getMetadataType(metric: string, metadata: PromMetricsMetadata): string | undefined {
|
||||||
|
if (!metadata[metric]) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return metadata[metric].type;
|
||||||
|
}
|
||||||
|
|
||||||
const PREFIX_DELIMITER_REGEX =
|
const PREFIX_DELIMITER_REGEX =
|
||||||
/(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/;
|
/(="|!="|=~"|!~"|\{|\[|\(|\+|-|\/|\*|%|\^|\band\b|\bor\b|\bunless\b|==|>=|!=|<=|>|<|=|~|,)/;
|
||||||
|
|
||||||
|
@ -0,0 +1,297 @@
|
|||||||
|
import { render, screen, waitFor } from '@testing-library/react';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data';
|
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
|
import PromQlLanguageProvider from '../../language_provider';
|
||||||
|
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||||
|
import { PromOptions } from '../../types';
|
||||||
|
import { PromVisualQuery } from '../types';
|
||||||
|
|
||||||
|
import { MetricEncyclopediaModal, testIds, placeholders } from './MetricEncyclopediaModal';
|
||||||
|
|
||||||
|
// don't care about interaction tracking in our unit tests
|
||||||
|
jest.mock('@grafana/runtime', () => ({
|
||||||
|
...jest.requireActual('@grafana/runtime'),
|
||||||
|
reportInteraction: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('MetricEncyclopediaModal', () => {
|
||||||
|
it('renders the modal', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('Browse Metrics')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a list of metrics', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('all-metrics')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders a list of metrics filtered by labels in the PromVisualQuery', async () => {
|
||||||
|
const query: PromVisualQuery = {
|
||||||
|
metric: 'random_metric',
|
||||||
|
labels: [
|
||||||
|
{
|
||||||
|
op: '=',
|
||||||
|
label: 'action',
|
||||||
|
value: 'add_presence',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
operations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
setup(query, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('with-labels')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a type for a metric when the metric is clicked', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('all-metrics')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const interactiveMetric = screen.getByText('all-metrics');
|
||||||
|
|
||||||
|
await userEvent.click(interactiveMetric);
|
||||||
|
|
||||||
|
expect(screen.getByText('all-metrics-type')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays a description for a metric', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('all-metrics')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const interactiveMetric = screen.getByText('all-metrics');
|
||||||
|
|
||||||
|
await userEvent.click(interactiveMetric);
|
||||||
|
|
||||||
|
expect(screen.getByText('all-metrics-help')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('displays no metadata for a metric missing metadata when the metric is clicked', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('b')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const interactiveMetric = screen.getByText('b');
|
||||||
|
|
||||||
|
await userEvent.click(interactiveMetric);
|
||||||
|
|
||||||
|
expect(screen.getByText('No metadata available')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filtering
|
||||||
|
it('has a filter for selected type', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const selectType = screen.getByText(placeholders.type);
|
||||||
|
expect(selectType).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters by alphebetical letter choice', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
// pick the letter J
|
||||||
|
const letterJ = screen.getByTestId('letter-J');
|
||||||
|
await userEvent.click(letterJ);
|
||||||
|
|
||||||
|
// check metrics that start with J
|
||||||
|
const metricStartingWithJ = screen.getByText('j');
|
||||||
|
expect(metricStartingWithJ).toBeInTheDocument();
|
||||||
|
// check metrics that don't start with J
|
||||||
|
const metricStartingWithSomethingElse = screen.queryByText('a');
|
||||||
|
expect(metricStartingWithSomethingElse).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('allows a user to select a template variable', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
const selectType = screen.getByText(placeholders.variables);
|
||||||
|
expect(selectType).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Pagination
|
||||||
|
it('shows metrics within a range by pagination', async () => {
|
||||||
|
// default resultsPerPage is 10
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText('all-metrics')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('a_bucket')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('a')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('b')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('c')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('d')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('e')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('f')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('g')).toBeInTheDocument();
|
||||||
|
expect(screen.getByText('h')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not show metrics outside a range by pagination', async () => {
|
||||||
|
// default resultsPerPage is 10
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
const metricOutsideRange = screen.queryByText('j');
|
||||||
|
expect(metricOutsideRange).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shows results metrics per page chosen by the user', async () => {
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
const resultsPerPageInput = screen.getByTestId(testIds.resultsPerPage);
|
||||||
|
await userEvent.type(resultsPerPageInput, '12');
|
||||||
|
const metricInsideRange = screen.getByText('j');
|
||||||
|
expect(metricInsideRange).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('paginates millions of metrics and does not run out of memory', async () => {
|
||||||
|
const millionsOfMetrics: string[] = [...Array(1000000).keys()].map((i) => '' + i);
|
||||||
|
setup(defaultQuery, millionsOfMetrics);
|
||||||
|
await waitFor(() => {
|
||||||
|
// doesn't break on loading
|
||||||
|
expect(screen.getByText('0')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const resultsPerPageInput = screen.getByTestId(testIds.resultsPerPage);
|
||||||
|
// doesn't break on changing results per page
|
||||||
|
await userEvent.type(resultsPerPageInput, '11');
|
||||||
|
const metricInsideRange = screen.getByText('10');
|
||||||
|
expect(metricInsideRange).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fuzzy search
|
||||||
|
it('searches and filter by metric name with a fuzzy search', async () => {
|
||||||
|
// search for a_bucket by name
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
let metricAll: HTMLElement | null;
|
||||||
|
let metricABucket: HTMLElement | null;
|
||||||
|
await waitFor(() => {
|
||||||
|
metricAll = screen.getByText('all-metrics');
|
||||||
|
metricABucket = screen.getByText('a_bucket');
|
||||||
|
expect(metricAll).toBeInTheDocument();
|
||||||
|
expect(metricABucket).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
const searchMetric = screen.getByTestId(testIds.searchMetric);
|
||||||
|
expect(searchMetric).toBeInTheDocument();
|
||||||
|
await userEvent.type(searchMetric, 'a_b');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
metricABucket = screen.getByText('a_bucket');
|
||||||
|
expect(metricABucket).toBeInTheDocument();
|
||||||
|
metricAll = screen.queryByText('all-metrics');
|
||||||
|
expect(metricAll).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('searches by all metric metadata with a fuzzy search', async () => {
|
||||||
|
// search for a_bucket by metadata type counter but only type countt
|
||||||
|
setup(defaultQuery, listOfMetrics);
|
||||||
|
let metricABucket: HTMLElement | null;
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
metricABucket = screen.getByText('a_bucket');
|
||||||
|
expect(metricABucket).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
const metadataSwitch = screen.getByTestId(testIds.searchWithMetadata);
|
||||||
|
expect(metadataSwitch).toBeInTheDocument();
|
||||||
|
await userEvent.click(metadataSwitch);
|
||||||
|
|
||||||
|
const searchMetric = screen.getByTestId(testIds.searchMetric);
|
||||||
|
expect(searchMetric).toBeInTheDocument();
|
||||||
|
await userEvent.type(searchMetric, 'countt');
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
metricABucket = screen.getByText('a_bucket');
|
||||||
|
expect(metricABucket).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultQuery: PromVisualQuery = {
|
||||||
|
metric: 'random_metric',
|
||||||
|
labels: [],
|
||||||
|
operations: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
const listOfMetrics: string[] = ['all-metrics', 'a_bucket', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'];
|
||||||
|
|
||||||
|
function createDatasource(metrics: string[], withLabels?: boolean) {
|
||||||
|
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
|
||||||
|
|
||||||
|
// display different results if their are labels selected in the PromVisualQuery
|
||||||
|
if (withLabels) {
|
||||||
|
languageProvider.getSeries = () => Promise.resolve({ __name__: ['with-labels'] });
|
||||||
|
languageProvider.metricsMetadata = {
|
||||||
|
'with-labels': {
|
||||||
|
type: 'with-labels-type',
|
||||||
|
help: 'with-labels-help',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// all metrics
|
||||||
|
languageProvider.getLabelValues = () => Promise.resolve(metrics);
|
||||||
|
languageProvider.metricsMetadata = {
|
||||||
|
'all-metrics': {
|
||||||
|
type: 'all-metrics-type',
|
||||||
|
help: 'all-metrics-help',
|
||||||
|
},
|
||||||
|
a: {
|
||||||
|
type: 'counter',
|
||||||
|
help: 'a-metric-help',
|
||||||
|
},
|
||||||
|
a_bucket: {
|
||||||
|
type: 'counter',
|
||||||
|
help: 'for functions',
|
||||||
|
},
|
||||||
|
// missing metadata for other metrics is tested for, see below
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const datasource = new PrometheusDatasource(
|
||||||
|
{
|
||||||
|
url: '',
|
||||||
|
jsonData: {},
|
||||||
|
meta: {} as DataSourcePluginMeta,
|
||||||
|
} as DataSourceInstanceSettings<PromOptions>,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
languageProvider
|
||||||
|
);
|
||||||
|
return datasource;
|
||||||
|
}
|
||||||
|
|
||||||
|
function createProps(query: PromVisualQuery, datasource: PrometheusDatasource) {
|
||||||
|
return {
|
||||||
|
datasource,
|
||||||
|
isOpen: true,
|
||||||
|
onChange: jest.fn(),
|
||||||
|
onClose: jest.fn(),
|
||||||
|
query: query,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function setup(query: PromVisualQuery, metrics: string[], withlabels?: boolean) {
|
||||||
|
const withLabels: boolean = query.labels.length > 0;
|
||||||
|
const datasource = createDatasource(metrics, withLabels);
|
||||||
|
const props = createProps(query, datasource);
|
||||||
|
|
||||||
|
// render the modal only
|
||||||
|
const { container } = render(<MetricEncyclopediaModal {...props} />);
|
||||||
|
|
||||||
|
return container;
|
||||||
|
}
|
@ -0,0 +1,735 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
|
import uFuzzy from '@leeoniya/ufuzzy';
|
||||||
|
import debounce from 'debounce-promise';
|
||||||
|
import { debounce as debounceLodash } from 'lodash';
|
||||||
|
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { GrafanaTheme2, SelectableValue } from '@grafana/data';
|
||||||
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Collapse,
|
||||||
|
InlineField,
|
||||||
|
InlineLabel,
|
||||||
|
InlineSwitch,
|
||||||
|
Input,
|
||||||
|
Modal,
|
||||||
|
MultiSelect,
|
||||||
|
Select,
|
||||||
|
Spinner,
|
||||||
|
useStyles2,
|
||||||
|
} from '@grafana/ui';
|
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
|
import { getMetadataHelp, getMetadataType } from '../../language_provider';
|
||||||
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
|
import { regexifyLabelValuesQueryString } from '../shared/parsingUtils';
|
||||||
|
import { PromVisualQuery } from '../types';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
datasource: PrometheusDatasource;
|
||||||
|
isOpen: boolean;
|
||||||
|
query: PromVisualQuery;
|
||||||
|
onClose: () => void;
|
||||||
|
onChange: (query: PromVisualQuery) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MetricsData = MetricData[];
|
||||||
|
|
||||||
|
type MetricData = {
|
||||||
|
value: string;
|
||||||
|
type?: string;
|
||||||
|
description?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PromFilterOption = {
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const promTypes: PromFilterOption[] = [
|
||||||
|
{
|
||||||
|
value: 'counter',
|
||||||
|
description:
|
||||||
|
'A cumulative metric that represents a single monotonically increasing counter whose value can only increase or be reset to zero on restart.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'gauge',
|
||||||
|
description: 'A metric that represents a single numerical value that can arbitrarily go up and down.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'histogram',
|
||||||
|
description:
|
||||||
|
'A histogram samples observations (usually things like request durations or response sizes) and counts them in configurable buckets.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'summary',
|
||||||
|
description:
|
||||||
|
'A summary samples observations (usually things like request durations and response sizes) and can calculate configurable quantiles over a sliding time window.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const placeholders = {
|
||||||
|
browse: 'Browse metric names by text',
|
||||||
|
metadataSearchSwicth: 'Browse by metadata type and description in addition to metric name',
|
||||||
|
type: 'Counter, gauge, histogram, or summary',
|
||||||
|
variables: 'Select a template variable for your metric',
|
||||||
|
excludeNoMetadata: 'Exclude results with no metadata when filtering',
|
||||||
|
setUseBackend: 'Use the backend to browse metrics and disable fuzzy search metadata browsing',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_RESULTS_PER_PAGE = 10;
|
||||||
|
|
||||||
|
export const MetricEncyclopediaModal = (props: Props) => {
|
||||||
|
const uf = UseUfuzzy();
|
||||||
|
|
||||||
|
const { datasource, isOpen, onClose, onChange, query } = props;
|
||||||
|
|
||||||
|
const [variables, setVariables] = useState<Array<SelectableValue<string>>>([]);
|
||||||
|
|
||||||
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
|
|
||||||
|
// metric list
|
||||||
|
const [metrics, setMetrics] = useState<MetricsData>([]);
|
||||||
|
const [hasMetadata, setHasMetadata] = useState<boolean>(true);
|
||||||
|
const [haystack, setHaystack] = useState<string[]>([]);
|
||||||
|
const [nameHaystack, setNameHaystack] = useState<string[]>([]);
|
||||||
|
const [openTabs, setOpenTabs] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// pagination
|
||||||
|
const [resultsPerPage, setResultsPerPage] = useState<number>(DEFAULT_RESULTS_PER_PAGE);
|
||||||
|
const [pageNum, setPageNum] = useState<number>(1);
|
||||||
|
|
||||||
|
// filters
|
||||||
|
const [fuzzySearchQuery, setFuzzySearchQuery] = useState<string>('');
|
||||||
|
const [fuzzyMetaSearchResults, setFuzzyMetaSearchResults] = useState<number[]>([]);
|
||||||
|
const [fuzzyNameSearchResults, setNameFuzzySearchResults] = useState<number[]>([]);
|
||||||
|
const [fullMetaSearch, setFullMetaSearch] = useState<boolean>(false);
|
||||||
|
const [excludeNullMetadata, setExcludeNullMetadata] = useState<boolean>(false);
|
||||||
|
const [selectedTypes, setSelectedTypes] = useState<Array<SelectableValue<string>>>([]);
|
||||||
|
const [letterSearch, setLetterSearch] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// backend search metric names by text
|
||||||
|
const [useBackend, setUseBackend] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const updateMetricsMetadata = useCallback(async () => {
|
||||||
|
// *** Loading Gif
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
// Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we
|
||||||
|
// don't use it with the visual builder and there is no need to run all the start() setup anyway.
|
||||||
|
if (!datasource.languageProvider.metricsMetadata) {
|
||||||
|
await datasource.languageProvider.loadMetricsMetadata();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error handling for when metrics metadata returns as undefined
|
||||||
|
// *** Will have to handle metadata filtering if this happens
|
||||||
|
// *** only display metrics fuzzy search, filter and pagination
|
||||||
|
if (!datasource.languageProvider.metricsMetadata) {
|
||||||
|
setHasMetadata(false);
|
||||||
|
datasource.languageProvider.metricsMetadata = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
// filter by adding the query.labels to the search?
|
||||||
|
// *** do this in the filter???
|
||||||
|
let metrics;
|
||||||
|
if (query.labels.length > 0) {
|
||||||
|
const expr = promQueryModeller.renderLabels(query.labels);
|
||||||
|
metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
|
||||||
|
} else {
|
||||||
|
metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let haystackData: string[] = [];
|
||||||
|
let haystackNameData: string[] = [];
|
||||||
|
let metricsData: MetricsData = metrics.map((m) => {
|
||||||
|
const type = getMetadataType(m, datasource.languageProvider.metricsMetadata!);
|
||||||
|
const description = getMetadataHelp(m, datasource.languageProvider.metricsMetadata!);
|
||||||
|
|
||||||
|
// string[] = name + type + description
|
||||||
|
haystackData.push(`${m} ${type} ${description}`);
|
||||||
|
haystackNameData.push(m);
|
||||||
|
return {
|
||||||
|
value: m,
|
||||||
|
type: type,
|
||||||
|
description: description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// setting this by the backend if useBackend is true
|
||||||
|
setMetrics(metricsData);
|
||||||
|
setHaystack(haystackData);
|
||||||
|
setNameHaystack(haystackNameData);
|
||||||
|
|
||||||
|
setVariables(
|
||||||
|
datasource.getVariables().map((v) => {
|
||||||
|
return {
|
||||||
|
value: v,
|
||||||
|
label: v,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [query, datasource]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
updateMetricsMetadata();
|
||||||
|
}, [updateMetricsMetadata]);
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
|
|
||||||
|
const typeOptions: SelectableValue[] = promTypes.map((t: PromFilterOption) => {
|
||||||
|
return {
|
||||||
|
value: t.value,
|
||||||
|
label: t.value,
|
||||||
|
description: t.description,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
function calculatePageList(metrics: MetricsData, resultsPerPage: number) {
|
||||||
|
if (!metrics.length) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const calcResultsPerPage: number = resultsPerPage === 0 ? 1 : resultsPerPage;
|
||||||
|
|
||||||
|
const pages = Math.floor(filterMetrics(metrics).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);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasMetaDataFilters() {
|
||||||
|
return selectedTypes.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function fuzzySearch(query: string) {
|
||||||
|
// search either the names or all metadata
|
||||||
|
// fuzzy search go!
|
||||||
|
|
||||||
|
if (fullMetaSearch) {
|
||||||
|
// considered simply filtering indexes with reduce and includes
|
||||||
|
// Performance comparison with 13,000 metrics searching metadata
|
||||||
|
// Fuzzy 6326ms
|
||||||
|
// Reduce & Includes 5541ms
|
||||||
|
const metaIdxs = uf.filter(haystack, query.toLowerCase());
|
||||||
|
setFuzzyMetaSearchResults(metaIdxs);
|
||||||
|
} else {
|
||||||
|
const nameIdxs = uf.filter(nameHaystack, query.toLowerCase());
|
||||||
|
setNameFuzzySearchResults(nameIdxs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const debouncedFuzzySearch = debounceLodash((query: string) => {
|
||||||
|
fuzzySearch(query);
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter
|
||||||
|
*
|
||||||
|
* @param metrics
|
||||||
|
* @param skipLetterSearch
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
function filterMetrics(metrics: MetricsData, skipLetterSearch?: boolean): MetricsData {
|
||||||
|
let filteredMetrics: MetricsData = metrics;
|
||||||
|
|
||||||
|
if (fuzzySearchQuery || excludeNullMetadata || (letterSearch && !skipLetterSearch) || selectedTypes.length > 0) {
|
||||||
|
filteredMetrics = filteredMetrics.filter((m: MetricData, idx) => {
|
||||||
|
let keepMetric = false;
|
||||||
|
|
||||||
|
// search by text
|
||||||
|
if (fuzzySearchQuery) {
|
||||||
|
if (useBackend) {
|
||||||
|
// skip for backend!
|
||||||
|
keepMetric = true;
|
||||||
|
} else if (fullMetaSearch) {
|
||||||
|
keepMetric = fuzzyMetaSearchResults.includes(idx);
|
||||||
|
} else {
|
||||||
|
keepMetric = fuzzyNameSearchResults.includes(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// user clicks the alphabet search
|
||||||
|
// backend and frontend
|
||||||
|
if (letterSearch && !skipLetterSearch) {
|
||||||
|
const letters: string[] = [letterSearch, letterSearch.toLowerCase()];
|
||||||
|
keepMetric = letters.includes(m.value[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// select by type, counter, gauge, etc
|
||||||
|
// skip for backend because no metadata is returned
|
||||||
|
if (selectedTypes.length > 0 && !useBackend) {
|
||||||
|
// return the metric that matches the type
|
||||||
|
// return the metric if it has no type AND we are NOT excluding metrics without metadata
|
||||||
|
|
||||||
|
// Matches type
|
||||||
|
const matchesSelectedType = selectedTypes.some((t) => t.value === m.type);
|
||||||
|
|
||||||
|
// missing type
|
||||||
|
const hasNoType = !m.type;
|
||||||
|
|
||||||
|
return matchesSelectedType || (hasNoType && !excludeNullMetadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
return keepMetric;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredMetrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The filtered and paginated metrics displayed in the modal
|
||||||
|
* */
|
||||||
|
function displayedMetrics(metrics: MetricsData) {
|
||||||
|
const filteredSorted: MetricsData = filterMetrics(metrics).sort(alphabetically(true, hasMetaDataFilters()));
|
||||||
|
|
||||||
|
const displayedMetrics: MetricsData = sliceMetrics(filteredSorted, pageNum, resultsPerPage);
|
||||||
|
|
||||||
|
return displayedMetrics;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* The backend debounced search
|
||||||
|
*/
|
||||||
|
const debouncedBackendSearch = useMemo(
|
||||||
|
() =>
|
||||||
|
debounce(async (metricText: string) => {
|
||||||
|
const queryString = regexifyLabelValuesQueryString(metricText);
|
||||||
|
|
||||||
|
const labelsParams = query.labels.map((label) => {
|
||||||
|
return `,${label.label}="${label.value}"`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const params = `label_values({__name__=~".*${queryString}"${
|
||||||
|
query.labels ? labelsParams.join() : ''
|
||||||
|
}},__name__)`;
|
||||||
|
|
||||||
|
const results = datasource.metricFindQuery(params);
|
||||||
|
|
||||||
|
const metrics = await results.then((results) => {
|
||||||
|
return results.map((result) => {
|
||||||
|
return {
|
||||||
|
value: result.text,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
setMetrics(metrics);
|
||||||
|
setIsLoading(false);
|
||||||
|
}, 300),
|
||||||
|
[datasource, query.labels]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
data-testid={testIds.metricModal}
|
||||||
|
isOpen={isOpen}
|
||||||
|
title="Browse Metrics"
|
||||||
|
onDismiss={onClose}
|
||||||
|
aria-label="Metric Encyclopedia"
|
||||||
|
>
|
||||||
|
<div className={styles.spacing}>
|
||||||
|
Browse {metrics.length} metric{metrics.length > 1 ? 's' : ''} by text, by type, alphabetically or select a
|
||||||
|
variable.
|
||||||
|
{isLoading && (
|
||||||
|
<div className={styles.inlineSpinner}>
|
||||||
|
<Spinner></Spinner>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{query.labels.length > 0 && (
|
||||||
|
<div className={styles.spacing}>
|
||||||
|
<i>These metrics have been pre-filtered by labels chosen in the label filters.</i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="gf-form">
|
||||||
|
<Input
|
||||||
|
data-testid={testIds.searchMetric}
|
||||||
|
placeholder={placeholders.browse}
|
||||||
|
value={fuzzySearchQuery}
|
||||||
|
autoFocus
|
||||||
|
onInput={(e) => {
|
||||||
|
const value = e.currentTarget.value ?? '';
|
||||||
|
setFuzzySearchQuery(value);
|
||||||
|
if (useBackend && value === '') {
|
||||||
|
// get all metrics data if a user erases everything in the input
|
||||||
|
updateMetricsMetadata();
|
||||||
|
} else if (useBackend) {
|
||||||
|
setIsLoading(true);
|
||||||
|
debouncedBackendSearch(value);
|
||||||
|
} else {
|
||||||
|
// do the search on the frontend
|
||||||
|
debouncedFuzzySearch(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageNum(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hasMetadata && !useBackend && (
|
||||||
|
<InlineField label="" className={styles.labelColor} tooltip={<div>{placeholders.metadataSearchSwicth}</div>}>
|
||||||
|
<InlineSwitch
|
||||||
|
data-testid={testIds.searchWithMetadata}
|
||||||
|
showLabel={true}
|
||||||
|
value={fullMetaSearch}
|
||||||
|
onChange={() => {
|
||||||
|
setFullMetaSearch(!fullMetaSearch);
|
||||||
|
setPageNum(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
)}
|
||||||
|
<InlineField label="" className={styles.labelColor} tooltip={<div>{placeholders.setUseBackend}</div>}>
|
||||||
|
<InlineSwitch
|
||||||
|
data-testid={testIds.setUseBackend}
|
||||||
|
showLabel={true}
|
||||||
|
value={useBackend}
|
||||||
|
onChange={() => {
|
||||||
|
const newVal = !useBackend;
|
||||||
|
setUseBackend(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 (fuzzySearchQuery !== '') {
|
||||||
|
debouncedBackendSearch(fuzzySearchQuery);
|
||||||
|
}
|
||||||
|
// otherwise wait for user typing
|
||||||
|
}
|
||||||
|
|
||||||
|
setPageNum(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
</div>
|
||||||
|
{hasMetadata && !useBackend && (
|
||||||
|
<>
|
||||||
|
<div className="gf-form">
|
||||||
|
<h6>Filter by Type</h6>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<MultiSelect
|
||||||
|
data-testid={testIds.selectType}
|
||||||
|
inputId="my-select"
|
||||||
|
options={typeOptions}
|
||||||
|
value={selectedTypes}
|
||||||
|
placeholder={placeholders.type}
|
||||||
|
onChange={(v) => {
|
||||||
|
// *** Filter by type
|
||||||
|
// *** always include metrics without metadata but label it as unknown type
|
||||||
|
// Consider tabs select instead of actual select or multi select
|
||||||
|
setSelectedTypes(v);
|
||||||
|
setPageNum(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{hasMetadata && (
|
||||||
|
<InlineField label="" className={styles.labelColor} tooltip={<div>{placeholders.excludeNoMetadata}</div>}>
|
||||||
|
<InlineSwitch
|
||||||
|
showLabel={true}
|
||||||
|
value={excludeNullMetadata}
|
||||||
|
onChange={() => {
|
||||||
|
setExcludeNullMetadata(!excludeNullMetadata);
|
||||||
|
setPageNum(1);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</InlineField>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<div className="gf-form">
|
||||||
|
<h6>Variables</h6>
|
||||||
|
</div>
|
||||||
|
<div className="gf-form">
|
||||||
|
<Select
|
||||||
|
inputId="my-select"
|
||||||
|
options={variables}
|
||||||
|
value={''}
|
||||||
|
placeholder={placeholders.variables}
|
||||||
|
onChange={(v) => {
|
||||||
|
const value: string = v.value ?? '';
|
||||||
|
onChange({ ...query, metric: value });
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h5 className={`${styles.center} ${styles.topPadding}`}>Results</h5>
|
||||||
|
<div className={`${styles.center} ${styles.bottomPadding}`}>
|
||||||
|
{[
|
||||||
|
'A',
|
||||||
|
'B',
|
||||||
|
'C',
|
||||||
|
'D',
|
||||||
|
'E',
|
||||||
|
'F',
|
||||||
|
'G',
|
||||||
|
'H',
|
||||||
|
'I',
|
||||||
|
'J',
|
||||||
|
'K',
|
||||||
|
'L',
|
||||||
|
'M',
|
||||||
|
'N',
|
||||||
|
'O',
|
||||||
|
'P',
|
||||||
|
'Q',
|
||||||
|
'R',
|
||||||
|
'S',
|
||||||
|
'T',
|
||||||
|
'U',
|
||||||
|
'V',
|
||||||
|
'W',
|
||||||
|
'X',
|
||||||
|
'Y',
|
||||||
|
'Z',
|
||||||
|
].map((letter, idx, coll) => {
|
||||||
|
const active: boolean = filterMetrics(metrics, true).some((m: MetricData) => {
|
||||||
|
return m.value[0] === letter || m.value[0] === letter?.toLowerCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
// starts with letter search
|
||||||
|
// filter by starts with letter
|
||||||
|
// if same letter searched null out remove letter search
|
||||||
|
function updateLetterSearch() {
|
||||||
|
if (letterSearch === letter) {
|
||||||
|
setLetterSearch(null);
|
||||||
|
} else {
|
||||||
|
setLetterSearch(letter);
|
||||||
|
}
|
||||||
|
setPageNum(1);
|
||||||
|
}
|
||||||
|
// selected letter to filter by
|
||||||
|
const selectedClass: string = letterSearch === letter ? styles.selAlpha : '';
|
||||||
|
// these letters are represented in the list of metrics
|
||||||
|
const activeClass: string = active ? styles.active : styles.gray;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
onClick={active ? updateLetterSearch : () => {}}
|
||||||
|
className={`${selectedClass} ${activeClass}`}
|
||||||
|
key={letter}
|
||||||
|
data-testid={'letter-' + letter}
|
||||||
|
>
|
||||||
|
{letter + ' '}
|
||||||
|
{/* {idx !== coll.length - 1 ? '|': ''} */}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
{metrics &&
|
||||||
|
displayedMetrics(metrics).map((metric: MetricData, idx) => {
|
||||||
|
return (
|
||||||
|
<Collapse
|
||||||
|
aria-label={`open and close ${metric.value} query starter card`}
|
||||||
|
data-testid={testIds.metricCard}
|
||||||
|
key={metric.value}
|
||||||
|
label={metric.value}
|
||||||
|
isOpen={openTabs.includes(metric.value)}
|
||||||
|
collapsible={true}
|
||||||
|
onToggle={() =>
|
||||||
|
setOpenTabs((tabs) =>
|
||||||
|
// close tab if it's already open, otherwise open it
|
||||||
|
tabs.includes(metric.value) ? tabs.filter((t) => t !== metric.value) : [...tabs, metric.value]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={styles.cardsContainer}>
|
||||||
|
<Card className={styles.card}>
|
||||||
|
<Card.Description>
|
||||||
|
{metric.description && metric.type ? (
|
||||||
|
<>
|
||||||
|
Type: <span className={styles.metadata}>{metric.type}</span>
|
||||||
|
<br />
|
||||||
|
Description: <span className={styles.metadata}>{metric.description}</span>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<i>No metadata available</i>
|
||||||
|
)}
|
||||||
|
</Card.Description>
|
||||||
|
<Card.Actions>
|
||||||
|
{/* *** Make selecting a metric easier, consider click on text */}
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
aria-label="use this metric button"
|
||||||
|
data-testid={testIds.useMetric}
|
||||||
|
onClick={() => {
|
||||||
|
onChange({ ...query, metric: metric.value });
|
||||||
|
reportInteraction('grafana_prom_metric_encycopedia_tracking', {
|
||||||
|
metric: metric.value,
|
||||||
|
hasVariables: variables.length > 0,
|
||||||
|
hasMetadata: hasMetadata,
|
||||||
|
totalMetricCount: metrics.length,
|
||||||
|
fuzzySearchQuery: fuzzySearchQuery,
|
||||||
|
fullMetaSearch: fullMetaSearch,
|
||||||
|
selectedTypes: selectedTypes,
|
||||||
|
letterSearch: letterSearch,
|
||||||
|
});
|
||||||
|
onClose();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Use this metric
|
||||||
|
</Button>
|
||||||
|
</Card.Actions>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</Collapse>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
<div className="gf-form">
|
||||||
|
<InlineLabel width={20} className="query-keyword">
|
||||||
|
Select Page
|
||||||
|
</InlineLabel>
|
||||||
|
<Select
|
||||||
|
data-testid={testIds.searchPage}
|
||||||
|
options={calculatePageList(metrics, resultsPerPage).map((p) => {
|
||||||
|
return { value: p, label: '' + p };
|
||||||
|
})}
|
||||||
|
value={pageNum ?? 1}
|
||||||
|
placeholder="select page"
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.value ?? 1;
|
||||||
|
setPageNum(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<InlineLabel width={20} className="query-keyword">
|
||||||
|
# results per page
|
||||||
|
</InlineLabel>
|
||||||
|
<Input
|
||||||
|
data-testid={testIds.resultsPerPage}
|
||||||
|
value={resultsPerPage ?? 10}
|
||||||
|
placeholder="results per page"
|
||||||
|
onInput={(e) => {
|
||||||
|
const value = +e.currentTarget.value;
|
||||||
|
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setResultsPerPage(value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<br />
|
||||||
|
<Button aria-label="close metric encyclopedia modal" variant="secondary" onClick={onClose}>
|
||||||
|
Close
|
||||||
|
</Button>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
function alphabetically(ascending: boolean, metadataFilters: boolean) {
|
||||||
|
return function (a: MetricData, b: MetricData) {
|
||||||
|
// equal items sort equally
|
||||||
|
if (a.value === b.value) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *** NO METADATA? SORT LAST
|
||||||
|
// undefined metadata sort after anything else
|
||||||
|
// if filters are on
|
||||||
|
if (metadataFilters) {
|
||||||
|
if (a.type === undefined) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
if (b.type === undefined) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise, if we're ascending, lowest sorts first
|
||||||
|
if (ascending) {
|
||||||
|
return a.value < b.value ? -1 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if descending, highest sorts first
|
||||||
|
return a.value < b.value ? 1 : -1;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function UseUfuzzy(): uFuzzy {
|
||||||
|
const ref = useRef<uFuzzy>();
|
||||||
|
|
||||||
|
if (!ref.current) {
|
||||||
|
ref.current = new uFuzzy({
|
||||||
|
intraMode: 1,
|
||||||
|
intraIns: 1,
|
||||||
|
intraSub: 1,
|
||||||
|
intraTrn: 1,
|
||||||
|
intraDel: 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref.current;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
cardsContainer: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
`,
|
||||||
|
spacing: css`
|
||||||
|
margin-bottom: ${theme.spacing(1)};
|
||||||
|
`,
|
||||||
|
center: css`
|
||||||
|
text-align: center;
|
||||||
|
padding: 4px;
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
topPadding: css`
|
||||||
|
padding: 10px 0 0 0;
|
||||||
|
`,
|
||||||
|
bottomPadding: css`
|
||||||
|
padding: 0 0 4px 0;
|
||||||
|
`,
|
||||||
|
card: css`
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`,
|
||||||
|
selAlpha: css`
|
||||||
|
font-style: italic;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #6e9fff;
|
||||||
|
`,
|
||||||
|
active: css`
|
||||||
|
cursor: pointer;
|
||||||
|
`,
|
||||||
|
gray: css`
|
||||||
|
color: grey;
|
||||||
|
`,
|
||||||
|
metadata: css`
|
||||||
|
color: rgb(204, 204, 220);
|
||||||
|
`,
|
||||||
|
labelColor: css`
|
||||||
|
color: #6e9fff;
|
||||||
|
`,
|
||||||
|
inlineSpinner: css`
|
||||||
|
display: inline-block;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const testIds = {
|
||||||
|
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',
|
||||||
|
};
|
@ -1,7 +1,10 @@
|
|||||||
|
import { css } from '@emotion/css';
|
||||||
import React, { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { DataSourceApi, PanelData, SelectableValue } from '@grafana/data';
|
import { DataSourceApi, GrafanaTheme2, PanelData, SelectableValue } from '@grafana/data';
|
||||||
import { EditorRow } from '@grafana/experimental';
|
import { EditorRow } from '@grafana/experimental';
|
||||||
|
import { config } from '@grafana/runtime';
|
||||||
|
import { Button, Tag, useStyles2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import { getMetadataString } from '../../language_provider';
|
import { getMetadataString } from '../../language_provider';
|
||||||
@ -19,6 +22,7 @@ import { QueryBuilderLabelFilter, QueryBuilderOperation } from '../shared/types'
|
|||||||
import { PromVisualQuery } from '../types';
|
import { PromVisualQuery } from '../types';
|
||||||
|
|
||||||
import { LabelFilters } from './LabelFilters';
|
import { LabelFilters } from './LabelFilters';
|
||||||
|
import { MetricEncyclopediaModal } from './MetricEncyclopediaModal';
|
||||||
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
|
import { MetricSelect, PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './MetricSelect';
|
||||||
import { NestedQueryList } from './NestedQueryList';
|
import { NestedQueryList } from './NestedQueryList';
|
||||||
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
|
import { EXPLAIN_LABEL_FILTER_CONTENT } from './PromQueryBuilderExplained';
|
||||||
@ -35,10 +39,12 @@ export interface Props {
|
|||||||
export const PromQueryBuilder = React.memo<Props>((props) => {
|
export const PromQueryBuilder = React.memo<Props>((props) => {
|
||||||
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
const { datasource, query, onChange, onRunQuery, data, showExplain } = props;
|
||||||
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
const [highlightedOp, setHighlightedOp] = useState<QueryBuilderOperation | undefined>();
|
||||||
|
const [metricEncyclopediaModalOpen, setMetricEncyclopediaModalOpen] = useState(false);
|
||||||
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
const onChangeLabels = (labels: QueryBuilderLabelFilter[]) => {
|
||||||
onChange({ ...query, labels });
|
onChange({ ...query, labels });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const styles = useStyles2(getStyles);
|
||||||
/**
|
/**
|
||||||
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
|
* Map metric metadata to SelectableValue for Select component and also adds defined template variables to the list.
|
||||||
*/
|
*/
|
||||||
@ -202,17 +208,51 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
|||||||
}, [datasource, query, withTemplateVariableOptions]);
|
}, [datasource, query, withTemplateVariableOptions]);
|
||||||
|
|
||||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||||
|
const MetricEncyclopedia = config.featureToggles.prometheusMetricEncyclopedia;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<EditorRow>
|
<EditorRow>
|
||||||
<MetricSelect
|
{MetricEncyclopedia ? (
|
||||||
query={query}
|
<>
|
||||||
onChange={onChange}
|
<Button
|
||||||
onGetMetrics={onGetMetrics}
|
className={styles.button}
|
||||||
datasource={datasource}
|
variant="secondary"
|
||||||
labelsFilters={query.labels}
|
size="sm"
|
||||||
/>
|
onClick={() => setMetricEncyclopediaModalOpen((prevValue) => !prevValue)}
|
||||||
|
>
|
||||||
|
Metric Encyclopedia
|
||||||
|
</Button>
|
||||||
|
{query.metric && (
|
||||||
|
<Tag
|
||||||
|
name={query.metric}
|
||||||
|
color="#3D71D9"
|
||||||
|
onClick={() => {
|
||||||
|
onChange({ ...query, metric: '' });
|
||||||
|
}}
|
||||||
|
title="Click to remove metric"
|
||||||
|
className={styles.metricTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{metricEncyclopediaModalOpen && (
|
||||||
|
<MetricEncyclopediaModal
|
||||||
|
datasource={datasource}
|
||||||
|
isOpen={metricEncyclopediaModalOpen}
|
||||||
|
onClose={() => setMetricEncyclopediaModalOpen(false)}
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<MetricSelect
|
||||||
|
query={query}
|
||||||
|
onChange={onChange}
|
||||||
|
onGetMetrics={onGetMetrics}
|
||||||
|
datasource={datasource}
|
||||||
|
labelsFilters={query.labels}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<LabelFilters
|
<LabelFilters
|
||||||
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
|
getLabelValuesAutofillSuggestions={getLabelValuesAutocompleteSuggestions}
|
||||||
labelsFilters={query.labels}
|
labelsFilters={query.labels}
|
||||||
@ -308,3 +348,15 @@ async function getMetrics(
|
|||||||
}
|
}
|
||||||
|
|
||||||
PromQueryBuilder.displayName = 'PromQueryBuilder';
|
PromQueryBuilder.displayName = 'PromQueryBuilder';
|
||||||
|
|
||||||
|
const getStyles = (theme: GrafanaTheme2) => {
|
||||||
|
return {
|
||||||
|
button: css`
|
||||||
|
height: auto;
|
||||||
|
`,
|
||||||
|
metricTag: css`
|
||||||
|
margin: '10px 0 10px 0',
|
||||||
|
backgroundColor: '#3D71D9',
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user