mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 19:22:25 +08:00
Chore: Convert prometheus query field to a functional component (#101515)
* convert it to functional component * useReducer * usePromQueryFieldEffects * clean up the code * remove localStorage provider * introduce usePromQueryFieldEffects.test.ts * simpler state management * remove mocks * linting + betterer * remove unnecessary check * use range * remove unused languageProvider * prettier
This commit is contained in:
@ -401,9 +401,6 @@ exports[`better eslint`] = {
|
|||||||
"packages/grafana-o11y-ds-frontend/src/createNodeGraphFrames.ts:5381": [
|
"packages/grafana-o11y-ds-frontend/src/createNodeGraphFrames.ts:5381": [
|
||||||
[0, 0, 0, "Do not use any type assertions.", "0"]
|
[0, 0, 0, "Do not use any type assertions.", "0"]
|
||||||
],
|
],
|
||||||
"packages/grafana-prometheus/src/components/PromQueryField.test.tsx:5381": [
|
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
|
||||||
],
|
|
||||||
"packages/grafana-prometheus/src/components/PromQueryField.tsx:5381": [
|
"packages/grafana-prometheus/src/components/PromQueryField.tsx:5381": [
|
||||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
|
||||||
],
|
],
|
||||||
|
@ -24,7 +24,6 @@ jest.mock('./monaco-query-field/MonacoQueryFieldLazy', () => {
|
|||||||
|
|
||||||
function setup(app: CoreApp): { onRunQuery: jest.Mock } {
|
function setup(app: CoreApp): { onRunQuery: jest.Mock } {
|
||||||
const dataSource = {
|
const dataSource = {
|
||||||
getInitHints: () => [],
|
|
||||||
getPrometheusTime: jest.fn((date, roundup) => 123),
|
getPrometheusTime: jest.fn((date, roundup) => 123),
|
||||||
getQueryHints: jest.fn(() => []),
|
getQueryHints: jest.fn(() => []),
|
||||||
getDebounceTimeInMilliseconds: jest.fn(() => 300),
|
getDebounceTimeInMilliseconds: jest.fn(() => 300),
|
||||||
|
@ -1,12 +1,12 @@
|
|||||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx
|
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.test.tsx
|
||||||
import { getByTestId, render, screen, waitFor } from '@testing-library/react';
|
import { getByTestId, render, screen } from '@testing-library/react';
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
import { CoreApp, DataFrame, LoadingState, PanelData } from '@grafana/data';
|
import { CoreApp, DataFrame, dateTime, LoadingState, PanelData } from '@grafana/data';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource } from '../datasource';
|
||||||
import PromQlLanguageProvider from '../language_provider';
|
import * as queryHints from '../query_hints';
|
||||||
|
|
||||||
import { PromQueryField } from './PromQueryField';
|
import { PromQueryField } from './PromQueryField';
|
||||||
import { Props } from './monaco-query-field/MonacoQueryFieldProps';
|
import { Props } from './monaco-query-field/MonacoQueryFieldProps';
|
||||||
@ -31,7 +31,6 @@ const defaultProps = {
|
|||||||
getLabelKeys: () => [],
|
getLabelKeys: () => [],
|
||||||
metrics: [],
|
metrics: [],
|
||||||
},
|
},
|
||||||
getInitHints: () => [],
|
|
||||||
} as unknown as PrometheusDatasource,
|
} as unknown as PrometheusDatasource,
|
||||||
query: {
|
query: {
|
||||||
expr: '',
|
expr: '',
|
||||||
@ -40,6 +39,14 @@ const defaultProps = {
|
|||||||
onRunQuery: () => {},
|
onRunQuery: () => {},
|
||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
history: [],
|
history: [],
|
||||||
|
range: {
|
||||||
|
from: dateTime('2022-01-01T00:00:00Z'),
|
||||||
|
to: dateTime('2022-01-02T00:00:00Z'),
|
||||||
|
raw: {
|
||||||
|
from: 'now-1d',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('PromQueryField', () => {
|
describe('PromQueryField', () => {
|
||||||
@ -48,6 +55,10 @@ describe('PromQueryField', () => {
|
|||||||
window.getSelection = () => {};
|
window.getSelection = () => {};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('renders metrics chooser regularly if lookups are not disabled in the datasource settings', async () => {
|
it('renders metrics chooser regularly if lookups are not disabled in the datasource settings', async () => {
|
||||||
const queryField = render(<PromQueryField {...defaultProps} />);
|
const queryField = render(<PromQueryField {...defaultProps} />);
|
||||||
|
|
||||||
@ -72,7 +83,9 @@ describe('PromQueryField', () => {
|
|||||||
it('renders an initial hint if no data and initial hint provided', async () => {
|
it('renders an initial hint if no data and initial hint provided', async () => {
|
||||||
const props = defaultProps;
|
const props = defaultProps;
|
||||||
props.datasource.lookupsDisabled = true;
|
props.datasource.lookupsDisabled = true;
|
||||||
props.datasource.getInitHints = () => [{ label: 'Initial hint', type: 'INFO' }];
|
|
||||||
|
jest.spyOn(queryHints, 'getInitHints').mockReturnValue([{ label: 'Initial hint', type: 'INFO' }]);
|
||||||
|
|
||||||
render(<PromQueryField {...props} />);
|
render(<PromQueryField {...props} />);
|
||||||
|
|
||||||
// wait for component to render
|
// wait for component to render
|
||||||
@ -84,7 +97,6 @@ describe('PromQueryField', () => {
|
|||||||
it('renders query hint if data, query hint and initial hint provided', async () => {
|
it('renders query hint if data, query hint and initial hint provided', async () => {
|
||||||
const props = defaultProps;
|
const props = defaultProps;
|
||||||
props.datasource.lookupsDisabled = true;
|
props.datasource.lookupsDisabled = true;
|
||||||
props.datasource.getInitHints = () => [{ label: 'Initial hint', type: 'INFO' }];
|
|
||||||
props.datasource.getQueryHints = () => [{ label: 'Query hint', type: 'INFO' }];
|
props.datasource.getQueryHints = () => [{ label: 'Query hint', type: 'INFO' }];
|
||||||
render(
|
render(
|
||||||
<PromQueryField
|
<PromQueryField
|
||||||
@ -105,51 +117,6 @@ describe('PromQueryField', () => {
|
|||||||
expect(screen.queryByText('Initial hint')).not.toBeInTheDocument();
|
expect(screen.queryByText('Initial hint')).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('refreshes metrics when the data source changes', async () => {
|
|
||||||
const defaultProps = {
|
|
||||||
query: { expr: '', refId: '' },
|
|
||||||
onRunQuery: () => {},
|
|
||||||
onChange: () => {},
|
|
||||||
history: [],
|
|
||||||
};
|
|
||||||
const metrics = ['foo', 'bar'];
|
|
||||||
const queryField = render(
|
|
||||||
<PromQueryField
|
|
||||||
datasource={
|
|
||||||
{
|
|
||||||
languageProvider: makeLanguageProvider({ metrics: [metrics] }),
|
|
||||||
getInitHints: () => [],
|
|
||||||
} as unknown as PrometheusDatasource
|
|
||||||
}
|
|
||||||
{...defaultProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// wait for component to render
|
|
||||||
await screen.findByRole('button');
|
|
||||||
|
|
||||||
const changedMetrics = ['baz', 'moo'];
|
|
||||||
queryField.rerender(
|
|
||||||
<PromQueryField
|
|
||||||
// @ts-ignore
|
|
||||||
datasource={{
|
|
||||||
languageProvider: makeLanguageProvider({ metrics: [changedMetrics] }),
|
|
||||||
}}
|
|
||||||
{...defaultProps}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// If we check the label browser right away it should be in loading state
|
|
||||||
let labelBrowser = screen.getByRole('button');
|
|
||||||
expect(labelBrowser).toHaveTextContent('Loading');
|
|
||||||
|
|
||||||
// wait for component to rerender
|
|
||||||
labelBrowser = await screen.findByRole('button');
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(labelBrowser).toHaveTextContent('Metrics browser');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not run query onBlur', async () => {
|
it('should not run query onBlur', async () => {
|
||||||
const onRunQuery = jest.fn();
|
const onRunQuery = jest.fn();
|
||||||
const { container } = render(<PromQueryField {...defaultProps} app={CoreApp.Explore} onRunQuery={onRunQuery} />);
|
const { container } = render(<PromQueryField {...defaultProps} app={CoreApp.Explore} onRunQuery={onRunQuery} />);
|
||||||
@ -166,18 +133,3 @@ describe('PromQueryField', () => {
|
|||||||
expect(onRunQuery).not.toHaveBeenCalled();
|
expect(onRunQuery).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function makeLanguageProvider(options: { metrics: string[][] }) {
|
|
||||||
const metricsStack = [...options.metrics];
|
|
||||||
return {
|
|
||||||
histogramMetrics: [],
|
|
||||||
metrics: [],
|
|
||||||
metricsMetadata: {},
|
|
||||||
lookupsDisabled: false,
|
|
||||||
getLabelKeys: () => [],
|
|
||||||
start() {
|
|
||||||
this.metrics = metricsStack.shift();
|
|
||||||
return Promise.resolve([]);
|
|
||||||
},
|
|
||||||
} as any as PromQlLanguageProvider;
|
|
||||||
}
|
|
||||||
|
@ -1,118 +1,96 @@
|
|||||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
|
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx
|
||||||
import { css, cx } from '@emotion/css';
|
import { css, cx } from '@emotion/css';
|
||||||
import { PureComponent, ReactNode } from 'react';
|
import { MutableRefObject, ReactNode, useCallback, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import { getDefaultTimeRange, isDataFrame, QueryEditorProps, QueryHint, toLegacyResponseData } from '@grafana/data';
|
||||||
getDefaultTimeRange,
|
|
||||||
isDataFrame,
|
|
||||||
LocalStorageValueProvider,
|
|
||||||
QueryEditorProps,
|
|
||||||
QueryHint,
|
|
||||||
TimeRange,
|
|
||||||
toLegacyResponseData,
|
|
||||||
} from '@grafana/data';
|
|
||||||
import { selectors } from '@grafana/e2e-selectors';
|
import { selectors } from '@grafana/e2e-selectors';
|
||||||
import { reportInteraction } from '@grafana/runtime';
|
import { reportInteraction } from '@grafana/runtime';
|
||||||
import { clearButtonStyles, Icon, Themeable2, withTheme2 } from '@grafana/ui';
|
import { clearButtonStyles, Icon, useTheme2 } from '@grafana/ui';
|
||||||
|
|
||||||
import { PrometheusDatasource } from '../datasource';
|
import { PrometheusDatasource } from '../datasource';
|
||||||
import { roundMsToMin } from '../language_utils';
|
import { getInitHints } from '../query_hints';
|
||||||
import { PromOptions, PromQuery } from '../types';
|
import { PromOptions, PromQuery } from '../types';
|
||||||
|
|
||||||
import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser';
|
import { PrometheusMetricsBrowser } from './PrometheusMetricsBrowser';
|
||||||
import { CancelablePromise, isCancelablePromiseRejection, makePromiseCancelable } from './cancelable-promise';
|
import { CancelablePromise, isCancelablePromiseRejection, makePromiseCancelable } from './cancelable-promise';
|
||||||
import { MonacoQueryFieldWrapper } from './monaco-query-field/MonacoQueryFieldWrapper';
|
import { MonacoQueryFieldWrapper } from './monaco-query-field/MonacoQueryFieldWrapper';
|
||||||
|
import { useMetricsState } from './useMetricsState';
|
||||||
|
import { usePromQueryFieldEffects } from './usePromQueryFieldEffects';
|
||||||
|
|
||||||
const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels';
|
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions> {
|
||||||
|
|
||||||
function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, hasMetrics: boolean) {
|
|
||||||
if (metricsLookupDisabled) {
|
|
||||||
return '(Disabled)';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasSyntax) {
|
|
||||||
return 'Loading metrics...';
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hasMetrics) {
|
|
||||||
return '(No metrics found)';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Metrics browser';
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PromQueryFieldProps extends QueryEditorProps<PrometheusDatasource, PromQuery, PromOptions>, Themeable2 {
|
|
||||||
ExtraFieldElement?: ReactNode;
|
ExtraFieldElement?: ReactNode;
|
||||||
'data-testid'?: string;
|
'data-testid'?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PromQueryFieldState {
|
export const PromQueryField = (props: PromQueryFieldProps) => {
|
||||||
labelBrowserVisible: boolean;
|
const {
|
||||||
syntaxLoaded: boolean;
|
app,
|
||||||
hint: QueryHint | null;
|
datasource,
|
||||||
}
|
datasource: { languageProvider },
|
||||||
|
query,
|
||||||
|
ExtraFieldElement,
|
||||||
|
history = [],
|
||||||
|
data,
|
||||||
|
range,
|
||||||
|
onChange,
|
||||||
|
onRunQuery,
|
||||||
|
} = props;
|
||||||
|
|
||||||
class PromQueryFieldClass extends PureComponent<PromQueryFieldProps, PromQueryFieldState> {
|
const theme = useTheme2();
|
||||||
declare languageProviderInitializationPromise: CancelablePromise<any>;
|
|
||||||
|
|
||||||
constructor(props: PromQueryFieldProps) {
|
const [syntaxLoaded, setSyntaxLoaded] = useState(false);
|
||||||
super(props);
|
const [hint, setHint] = useState<QueryHint | null>(null);
|
||||||
|
const [labelBrowserVisible, setLabelBrowserVisible] = useState(false);
|
||||||
|
|
||||||
this.state = {
|
const updateLanguage = useCallback(() => {
|
||||||
labelBrowserVisible: false,
|
if (languageProvider.metrics) {
|
||||||
syntaxLoaded: false,
|
setSyntaxLoaded(true);
|
||||||
hint: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.datasource.languageProvider) {
|
|
||||||
this.refreshMetrics();
|
|
||||||
}
|
}
|
||||||
this.refreshHint();
|
}, [languageProvider]);
|
||||||
}
|
|
||||||
|
|
||||||
componentWillUnmount() {
|
const refreshMetrics = useCallback(
|
||||||
if (this.languageProviderInitializationPromise) {
|
async (languageProviderInitRef: MutableRefObject<CancelablePromise<any> | null>) => {
|
||||||
this.languageProviderInitializationPromise.cancel();
|
// Cancel any existing initialization using the ref
|
||||||
}
|
if (languageProviderInitRef.current) {
|
||||||
}
|
languageProviderInitRef.current.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate(prevProps: PromQueryFieldProps) {
|
if (!languageProvider || !range) {
|
||||||
const {
|
return;
|
||||||
data,
|
}
|
||||||
datasource: { languageProvider },
|
|
||||||
range,
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
if (languageProvider !== prevProps.datasource.languageProvider) {
|
try {
|
||||||
// We reset this only on DS change so we do not flesh loading state on every rangeChange which happens on every
|
const initialization = makePromiseCancelable(languageProvider.start(range));
|
||||||
// query run if using relative range.
|
languageProviderInitRef.current = initialization;
|
||||||
this.setState({
|
|
||||||
syntaxLoaded: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const changedRangeToRefresh = this.rangeChangedToRefresh(range, prevProps.range);
|
const remainingTasks = await initialization.promise;
|
||||||
// We want to refresh metrics when language provider changes and/or when range changes (we round up intervals to a minute)
|
|
||||||
if (languageProvider !== prevProps.datasource.languageProvider || changedRangeToRefresh) {
|
|
||||||
this.refreshMetrics();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data && prevProps.data && prevProps.data.series !== data.series) {
|
// If there are remaining tasks, wait for them
|
||||||
this.refreshHint();
|
if (Array.isArray(remainingTasks) && remainingTasks.length > 0) {
|
||||||
}
|
await Promise.all(remainingTasks);
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshHint = () => {
|
updateLanguage();
|
||||||
const { datasource, query, data } = this.props;
|
} catch (err) {
|
||||||
const initHints = datasource.getInitHints();
|
if (isCancelablePromiseRejection(err) && err.isCanceled) {
|
||||||
const initHint = initHints.length > 0 ? initHints[0] : null;
|
// do nothing, promise was canceled
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
languageProviderInitRef.current = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[languageProvider, range, updateLanguage]
|
||||||
|
);
|
||||||
|
|
||||||
if (!data || data.series.length === 0) {
|
const refreshHint = useCallback(() => {
|
||||||
this.setState({
|
const initHints = getInitHints(datasource);
|
||||||
hint: initHint,
|
const initHint = initHints[0] ?? null;
|
||||||
});
|
|
||||||
|
// If no data or empty series, use default hint
|
||||||
|
if (!data?.series?.length) {
|
||||||
|
setHint(initHint);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -120,180 +98,108 @@ class PromQueryFieldClass extends PureComponent<PromQueryFieldProps, PromQueryFi
|
|||||||
const queryHints = datasource.getQueryHints(query, result);
|
const queryHints = datasource.getQueryHints(query, result);
|
||||||
let queryHint = queryHints.length > 0 ? queryHints[0] : null;
|
let queryHint = queryHints.length > 0 ? queryHints[0] : null;
|
||||||
|
|
||||||
this.setState({ hint: queryHint ?? initHint });
|
setHint(queryHint ?? initHint);
|
||||||
};
|
}, [data, datasource, query]);
|
||||||
|
|
||||||
refreshMetrics = async () => {
|
const onChangeQuery = (value: string, override?: boolean) => {
|
||||||
const {
|
if (!onChange) {
|
||||||
range,
|
return;
|
||||||
datasource: { languageProvider },
|
|
||||||
} = this.props;
|
|
||||||
|
|
||||||
this.languageProviderInitializationPromise = makePromiseCancelable(languageProvider.start(range));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const remainingTasks = await this.languageProviderInitializationPromise.promise;
|
|
||||||
await Promise.all(remainingTasks);
|
|
||||||
this.onUpdateLanguage();
|
|
||||||
} catch (err) {
|
|
||||||
if (isCancelablePromiseRejection(err) && err.isCanceled) {
|
|
||||||
// do nothing, promise was canceled
|
|
||||||
} else {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
rangeChangedToRefresh(range?: TimeRange, prevRange?: TimeRange): boolean {
|
|
||||||
if (range && prevRange) {
|
|
||||||
const sameMinuteFrom = roundMsToMin(range.from.valueOf()) === roundMsToMin(prevRange.from.valueOf());
|
|
||||||
const sameMinuteTo = roundMsToMin(range.to.valueOf()) === roundMsToMin(prevRange.to.valueOf());
|
|
||||||
// If both are same, don't need to refresh.
|
|
||||||
return !(sameMinuteFrom && sameMinuteTo);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* TODO #33976: Remove this, add histogram group (query = `histogram_quantile(0.95, sum(rate(${metric}[5m])) by (le))`;)
|
|
||||||
*/
|
|
||||||
onChangeLabelBrowser = (selector: string) => {
|
|
||||||
this.onChangeQuery(selector, true);
|
|
||||||
this.setState({ labelBrowserVisible: false });
|
|
||||||
};
|
|
||||||
|
|
||||||
onChangeQuery = (value: string, override?: boolean) => {
|
|
||||||
// Send text change to parent
|
// Send text change to parent
|
||||||
const { query, onChange, onRunQuery } = this.props;
|
const nextQuery: PromQuery = { ...query, expr: value };
|
||||||
if (onChange) {
|
onChange(nextQuery);
|
||||||
const nextQuery: PromQuery = { ...query, expr: value };
|
|
||||||
onChange(nextQuery);
|
|
||||||
|
|
||||||
if (override && onRunQuery) {
|
if (override && onRunQuery) {
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickChooserButton = () => {
|
const onChangeLabelBrowser = (selector: string) => {
|
||||||
this.setState((state) => ({ labelBrowserVisible: !state.labelBrowserVisible }));
|
onChangeQuery(selector, true);
|
||||||
|
setLabelBrowserVisible(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onClickChooserButton = () => {
|
||||||
|
setLabelBrowserVisible(!labelBrowserVisible);
|
||||||
|
|
||||||
reportInteraction('user_grafana_prometheus_metrics_browser_clicked', {
|
reportInteraction('user_grafana_prometheus_metrics_browser_clicked', {
|
||||||
editorMode: this.state.labelBrowserVisible ? 'metricViewClosed' : 'metricViewOpen',
|
editorMode: labelBrowserVisible ? 'metricViewClosed' : 'metricViewOpen',
|
||||||
app: this.props?.app ?? '',
|
app: app ?? '',
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onClickHintFix = () => {
|
const onClickHintFix = () => {
|
||||||
const { datasource, query, onChange, onRunQuery } = this.props;
|
|
||||||
const { hint } = this.state;
|
|
||||||
if (hint?.fix?.action) {
|
if (hint?.fix?.action) {
|
||||||
onChange(datasource.modifyQuery(query, hint.fix.action));
|
onChange(datasource.modifyQuery(query, hint.fix.action));
|
||||||
}
|
}
|
||||||
onRunQuery();
|
onRunQuery();
|
||||||
};
|
};
|
||||||
|
|
||||||
onUpdateLanguage = () => {
|
// Use our custom effects hook
|
||||||
const {
|
usePromQueryFieldEffects(range, data?.series, refreshMetrics, refreshHint);
|
||||||
datasource: { languageProvider },
|
|
||||||
} = this.props;
|
|
||||||
const { metrics } = languageProvider;
|
|
||||||
|
|
||||||
if (!metrics) {
|
const { chooserText, buttonDisabled } = useMetricsState(datasource, languageProvider, syntaxLoaded);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setState({ syntaxLoaded: true });
|
return (
|
||||||
};
|
<>
|
||||||
|
<div
|
||||||
|
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
|
||||||
|
data-testid={props['data-testid']}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="gf-form-label query-keyword pointer"
|
||||||
|
onClick={onClickChooserButton}
|
||||||
|
disabled={buttonDisabled}
|
||||||
|
type="button"
|
||||||
|
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton}
|
||||||
|
>
|
||||||
|
{chooserText}
|
||||||
|
<Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />
|
||||||
|
</button>
|
||||||
|
|
||||||
render() {
|
<div className="flex-grow-1 min-width-15">
|
||||||
const {
|
<MonacoQueryFieldWrapper
|
||||||
datasource,
|
languageProvider={languageProvider}
|
||||||
datasource: { languageProvider },
|
history={history}
|
||||||
query,
|
onChange={onChangeQuery}
|
||||||
ExtraFieldElement,
|
onRunQuery={onRunQuery}
|
||||||
history = [],
|
initialValue={query.expr ?? ''}
|
||||||
theme,
|
placeholder="Enter a PromQL query…"
|
||||||
} = this.props;
|
datasource={datasource}
|
||||||
|
timeRange={range ?? getDefaultTimeRange()}
|
||||||
const { labelBrowserVisible, syntaxLoaded, hint } = this.state;
|
/>
|
||||||
const hasMetrics = languageProvider.metrics.length > 0;
|
</div>
|
||||||
const chooserText = getChooserText(datasource.lookupsDisabled, syntaxLoaded, hasMetrics);
|
</div>
|
||||||
const buttonDisabled = !(syntaxLoaded && hasMetrics);
|
{labelBrowserVisible && (
|
||||||
|
<div className="gf-form">
|
||||||
return (
|
<PrometheusMetricsBrowser
|
||||||
<LocalStorageValueProvider<string[]> storageKey={LAST_USED_LABELS_KEY} defaultValue={[]}>
|
languageProvider={languageProvider}
|
||||||
{(lastUsedLabels, onLastUsedLabelsSave, onLastUsedLabelsDelete) => {
|
onChange={onChangeLabelBrowser}
|
||||||
return (
|
timeRange={range}
|
||||||
<>
|
/>
|
||||||
<div
|
</div>
|
||||||
className="gf-form-inline gf-form-inline--xs-view-flex-column flex-grow-1"
|
)}
|
||||||
data-testid={this.props['data-testid']}
|
{ExtraFieldElement}
|
||||||
|
{hint ? (
|
||||||
|
<div
|
||||||
|
className={css({
|
||||||
|
flexBasis: '100%',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className="text-warning">
|
||||||
|
{hint.label}{' '}
|
||||||
|
{hint.fix ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cx(clearButtonStyles(theme), 'text-link', 'muted')}
|
||||||
|
onClick={onClickHintFix}
|
||||||
>
|
>
|
||||||
<button
|
{hint.fix.label}
|
||||||
className="gf-form-label query-keyword pointer"
|
</button>
|
||||||
onClick={this.onClickChooserButton}
|
) : null}
|
||||||
disabled={buttonDisabled}
|
</div>
|
||||||
type="button"
|
</div>
|
||||||
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.openButton}
|
) : null}
|
||||||
>
|
</>
|
||||||
{chooserText}
|
);
|
||||||
<Icon name={labelBrowserVisible ? 'angle-down' : 'angle-right'} />
|
};
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex-grow-1 min-width-15">
|
|
||||||
<MonacoQueryFieldWrapper
|
|
||||||
languageProvider={languageProvider}
|
|
||||||
history={history}
|
|
||||||
onChange={this.onChangeQuery}
|
|
||||||
onRunQuery={this.props.onRunQuery}
|
|
||||||
initialValue={query.expr ?? ''}
|
|
||||||
placeholder="Enter a PromQL query…"
|
|
||||||
datasource={datasource}
|
|
||||||
timeRange={this.props.range ?? getDefaultTimeRange()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{labelBrowserVisible && (
|
|
||||||
<div className="gf-form">
|
|
||||||
<PrometheusMetricsBrowser
|
|
||||||
languageProvider={languageProvider}
|
|
||||||
onChange={this.onChangeLabelBrowser}
|
|
||||||
lastUsedLabels={lastUsedLabels || []}
|
|
||||||
storeLastUsedLabels={onLastUsedLabelsSave}
|
|
||||||
deleteLastUsedLabels={onLastUsedLabelsDelete}
|
|
||||||
timeRange={this.props.range}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{ExtraFieldElement}
|
|
||||||
{hint ? (
|
|
||||||
<div
|
|
||||||
className={css({
|
|
||||||
flexBasis: '100%',
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<div className="text-warning">
|
|
||||||
{hint.label}{' '}
|
|
||||||
{hint.fix ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className={cx(clearButtonStyles(theme), 'text-link', 'muted')}
|
|
||||||
onClick={this.onClickHintFix}
|
|
||||||
>
|
|
||||||
{hint.fix.label}
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
</LocalStorageValueProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PromQueryField = withTheme2(PromQueryFieldClass);
|
|
||||||
|
@ -184,9 +184,6 @@ describe('PrometheusMetricsBrowser', () => {
|
|||||||
onChange: () => {},
|
onChange: () => {},
|
||||||
autoSelect: 0,
|
autoSelect: 0,
|
||||||
languageProvider: mockLanguageProvider as unknown as PromQlLanguageProvider,
|
languageProvider: mockLanguageProvider as unknown as PromQlLanguageProvider,
|
||||||
lastUsedLabels: [],
|
|
||||||
storeLastUsedLabels: () => {},
|
|
||||||
deleteLastUsedLabels: () => {},
|
|
||||||
timeRange: getDefaultTimeRange(),
|
timeRange: getDefaultTimeRange(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ import { isValidLegacyName, utf8Support } from '../utf8_support';
|
|||||||
const EMPTY_SELECTOR = '{}';
|
const EMPTY_SELECTOR = '{}';
|
||||||
const METRIC_LABEL = '__name__';
|
const METRIC_LABEL = '__name__';
|
||||||
const LIST_ITEM_SIZE = 25;
|
const LIST_ITEM_SIZE = 25;
|
||||||
|
const LAST_USED_LABELS_KEY = 'grafana.datasources.prometheus.browser.labels';
|
||||||
|
|
||||||
export interface BrowserProps {
|
export interface BrowserProps {
|
||||||
languageProvider: PromQlLanguageProvider;
|
languageProvider: PromQlLanguageProvider;
|
||||||
@ -32,9 +33,6 @@ export interface BrowserProps {
|
|||||||
theme: GrafanaTheme2;
|
theme: GrafanaTheme2;
|
||||||
autoSelect?: number;
|
autoSelect?: number;
|
||||||
hide?: () => void;
|
hide?: () => void;
|
||||||
lastUsedLabels: string[];
|
|
||||||
storeLastUsedLabels: (labels: string[]) => void;
|
|
||||||
deleteLastUsedLabels: () => void;
|
|
||||||
timeRange?: TimeRange;
|
timeRange?: TimeRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -274,7 +272,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
|
|||||||
valueSearchTerm: '',
|
valueSearchTerm: '',
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
this.props.deleteLastUsedLabels();
|
localStorage.removeItem(LAST_USED_LABELS_KEY);
|
||||||
// Get metrics
|
// Get metrics
|
||||||
this.fetchValues(METRIC_LABEL, EMPTY_SELECTOR);
|
this.fetchValues(METRIC_LABEL, EMPTY_SELECTOR);
|
||||||
};
|
};
|
||||||
@ -347,9 +345,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { languageProvider, lastUsedLabels } = this.props;
|
const { languageProvider } = this.props;
|
||||||
if (languageProvider) {
|
if (languageProvider) {
|
||||||
const selectedLabels: string[] = lastUsedLabels;
|
const selectedLabels: string[] = JSON.parse(localStorage.getItem(LAST_USED_LABELS_KEY) ?? `[]`) ?? [];
|
||||||
languageProvider.start(this.props.timeRange).then(() => {
|
languageProvider.start(this.props.timeRange).then(() => {
|
||||||
let rawLabels: string[] = languageProvider.getLabelKeys();
|
let rawLabels: string[] = languageProvider.getLabelKeys();
|
||||||
// Get metrics
|
// Get metrics
|
||||||
@ -378,7 +376,8 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedLabels = this.state.labels.filter((label) => label.selected).map((label) => label.name);
|
const selectedLabels = this.state.labels.filter((label) => label.selected).map((label) => label.name);
|
||||||
this.props.storeLastUsedLabels(selectedLabels);
|
localStorage.setItem(LAST_USED_LABELS_KEY, JSON.stringify(selectedLabels));
|
||||||
|
|
||||||
if (label.selected) {
|
if (label.selected) {
|
||||||
// Refetch values for newly selected label...
|
// Refetch values for newly selected label...
|
||||||
if (!label.values) {
|
if (!label.values) {
|
||||||
|
@ -144,7 +144,6 @@ describe('PromVariableQueryEditor', () => {
|
|||||||
getLabelValues: jest.fn().mockImplementation(() => ['that']),
|
getLabelValues: jest.fn().mockImplementation(() => ['that']),
|
||||||
fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })),
|
fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })),
|
||||||
} as Partial<PrometheusLanguageProvider> as PrometheusLanguageProvider,
|
} as Partial<PrometheusLanguageProvider> as PrometheusLanguageProvider,
|
||||||
getInitHints: () => [],
|
|
||||||
getDebounceTimeInMilliseconds: jest.fn(),
|
getDebounceTimeInMilliseconds: jest.fn(),
|
||||||
getTagKeys: jest.fn().mockImplementation(() => Promise.resolve(['this'])),
|
getTagKeys: jest.fn().mockImplementation(() => Promise.resolve(['this'])),
|
||||||
getVariables: jest.fn().mockImplementation(() => []),
|
getVariables: jest.fn().mockImplementation(() => []),
|
||||||
|
@ -0,0 +1,113 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../datasource';
|
||||||
|
import PromQlLanguageProvider from '../language_provider';
|
||||||
|
|
||||||
|
import { useMetricsState } from './useMetricsState';
|
||||||
|
|
||||||
|
// Mock implementations
|
||||||
|
const createMockLanguageProvider = (metrics: string[] = []): PromQlLanguageProvider =>
|
||||||
|
({
|
||||||
|
metrics,
|
||||||
|
}) as unknown as PromQlLanguageProvider;
|
||||||
|
|
||||||
|
const createMockDatasource = (lookupsDisabled = false): PrometheusDatasource =>
|
||||||
|
({
|
||||||
|
lookupsDisabled,
|
||||||
|
}) as unknown as PrometheusDatasource;
|
||||||
|
|
||||||
|
describe('useMetricsState', () => {
|
||||||
|
describe('chooserText', () => {
|
||||||
|
it('should return disabled message when lookups are disabled', () => {
|
||||||
|
const datasource = createMockDatasource(true);
|
||||||
|
const languageProvider = createMockLanguageProvider([]);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.chooserText).toBe('(Disabled)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return loading message when syntax is not loaded', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, false));
|
||||||
|
expect(result.current.chooserText).toBe('Loading metrics...');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return no metrics message when no metrics are found', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider([]);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.chooserText).toBe('(No metrics found)');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return metrics browser text when metrics are available', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.chooserText).toBe('Metrics browser');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buttonDisabled', () => {
|
||||||
|
it('should be disabled when syntax is not loaded', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, false));
|
||||||
|
expect(result.current.buttonDisabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be disabled when no metrics are available', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider([]);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.buttonDisabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be enabled when syntax is loaded and metrics are available', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.buttonDisabled).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasMetrics', () => {
|
||||||
|
it('should be false when no metrics are available', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider([]);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.hasMetrics).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be true when metrics are available', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
expect(result.current.hasMetrics).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('memoization', () => {
|
||||||
|
it('should return same values when dependencies have not changed', () => {
|
||||||
|
const datasource = createMockDatasource();
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result, rerender } = renderHook(() => useMetricsState(datasource, languageProvider, true));
|
||||||
|
const firstResult = result.current;
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
expect(result.current).toBe(firstResult);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should update when datasource lookupsDisabled changes', () => {
|
||||||
|
const initialDatasource = createMockDatasource(false);
|
||||||
|
const languageProvider = createMockLanguageProvider(['metric1']);
|
||||||
|
const { result, rerender } = renderHook(({ ds }) => useMetricsState(ds, languageProvider, true), {
|
||||||
|
initialProps: { ds: initialDatasource },
|
||||||
|
});
|
||||||
|
const firstResult = result.current;
|
||||||
|
|
||||||
|
const updatedDatasource = createMockDatasource(true);
|
||||||
|
rerender({ ds: updatedDatasource });
|
||||||
|
expect(result.current).not.toBe(firstResult);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,38 @@
|
|||||||
|
import { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { PrometheusDatasource } from '../datasource';
|
||||||
|
import PromQlLanguageProvider from '../language_provider';
|
||||||
|
|
||||||
|
function getChooserText(metricsLookupDisabled: boolean, hasSyntax: boolean, hasMetrics: boolean) {
|
||||||
|
if (metricsLookupDisabled) {
|
||||||
|
return '(Disabled)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSyntax) {
|
||||||
|
return 'Loading metrics...';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasMetrics) {
|
||||||
|
return '(No metrics found)';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Metrics browser';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMetricsState(
|
||||||
|
datasource: PrometheusDatasource,
|
||||||
|
languageProvider: PromQlLanguageProvider,
|
||||||
|
syntaxLoaded: boolean
|
||||||
|
) {
|
||||||
|
return useMemo(() => {
|
||||||
|
const hasMetrics = languageProvider.metrics.length > 0;
|
||||||
|
const chooserText = getChooserText(datasource.lookupsDisabled, syntaxLoaded, hasMetrics);
|
||||||
|
const buttonDisabled = !(syntaxLoaded && hasMetrics);
|
||||||
|
|
||||||
|
return {
|
||||||
|
hasMetrics,
|
||||||
|
chooserText,
|
||||||
|
buttonDisabled,
|
||||||
|
};
|
||||||
|
}, [languageProvider.metrics, datasource.lookupsDisabled, syntaxLoaded]);
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
import { renderHook } from '@testing-library/react';
|
||||||
|
|
||||||
|
import { DataFrame, dateTime, TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
|
import PromQlLanguageProvider from '../language_provider';
|
||||||
|
|
||||||
|
import { usePromQueryFieldEffects } from './usePromQueryFieldEffects';
|
||||||
|
|
||||||
|
type TestProps = {
|
||||||
|
languageProvider: PromQlLanguageProvider;
|
||||||
|
range: TimeRange | undefined;
|
||||||
|
series: DataFrame[];
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('usePromQueryFieldEffects', () => {
|
||||||
|
const mockLanguageProvider = {
|
||||||
|
start: jest.fn().mockResolvedValue([]),
|
||||||
|
histogramMetrics: [],
|
||||||
|
timeRange: {},
|
||||||
|
metrics: ['metric1'],
|
||||||
|
startTask: Promise.resolve(),
|
||||||
|
datasource: {},
|
||||||
|
lookupsDisabled: false,
|
||||||
|
syntax: jest.fn(),
|
||||||
|
getLabelKeys: jest.fn(),
|
||||||
|
cleanText: jest.fn(),
|
||||||
|
hasLookupsDisabled: jest.fn(),
|
||||||
|
getBeginningCompletionItems: jest.fn(),
|
||||||
|
getLabelCompletionItems: jest.fn(),
|
||||||
|
getMetricCompletionItems: jest.fn(),
|
||||||
|
getTermCompletionItems: jest.fn(),
|
||||||
|
request: jest.fn(),
|
||||||
|
importQueries: jest.fn(),
|
||||||
|
labelKeys: [],
|
||||||
|
labelFetchTs: 0,
|
||||||
|
getDefaultCacheHeaders: jest.fn(),
|
||||||
|
loadMetricsMetadata: jest.fn(),
|
||||||
|
loadMetrics: jest.fn(),
|
||||||
|
loadLabelKeys: jest.fn(),
|
||||||
|
loadLabelValues: jest.fn(),
|
||||||
|
modifyQuery: jest.fn(),
|
||||||
|
} as unknown as PromQlLanguageProvider;
|
||||||
|
|
||||||
|
const mockRange: TimeRange = {
|
||||||
|
from: dateTime('2022-01-01T00:00:00Z'),
|
||||||
|
to: dateTime('2022-01-02T00:00:00Z'),
|
||||||
|
raw: {
|
||||||
|
from: 'now-1d',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockNewRange: TimeRange = {
|
||||||
|
from: dateTime('2022-01-02T00:00:00Z'),
|
||||||
|
to: dateTime('2022-01-03T00:00:00Z'),
|
||||||
|
raw: {
|
||||||
|
from: 'now-1d',
|
||||||
|
to: 'now',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let refreshMetricsMock: jest.Mock;
|
||||||
|
let refreshHintMock: jest.Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
refreshMetricsMock = jest.fn().mockImplementation(() => Promise.resolve());
|
||||||
|
refreshHintMock = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refreshMetrics and refreshHint on initial render', async () => {
|
||||||
|
renderHook(() => usePromQueryFieldEffects(mockRange, [], refreshMetricsMock, refreshHintMock));
|
||||||
|
|
||||||
|
expect(refreshMetricsMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(refreshHintMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refreshMetrics when the time range changes', async () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
(props: TestProps) => usePromQueryFieldEffects(props.range, props.series, refreshMetricsMock, refreshHintMock),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series: [] as DataFrame[],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render already called refreshMetrics once
|
||||||
|
expect(refreshMetricsMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Change the range
|
||||||
|
rerender({
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockNewRange,
|
||||||
|
series: [] as DataFrame[],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refreshMetricsMock).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call refreshMetrics when the time range is the same', () => {
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
(props: TestProps) => usePromQueryFieldEffects(props.range, props.series, refreshMetricsMock, refreshHintMock),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series: [] as DataFrame[],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render already called refreshMetrics once
|
||||||
|
expect(refreshMetricsMock).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
// Rerender with the same range
|
||||||
|
rerender({
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: { ...mockRange }, // create a new object with the same values
|
||||||
|
series: [] as DataFrame[],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should still be called only once (from initial render)
|
||||||
|
expect(refreshMetricsMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call refreshHint when series changes', () => {
|
||||||
|
const mockSeries = [{ name: 'new series', fields: [], length: 0 }] as DataFrame[];
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
(props: TestProps) => usePromQueryFieldEffects(props.range, props.series, refreshMetricsMock, refreshHintMock),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series: [] as DataFrame[],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render already called refreshHint once
|
||||||
|
expect(refreshHintMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
||||||
|
refreshHintMock.mockClear();
|
||||||
|
|
||||||
|
// Change the series
|
||||||
|
rerender({
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series: mockSeries,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refreshHintMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not call refreshHint when series is the same', () => {
|
||||||
|
const series = [] as DataFrame[];
|
||||||
|
const { rerender } = renderHook(
|
||||||
|
(props: TestProps) => usePromQueryFieldEffects(props.range, props.series, refreshMetricsMock, refreshHintMock),
|
||||||
|
{
|
||||||
|
initialProps: {
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Initial render already called refreshHint once
|
||||||
|
refreshHintMock.mockClear();
|
||||||
|
|
||||||
|
// Rerender with the same series
|
||||||
|
rerender({
|
||||||
|
languageProvider: mockLanguageProvider,
|
||||||
|
range: mockRange,
|
||||||
|
series, // same empty array
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(refreshHintMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
@ -0,0 +1,60 @@
|
|||||||
|
import { MutableRefObject, useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { DataFrame, DateTime, TimeRange } from '@grafana/data';
|
||||||
|
|
||||||
|
import { roundMsToMin } from '../language_utils';
|
||||||
|
|
||||||
|
import { CancelablePromise } from './cancelable-promise';
|
||||||
|
|
||||||
|
export function usePromQueryFieldEffects(
|
||||||
|
range: TimeRange | undefined,
|
||||||
|
series: DataFrame[] | undefined,
|
||||||
|
refreshMetrics: (languageProviderInitRef: MutableRefObject<CancelablePromise<unknown> | null>) => Promise<void>,
|
||||||
|
refreshHint: () => void
|
||||||
|
) {
|
||||||
|
const lastRangeRef = useRef<{ from: DateTime; to: DateTime } | null>(null);
|
||||||
|
const languageProviderInitRef = useRef<CancelablePromise<unknown> | null>(null);
|
||||||
|
|
||||||
|
// Effect for initial load
|
||||||
|
useEffect(() => {
|
||||||
|
refreshMetrics(languageProviderInitRef);
|
||||||
|
refreshHint();
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (languageProviderInitRef.current) {
|
||||||
|
languageProviderInitRef.current.cancel();
|
||||||
|
languageProviderInitRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Effect for time range changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (!range) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentFrom = roundMsToMin(range.from.valueOf());
|
||||||
|
const currentTo = roundMsToMin(range.to.valueOf());
|
||||||
|
|
||||||
|
if (!lastRangeRef.current) {
|
||||||
|
lastRangeRef.current = { from: range.from, to: range.to };
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastFrom = roundMsToMin(lastRangeRef.current.from.valueOf());
|
||||||
|
const lastTo = roundMsToMin(lastRangeRef.current.to.valueOf());
|
||||||
|
|
||||||
|
if (currentFrom !== lastFrom || currentTo !== lastTo) {
|
||||||
|
lastRangeRef.current = { from: range.from, to: range.to };
|
||||||
|
refreshMetrics(languageProviderInitRef);
|
||||||
|
}
|
||||||
|
}, [range, refreshMetrics]);
|
||||||
|
|
||||||
|
// Effect for data changes (refreshing hints)
|
||||||
|
useEffect(() => {
|
||||||
|
refreshHint();
|
||||||
|
}, [series, refreshHint]);
|
||||||
|
|
||||||
|
return languageProviderInitRef;
|
||||||
|
}
|
@ -56,7 +56,7 @@ import {
|
|||||||
getRangeSnapInterval,
|
getRangeSnapInterval,
|
||||||
} from './language_utils';
|
} from './language_utils';
|
||||||
import { PrometheusMetricFindQuery } from './metric_find_query';
|
import { PrometheusMetricFindQuery } from './metric_find_query';
|
||||||
import { getInitHints, getQueryHints } from './query_hints';
|
import { getQueryHints } from './query_hints';
|
||||||
import { promQueryModeller } from './querybuilder/PromQueryModeller';
|
import { promQueryModeller } from './querybuilder/PromQueryModeller';
|
||||||
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
|
import { QueryBuilderLabelFilter, QueryEditorMode } from './querybuilder/shared/types';
|
||||||
import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
|
import { CacheRequestInfo, defaultPrometheusQueryOverlapWindow, QueryCache } from './querycache/QueryCache';
|
||||||
@ -777,10 +777,6 @@ export class PrometheusDatasource
|
|||||||
return getQueryHints(query.expr ?? '', result, this);
|
return getQueryHints(query.expr ?? '', result, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
getInitHints() {
|
|
||||||
return getInitHints(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
async loadRules() {
|
async loadRules() {
|
||||||
try {
|
try {
|
||||||
const res = await this.metadataRequest('/api/v1/rules', {}, { showErrorAlert: false });
|
const res = await this.metadataRequest('/api/v1/rules', {}, { showErrorAlert: false });
|
||||||
|
@ -8,7 +8,6 @@ import {
|
|||||||
LoadingState,
|
LoadingState,
|
||||||
MutableDataFrame,
|
MutableDataFrame,
|
||||||
PanelData,
|
PanelData,
|
||||||
QueryHint,
|
|
||||||
TimeRange,
|
TimeRange,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
@ -16,6 +15,7 @@ import { TemplateSrv } from '@grafana/runtime';
|
|||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import PromQlLanguageProvider from '../../language_provider';
|
import PromQlLanguageProvider from '../../language_provider';
|
||||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||||
|
import * as queryHints from '../../query_hints';
|
||||||
import { PromApplication, PromOptions } from '../../types';
|
import { PromApplication, PromOptions } from '../../types';
|
||||||
import { getLabelSelects } from '../testUtils';
|
import { getLabelSelects } from '../testUtils';
|
||||||
import { PromVisualQuery } from '../types';
|
import { PromVisualQuery } from '../types';
|
||||||
@ -239,12 +239,7 @@ describe('PromQueryBuilder', () => {
|
|||||||
|
|
||||||
it('renders hint if initial hint provided', async () => {
|
it('renders hint if initial hint provided', async () => {
|
||||||
const { datasource } = createDatasource();
|
const { datasource } = createDatasource();
|
||||||
datasource.getInitHints = (): QueryHint[] => [
|
jest.spyOn(queryHints, 'getInitHints').mockReturnValue([{ label: 'Initial hint', type: 'warning' }]);
|
||||||
{
|
|
||||||
label: 'Initial hint',
|
|
||||||
type: 'warning',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const props = createProps(datasource);
|
const props = createProps(datasource);
|
||||||
render(
|
render(
|
||||||
<PromQueryBuilder
|
<PromQueryBuilder
|
||||||
@ -261,7 +256,7 @@ describe('PromQueryBuilder', () => {
|
|||||||
|
|
||||||
it('renders no hint if no initial hint provided', async () => {
|
it('renders no hint if no initial hint provided', async () => {
|
||||||
const { datasource } = createDatasource();
|
const { datasource } = createDatasource();
|
||||||
datasource.getInitHints = (): QueryHint[] => [];
|
jest.spyOn(queryHints, 'getInitHints').mockReturnValue([]);
|
||||||
const props = createProps(datasource);
|
const props = createProps(datasource);
|
||||||
render(
|
render(
|
||||||
<PromQueryBuilder
|
<PromQueryBuilder
|
||||||
|
@ -8,6 +8,7 @@ import { EditorRow } from '@grafana/plugin-ui';
|
|||||||
|
|
||||||
import { PrometheusDatasource } from '../../datasource';
|
import { PrometheusDatasource } from '../../datasource';
|
||||||
import promqlGrammar from '../../promql';
|
import promqlGrammar from '../../promql';
|
||||||
|
import { getInitHints } from '../../query_hints';
|
||||||
import { promQueryModeller } from '../PromQueryModeller';
|
import { promQueryModeller } from '../PromQueryModeller';
|
||||||
import { buildVisualQueryFromString } from '../parsing';
|
import { buildVisualQueryFromString } from '../parsing';
|
||||||
import { OperationExplainedBox } from '../shared/OperationExplainedBox';
|
import { OperationExplainedBox } from '../shared/OperationExplainedBox';
|
||||||
@ -38,7 +39,7 @@ export const PromQueryBuilder = memo<PromQueryBuilderProps>((props) => {
|
|||||||
|
|
||||||
const lang = { grammar: promqlGrammar, name: 'promql' };
|
const lang = { grammar: promqlGrammar, name: 'promql' };
|
||||||
|
|
||||||
const initHints = datasource.getInitHints();
|
const initHints = getInitHints(datasource);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
Reference in New Issue
Block a user