Prometheus: Mectircs browser refreshes metrics when series limit changes (#104101)

* refetch when series limit is changed

* when series limit is empty use the default series limit

* tests for timerange handling

* attempt to reduce the test file size

* remove ineffective memory tests

* update test

* clearer tests

* update help text

* update limit tooltip
This commit is contained in:
ismail simsek
2025-05-13 14:32:13 +02:00
committed by GitHub
parent b79c8b2a94
commit 7da1c9073e
4 changed files with 1255 additions and 907 deletions
@@ -31,7 +31,7 @@ export function MetricSelector() {
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric}
/>
</div>
<Label description="Set to 'none' to remove limit and show all labels for a selected metric. Removing the limit may cause performance issues.">
<Label description="The limit applies to all metrics, labels, and values. Leave the field empty to use the default limit. Set to 0 to disable the limit and fetch everything — this may cause performance issues.">
Series limit
</Label>
<div>
@@ -10,8 +10,7 @@ import { getMockTimeRange } from '../../test/__mocks__/datasource';
import { MetricsBrowserProvider, useMetricsBrowser } from './MetricsBrowserContext';
import { LAST_USED_LABELS_KEY, METRIC_LABEL } from './types';
// Mock the local storage
const localStorageMock = (() => {
const setupLocalStorageMock = () => {
let store: Record<string, string> = {};
return {
getItem: jest.fn((key: string) => store[key] || null),
@@ -22,27 +21,54 @@ const localStorageMock = (() => {
store = {};
}),
};
})();
};
const localStorageMock = setupLocalStorageMock();
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
// Mock language provider
const mockTimeRange = getMockTimeRange();
const mockLanguageProvider = {
metrics: ['metric1', 'metric2', 'metric3'],
labelKeys: ['__name__', 'instance', 'job', 'service'],
metricsMetadata: {
metric1: { type: 'counter', help: 'Test metric 1' },
metric2: { type: 'gauge', help: 'Test metric 2' },
},
fetchLabels: jest.fn(),
fetchSeriesLabelsMatch: jest.fn(),
fetchSeriesValuesWithMatch: jest.fn(),
fetchLabelsWithMatch: jest.fn(),
} as unknown as PromQlLanguageProvider;
/**
* Setup consistent mock response data for the language provider
*/
const setupLanguageProviderMock = () => {
const mockTimeRange = getMockTimeRange();
const mockLanguageProvider = {
metrics: ['metric1', 'metric2', 'metric3'],
labelKeys: ['__name__', 'instance', 'job', 'service'],
metricsMetadata: {
metric1: { type: 'counter', help: 'Test metric 1' },
metric2: { type: 'gauge', help: 'Test metric 2' },
},
fetchLabels: jest.fn().mockResolvedValue(['__name__', 'instance', 'job', 'service']),
fetchSeriesLabelsMatch: jest.fn().mockResolvedValue({
__name__: ['metric1', 'metric2'],
instance: ['instance1', 'instance2'],
job: ['job1', 'job2'],
service: ['service1', 'service2'],
}),
fetchSeriesValuesWithMatch: jest.fn().mockImplementation((_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['grafana', 'prometheus']);
}
if (label === 'instance') {
return Promise.resolve(['host1', 'host2']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2', 'metric3']);
}
return Promise.resolve([]);
}),
fetchLabelsWithMatch: jest.fn().mockResolvedValue({
job: ['job1', 'job2'],
instance: ['instance1', 'instance2'],
}),
} as unknown as PromQlLanguageProvider;
const mockOnChange = jest.fn();
return { mockTimeRange, mockLanguageProvider };
};
// Test component to render the context
/**
* Test component that renders context values and provides interaction buttons
*/
const TestComponent = () => {
const {
metrics,
@@ -86,422 +112,308 @@ const TestComponent = () => {
);
};
const renderWithProvider = (ui: ReactNode) => {
return render(
<MetricsBrowserProvider timeRange={mockTimeRange} languageProvider={mockLanguageProvider} onChange={mockOnChange}>
{ui}
</MetricsBrowserProvider>
);
/**
* Setup function for tests that returns mocks and render utilities
*/
const setupTest = () => {
const mockOnChange = jest.fn();
const { mockTimeRange, mockLanguageProvider } = setupLanguageProviderMock();
const renderWithProvider = (ui: ReactNode) => {
return render(
<MetricsBrowserProvider timeRange={mockTimeRange} languageProvider={mockLanguageProvider} onChange={mockOnChange}>
{ui}
</MetricsBrowserProvider>
);
};
return {
mockTimeRange,
mockLanguageProvider,
mockOnChange,
renderWithProvider,
};
};
describe('MetricsBrowserContext', () => {
beforeEach(() => {
jest.clearAllMocks();
localStorageMock.clear();
// Set up consistent mock implementation
(mockLanguageProvider.fetchLabels as jest.Mock).mockResolvedValue(['__name__', 'instance', 'job', 'service']);
// Default implementation for fetching metrics
(mockLanguageProvider.fetchSeriesValuesWithMatch as jest.Mock).mockImplementation(
(_timeRange: TimeRange, label: string) => {
if (label === 'job') {
return Promise.resolve(['grafana', 'prometheus']);
}
if (label === 'instance') {
return Promise.resolve(['host1', 'host2']);
}
if (label === METRIC_LABEL) {
return Promise.resolve(['metric1', 'metric2', 'metric3']);
}
return Promise.resolve([]);
}
);
// Mock for fetching label keys for a specific metric
(mockLanguageProvider.fetchSeriesLabelsMatch as jest.Mock).mockResolvedValue({
__name__: ['metric1', 'metric2'],
instance: ['instance1', 'instance2'],
job: ['job1', 'job2'],
service: ['service1', 'service2'],
});
// Mock for validation
(mockLanguageProvider.fetchLabelsWithMatch as jest.Mock).mockResolvedValue({
job: ['job1', 'job2'],
instance: ['instance1', 'instance2'],
});
});
it('should initialize with metrics from language provider', async () => {
renderWithProvider(<TestComponent />);
describe('basic functionality', () => {
it('should initialize and display metrics', async () => {
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for API calls and state update
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalledWith(
expect.anything(),
METRIC_LABEL,
undefined,
'MetricsBrowser_M',
'40000'
);
},
{ timeout: 5000 }
);
// Then check UI update
await waitFor(
() => {
// Verify metrics are displayed in the UI
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
},
{ timeout: 5000 }
);
});
});
});
it('should load saved label keys from localStorage on init', async () => {
// Setup localStorage with saved label keys
localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job', 'instance']));
it('should restore selected labels from storage on initialization', async () => {
// Setup localStorage with saved preference
localStorageMock.setItem(LAST_USED_LABELS_KEY, JSON.stringify(['job', 'instance']));
renderWithProvider(<TestComponent />);
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// First wait for API calls
await waitFor(
() => {
expect(mockLanguageProvider.fetchLabels).toHaveBeenCalled();
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Then wait for state update
await waitFor(
() => {
// Verify the saved labels are loaded and displayed
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job,instance');
},
{ timeout: 5000 }
);
});
});
});
it('should select and deselect metrics on click', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
describe('user interactions', () => {
it('should select and deselect metrics', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initialization to complete
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Initially no metric is selected
expect(screen.getByTestId('selected-metric').textContent).toBe('');
// Initially no metric is selected
expect(screen.getByTestId('selected-metric').textContent).toBe('');
// Select a metric
await user.click(screen.getByTestId('select-metric'));
// Select a metric
await user.click(screen.getByTestId('select-metric'));
// Wait for selection to complete and API calls
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesLabelsMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for state update
await waitFor(
() => {
// Verify selection in UI
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
},
{ timeout: 5000 }
);
});
// Mock setup for deselection
jest.clearAllMocks();
// Deselect by clicking the same metric
await user.click(screen.getByTestId('select-metric'));
// Click again to deselect
await user.click(screen.getByTestId('select-metric'));
// Wait for deselection to complete
await waitFor(
() => {
// Verify the deselection in UI
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('');
},
{ timeout: 5000 }
);
});
});
});
it('should select and deselect label keys on click', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
it('should select and deselect label keys with persistence', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initialization to complete
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Initially no label key is selected
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
// Initially no label key is selected
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
// Select a label key
await user.click(screen.getByTestId('select-label'));
// Select a label key
await user.click(screen.getByTestId('select-label'));
// Wait for selection and API calls
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalledWith(
expect.anything(),
'job',
undefined,
'MetricsBrowser_LV_job',
'40000'
);
},
{ timeout: 5000 }
);
// Wait for state update
await waitFor(
() => {
// Verify UI update
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
},
{ timeout: 5000 }
);
});
// Reset mocks for deselection test
jest.clearAllMocks();
// Deselect by clicking again
await user.click(screen.getByTestId('select-label'));
// Click again to deselect
await user.click(screen.getByTestId('select-label'));
// Wait for state update
await waitFor(
() => {
// Verify UI update
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
},
{ timeout: 5000 }
);
});
// Verify localStorage update
expect(localStorageMock.setItem).toHaveBeenCalledWith(LAST_USED_LABELS_KEY, JSON.stringify([]));
});
// Check localStorage was updated (not implementation but outcome)
const mockCalls = localStorageMock.setItem.mock.calls;
// Make sure we have calls to setItem
expect(mockCalls.length).toBeGreaterThan(0);
// Get the last call's arguments
const lastCall = mockCalls[mockCalls.length - 1];
expect(lastCall[0]).toBe(LAST_USED_LABELS_KEY);
expect(JSON.parse(lastCall[1])).toEqual([]);
});
it('should select and deselect label values on click', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
it('should build a selector when selecting label values', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initialization
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// First select the label key to enable label value selection
await user.click(screen.getByTestId('select-label'));
// Wait for label key selection to complete
await waitFor(
() => {
// First select a label key
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
},
{ timeout: 5000 }
);
});
jest.clearAllMocks();
// Select a label value
await user.click(screen.getByTestId('select-label-value'));
// Now select a label value
await user.click(screen.getByTestId('select-label-value'));
// Wait for API calls after value selection
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for selector update in UI
await waitFor(
() => {
// Verify the selector is updated
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toBe('{job="grafana"}');
},
{ timeout: 5000 }
);
});
});
});
it('should fetch label values with the current selector', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
it('should use metric in selector when both metric and labels are selected', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initialization to complete
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// First select a metric to create a non-empty selector
await user.click(screen.getByTestId('select-metric'));
// Wait for metric selection to complete
await waitFor(
() => {
// Select a metric first
await user.click(screen.getByTestId('select-metric'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
},
{ timeout: 5000 }
);
});
jest.clearAllMocks();
// Then select a label
await user.click(screen.getByTestId('select-label'));
// Wait for API call with the expected selector
await waitFor(
() => {
// Should call fetchSeriesValuesWithMatch with the current selector
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalledWith(
expect.anything(),
'job',
expect.stringContaining('metric1'),
expect.any(String),
'40000'
);
},
{ timeout: 5000 }
);
});
it('should clear all selections when clear is clicked', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
// Wait for initialization
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Set up some selections
await user.click(screen.getByTestId('select-metric'));
// Wait for metric selection
await waitFor(
() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
},
{ timeout: 5000 }
);
await user.click(screen.getByTestId('select-label'));
// Wait for label selection
await waitFor(
() => {
// Then select a label
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
},
{ timeout: 5000 }
);
});
// Reset mocks before clear
jest.clearAllMocks();
// Then select a label value
await user.click(screen.getByTestId('select-label-value'));
// Clear selections
await user.click(screen.getByTestId('clear'));
// Verify the selector includes both metric and label
await waitFor(() => {
const selector = screen.getByTestId('selector').textContent;
expect(selector).toContain('metric1');
expect(selector).toContain('job="grafana"');
});
});
});
// Wait for clear to trigger API calls
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
describe('selector operations', () => {
it('should clear all selections', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Verify everything is cleared
await waitFor(
() => {
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Make selections
await user.click(screen.getByTestId('select-metric'));
await user.click(screen.getByTestId('select-label'));
await user.click(screen.getByTestId('select-label-value'));
// Verify selections
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
expect(screen.getByTestId('selector').textContent).not.toBe('{}');
});
// Clear all selections
await user.click(screen.getByTestId('clear'));
// Verify all fields are cleared
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('');
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
expect(screen.getByTestId('selector').textContent).toBe('{}');
},
{ timeout: 5000 }
);
});
});
});
it('should validate selectors and show status', async () => {
const user = userEvent.setup();
renderWithProvider(<TestComponent />);
it('should validate selectors', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
// Wait for initialization
await waitFor(
() => {
expect(mockLanguageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
},
{ timeout: 5000 }
);
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Select a label to enable value selection
await user.click(screen.getByTestId('select-label'));
// Create a valid selector
await user.click(screen.getByTestId('select-label'));
await user.click(screen.getByTestId('select-label-value'));
// Wait for label selection
await waitFor(
() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
},
{ timeout: 5000 }
);
// Select a value to get a valid selector
await user.click(screen.getByTestId('select-label-value'));
// Wait for the value selection
await waitFor(
() => {
// Verify the selector is created
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toBe('{job="grafana"}');
},
{ timeout: 5000 }
);
});
jest.clearAllMocks();
// Trigger validation
await user.click(screen.getByTestId('validate'));
// Trigger validation
await user.click(screen.getByTestId('validate'));
// Wait for validation API call
await waitFor(
() => {
expect(mockLanguageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith(expect.anything(), '{job="grafana"}');
},
{ timeout: 5000 }
);
// Wait for validation status to update
await waitFor(
() => {
// Verify validation result
await waitFor(() => {
expect(screen.getByTestId('validation-status').textContent).toContain('Selector is valid');
},
{ timeout: 5000 }
);
});
});
});
it('should throw error when hook is used outside provider', () => {
// Suppress console.error for this test
jest.spyOn(console, 'error').mockImplementation(() => {});
describe('complete user workflows', () => {
it('should handle a full selection -> validation -> clear workflow', async () => {
const user = userEvent.setup();
const { renderWithProvider } = setupTest();
renderWithProvider(<TestComponent />);
expect(() => {
renderHook(() => useMetricsBrowser());
}).toThrow('useMetricsBrowser must be used within a MetricsBrowserProvider');
// Wait for component to be ready
await waitFor(() => {
expect(screen.getByTestId('metrics-count').textContent).toBe('3');
});
// Restore console.error
(console.error as jest.Mock).mockRestore();
// STEP 1: Select a metric
await user.click(screen.getByTestId('select-metric'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('metric1');
});
// STEP 2: Add a label
await user.click(screen.getByTestId('select-label'));
await waitFor(() => {
expect(screen.getByTestId('selected-label-keys').textContent).toBe('job');
});
// STEP 3: Select a value
await user.click(screen.getByTestId('select-label-value'));
await waitFor(() => {
expect(screen.getByTestId('selector').textContent).toContain('job="grafana"');
});
// STEP 4: Validate
await user.click(screen.getByTestId('validate'));
await waitFor(() => {
expect(screen.getByTestId('validation-status').textContent).toContain('Selector is valid');
});
// STEP 5: Clear
await user.click(screen.getByTestId('clear'));
await waitFor(() => {
expect(screen.getByTestId('selected-metric').textContent).toBe('');
expect(screen.getByTestId('selected-label-keys').textContent).toBe('');
expect(screen.getByTestId('selector').textContent).toBe('{}');
});
});
});
describe('error handling', () => {
it('should throw error when hook is used outside provider', () => {
// Suppress console.error for this test
jest.spyOn(console, 'error').mockImplementation(() => {});
expect(() => {
renderHook(() => useMetricsBrowser());
}).toThrow('useMetricsBrowser must be used within a MetricsBrowserProvider');
// Restore console.error
(console.error as jest.Mock).mockRestore();
});
});
});
@@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { useCallback, useEffect, useRef, useState, useMemo } from 'react';
import { useDebounce } from 'react-use';
import { TimeRange } from '@grafana/data';
@@ -9,6 +10,8 @@ import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, LAST_USED_LABELS_KEY, Metric, MET
export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: PromQlLanguageProvider) => {
const timeRangeRef = useRef<TimeRange>(timeRange);
const lastSeriesLimitRef = useRef(DEFAULT_SERIES_LIMIT);
const isInitializedRef = useRef(false);
const [seriesLimit, setSeriesLimit] = useState(DEFAULT_SERIES_LIMIT);
const [err, setErr] = useState('');
@@ -23,6 +26,9 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
const [labelValues, setLabelValues] = useState<Record<string, string[]>>({});
const [selectedLabelValues, setSelectedLabelValues] = useState<Record<string, string[]>>({});
// Memoize the effective series limit to use the default when seriesLimit is empty
const effectiveLimit = useMemo(() => seriesLimit || DEFAULT_SERIES_LIMIT, [seriesLimit]);
// We don't want to trigger fetching for small amount of time changes.
// When MetricsBrowser re-renders for any reason we might receive a new timerange.
// This particularly happens when we have relative time ranges: from: now, to: now-1h
@@ -88,7 +94,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
METRIC_LABEL,
safeSelector,
'MetricsBrowser_M',
seriesLimit
effectiveLimit
);
return fetchedMetrics.map((m) => ({
name: m,
@@ -99,7 +105,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
return [];
}
},
[getMetricDetails, handleError, languageProvider, seriesLimit]
[getMetricDetails, handleError, languageProvider, effectiveLimit]
);
// Fetches label keys based on an optional selector
@@ -109,17 +115,17 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
try {
if (safeSelector) {
return Object.keys(
await languageProvider.fetchSeriesLabelsMatch(timeRangeRef.current, safeSelector, seriesLimit)
await languageProvider.fetchSeriesLabelsMatch(timeRangeRef.current, safeSelector, effectiveLimit)
);
} else {
return (await languageProvider.fetchLabels(timeRangeRef.current, undefined, seriesLimit)) || [];
return (await languageProvider.fetchLabels(timeRangeRef.current, undefined, effectiveLimit)) || [];
}
} catch (e) {
handleError(e, 'Error fetching labels');
return [];
}
},
[handleError, languageProvider, seriesLimit]
[handleError, languageProvider, effectiveLimit]
);
// Fetches values for multiple label keys and also prepares selected values
@@ -134,7 +140,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
lk,
safeSelector,
`MetricsBrowser_LV_${lk}`,
seriesLimit
effectiveLimit
);
transformedLabelValues[lk] = values;
if (selectedLabelValues[lk]) {
@@ -146,7 +152,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
}
return [transformedLabelValues, newSelectedLabelValues];
},
[handleError, languageProvider, selectedLabelValues, seriesLimit]
[handleError, languageProvider, selectedLabelValues, effectiveLimit]
);
// Initial set up of the Metrics Browser
@@ -179,9 +185,23 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
// Initialize the hook
useEffect(() => {
initialize(selectedMetric, selectedLabelValues);
isInitializedRef.current = true;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// We use debounce here to prevent fetching data on every keystroke
// We also track the seriesLimit change to prevent fetching twice right after the initialization
useDebounce(
() => {
if (isInitializedRef.current && lastSeriesLimitRef.current !== seriesLimit) {
initialize(selectedMetric, selectedLabelValues);
lastSeriesLimitRef.current = seriesLimit;
}
},
300,
[seriesLimit]
);
// Handles metric selection changes.
// If a metric selected it fetches the labels of that metric
// Otherwise it fetches all the labels.
@@ -279,7 +299,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
lk,
safeSelector,
`MetricsBrowser_LV_${lk}`,
seriesLimit
effectiveLimit
);
// We don't want to discard values from last selected list.
@@ -332,7 +352,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
setErr('');
try {
const results = await languageProvider.fetchLabelsWithMatch(timeRangeRef.current, selector);
const results = await languageProvider.fetchSeriesLabelsMatch(timeRangeRef.current, selector, effectiveLimit);
setValidationStatus(`Selector is valid (${Object.keys(results).length} labels found)`);
} catch (e) {
handleError(e, 'Validation failed');