Prometheus: Improve indication of labels loading when using metrics browser. (#111127)
* add loading spinner for labels and values selectors for prom * add small margin bottom to hints to avoid component overlap when zoomed in
This commit is contained in:
@@ -2,17 +2,18 @@ import { useMemo, useState } from 'react';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { BrowserLabel as PromLabel, Input, Label, useStyles2 } from '@grafana/ui';
|
||||
import { BrowserLabel as PromLabel, Input, Label, useStyles2, Spinner } from '@grafana/ui';
|
||||
|
||||
import { METRIC_LABEL } from '../../constants';
|
||||
|
||||
import { useMetricsBrowser } from './MetricsBrowserContext';
|
||||
import { getStylesLabelSelector } from './styles';
|
||||
import { getStylesLabelSelector, getStylesMetricsBrowser } from './styles';
|
||||
|
||||
export function LabelSelector() {
|
||||
const styles = useStyles2(getStylesLabelSelector);
|
||||
const sharedStyles = useStyles2(getStylesMetricsBrowser);
|
||||
const [labelSearchTerm, setLabelSearchTerm] = useState('');
|
||||
const { labelKeys, selectedLabelKeys, onLabelKeyClick } = useMetricsBrowser();
|
||||
const { labelKeys, isLoadingLabelKeys, selectedLabelKeys, onLabelKeyClick } = useMetricsBrowser();
|
||||
|
||||
const filteredLabelKeys = useMemo(() => {
|
||||
return labelKeys.filter(
|
||||
@@ -44,24 +45,29 @@ export function LabelSelector() {
|
||||
/>
|
||||
</div>
|
||||
{/* Using fixed height here to prevent jumpy layout */}
|
||||
<div className={styles.list} style={{ height: 120 }}>
|
||||
{filteredLabelKeys.map((label) => (
|
||||
<PromLabel
|
||||
key={label}
|
||||
name={label}
|
||||
loading={false}
|
||||
active={selectedLabelKeys.includes(label)}
|
||||
hidden={false}
|
||||
facets={undefined}
|
||||
onClick={(name: string) => {
|
||||
// Resetting search to prevent empty results
|
||||
setLabelSearchTerm('');
|
||||
onLabelKeyClick(name);
|
||||
}}
|
||||
searchTerm={labelSearchTerm}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{isLoadingLabelKeys ? (
|
||||
<div className={sharedStyles.spinner}>
|
||||
<Spinner size="xl" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.list} style={{ height: 120 }}>
|
||||
{filteredLabelKeys.map((label) => (
|
||||
<PromLabel
|
||||
key={label}
|
||||
name={label}
|
||||
active={selectedLabelKeys.includes(label)}
|
||||
hidden={false}
|
||||
facets={undefined}
|
||||
onClick={(name: string) => {
|
||||
// Resetting search to prevent empty results
|
||||
setLabelSearchTerm('');
|
||||
onLabelKeyClick(name);
|
||||
}}
|
||||
searchTerm={labelSearchTerm}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+10
-2
@@ -33,6 +33,8 @@ interface MetricsBrowserContextType {
|
||||
// Data and selection state
|
||||
metrics: Metric[];
|
||||
labelKeys: string[];
|
||||
isLoadingLabelKeys: boolean;
|
||||
isLoadingLabelValues: boolean;
|
||||
labelValues: Record<string, string[]>;
|
||||
selectedMetric: string;
|
||||
selectedLabelKeys: string[];
|
||||
@@ -78,6 +80,8 @@ export function MetricsBrowserProvider({
|
||||
validationStatus,
|
||||
metrics,
|
||||
labelKeys,
|
||||
isLoadingLabelKeys,
|
||||
isLoadingLabelValues,
|
||||
labelValues,
|
||||
selectedMetric,
|
||||
selectedLabelKeys,
|
||||
@@ -109,6 +113,8 @@ export function MetricsBrowserProvider({
|
||||
getSelector,
|
||||
metrics,
|
||||
labelKeys,
|
||||
isLoadingLabelKeys,
|
||||
isLoadingLabelValues,
|
||||
labelValues,
|
||||
selectedMetric,
|
||||
selectedLabelKeys,
|
||||
@@ -128,16 +134,18 @@ export function MetricsBrowserProvider({
|
||||
setSeriesLimit,
|
||||
validationStatus,
|
||||
onChange,
|
||||
metrics,
|
||||
getSelector,
|
||||
metrics,
|
||||
labelKeys,
|
||||
isLoadingLabelKeys,
|
||||
isLoadingLabelValues,
|
||||
labelValues,
|
||||
selectedMetric,
|
||||
selectedLabelKeys,
|
||||
selectedLabelValues,
|
||||
handleSelectedMetricChange,
|
||||
handleSelectedLabelKeyChange,
|
||||
handleSelectedLabelValueChange,
|
||||
handleSelectedMetricChange,
|
||||
handleValidation,
|
||||
handleClear,
|
||||
]
|
||||
|
||||
@@ -3,17 +3,20 @@ import { FixedSizeList } from 'react-window';
|
||||
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { BrowserLabel as PromLabel, Input, Label, useStyles2 } from '@grafana/ui';
|
||||
import { BrowserLabel as PromLabel, Input, Label, useStyles2, Spinner } from '@grafana/ui';
|
||||
|
||||
import { LIST_ITEM_SIZE } from '../../constants';
|
||||
|
||||
import { useMetricsBrowser } from './MetricsBrowserContext';
|
||||
import { getStylesValueSelector } from './styles';
|
||||
import { getStylesMetricsBrowser, getStylesValueSelector } from './styles';
|
||||
|
||||
export function ValueSelector() {
|
||||
const styles = useStyles2(getStylesValueSelector);
|
||||
const sharedStyles = useStyles2(getStylesMetricsBrowser);
|
||||
|
||||
const [valueSearchTerm, setValueSearchTerm] = useState('');
|
||||
const { labelValues, selectedLabelValues, onLabelValueClick, onLabelKeyClick } = useMetricsBrowser();
|
||||
const { labelValues, selectedLabelValues, isLoadingLabelValues, onLabelValueClick, onLabelKeyClick } =
|
||||
useMetricsBrowser();
|
||||
|
||||
return (
|
||||
<div className={styles.section}>
|
||||
@@ -38,63 +41,62 @@ export function ValueSelector() {
|
||||
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.labelValuesFilter}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.valueListArea}>
|
||||
{Object.entries(labelValues).map(([lk, lv]) => {
|
||||
if (!lk || !lv) {
|
||||
console.error('label values are empty:', { lk, lv });
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
key={lk}
|
||||
aria-label={t(
|
||||
'grafana-prometheus.components.value-selector.aria-label-values-for',
|
||||
'Values for {{labelKey}}',
|
||||
{
|
||||
labelKey: lk,
|
||||
}
|
||||
)}
|
||||
className={styles.valueListWrapper}
|
||||
>
|
||||
<div className={styles.valueTitle}>
|
||||
<PromLabel
|
||||
name={lk}
|
||||
loading={false}
|
||||
active={true}
|
||||
hidden={false}
|
||||
facets={lv.length}
|
||||
onClick={onLabelKeyClick}
|
||||
/>
|
||||
</div>
|
||||
<FixedSizeList
|
||||
height={Math.min(200, LIST_ITEM_SIZE * (lv.length || 0))}
|
||||
itemCount={lv.length || 0}
|
||||
itemSize={28}
|
||||
itemKey={(i) => lv[i]}
|
||||
width={200}
|
||||
className={styles.valueList}
|
||||
{isLoadingLabelValues ? (
|
||||
<div className={sharedStyles.spinner}>
|
||||
<Spinner size="xl" />
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.valueListArea}>
|
||||
{Object.entries(labelValues).map(([lk, lv]) => {
|
||||
if (!lk || !lv) {
|
||||
console.error('label values are empty:', { lk, lv });
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="list"
|
||||
key={lk}
|
||||
aria-label={t(
|
||||
'grafana-prometheus.components.value-selector.aria-label-values-for',
|
||||
'Values for {{labelKey}}',
|
||||
{
|
||||
labelKey: lk,
|
||||
}
|
||||
)}
|
||||
className={styles.valueListWrapper}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const value = lv[index];
|
||||
const isSelected = selectedLabelValues[lk]?.includes(value);
|
||||
return (
|
||||
<div style={style}>
|
||||
<PromLabel
|
||||
name={value}
|
||||
value={value}
|
||||
active={isSelected}
|
||||
onClick={(name) => onLabelValueClick(lk, name, !isSelected)}
|
||||
searchTerm={valueSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.valueTitle}>
|
||||
<PromLabel name={lk} active={true} hidden={false} facets={lv.length} onClick={onLabelKeyClick} />
|
||||
</div>
|
||||
<FixedSizeList
|
||||
height={Math.min(200, LIST_ITEM_SIZE * (lv.length || 0))}
|
||||
itemCount={lv.length || 0}
|
||||
itemSize={28}
|
||||
itemKey={(i) => lv[i]}
|
||||
width={200}
|
||||
className={styles.valueList}
|
||||
>
|
||||
{({ index, style }) => {
|
||||
const value = lv[index];
|
||||
const isSelected = selectedLabelValues[lk]?.includes(value);
|
||||
return (
|
||||
<div style={style}>
|
||||
<PromLabel
|
||||
name={value}
|
||||
value={value}
|
||||
active={isSelected}
|
||||
onClick={(name) => onLabelValueClick(lk, name, !isSelected)}
|
||||
searchTerm={valueSearchTerm}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</FixedSizeList>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -8,6 +8,12 @@ export const getStylesMetricsBrowser = (theme: GrafanaTheme2) => ({
|
||||
padding: theme.spacing(1),
|
||||
width: '100%',
|
||||
}),
|
||||
spinner: css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
height: 120,
|
||||
}),
|
||||
});
|
||||
|
||||
export const getStylesMetricSelector = (theme: GrafanaTheme2) => ({
|
||||
|
||||
@@ -26,6 +26,8 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
const [lastSelectedLabelKey, setLastSelectedLabelKey] = useState('');
|
||||
const [labelValues, setLabelValues] = useState<Record<string, string[]>>({});
|
||||
const [selectedLabelValues, setSelectedLabelValues] = useState<Record<string, string[]>>({});
|
||||
const [isLoadingLabelKeys, setIsLoadingLabelKeys] = useState(false);
|
||||
const [isLoadingLabelValues, setIsLoadingLabelValues] = useState(false);
|
||||
|
||||
// Memoize the effective series limit to use the default when seriesLimit is empty
|
||||
const effectiveLimit = useMemo(() => seriesLimit, [seriesLimit]);
|
||||
@@ -163,6 +165,8 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
const transformedMetrics: Metric[] = await fetchMetrics(safeSelector);
|
||||
|
||||
// Labels
|
||||
setIsLoadingLabelKeys(true);
|
||||
setIsLoadingLabelValues(true);
|
||||
const transformedLabelKeys: string[] = await fetchLabelKeys(safeSelector);
|
||||
|
||||
// Selected Labels
|
||||
@@ -173,8 +177,10 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
|
||||
setMetrics(transformedMetrics);
|
||||
setLabelKeys(transformedLabelKeys);
|
||||
setIsLoadingLabelKeys(false);
|
||||
setSelectedLabelKeys(labelKeysInLocalStorage);
|
||||
setLabelValues(transformedLabelValues);
|
||||
setIsLoadingLabelValues(false);
|
||||
},
|
||||
[fetchLabelKeys, fetchLabelValues, fetchMetrics, loadSelectedLabelsFromStorage]
|
||||
);
|
||||
@@ -209,8 +215,11 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
const selector = buildSafeSelector(newSelectedMetric, selectedLabelValues);
|
||||
try {
|
||||
const fetchedMetrics = await fetchMetrics(selector);
|
||||
setIsLoadingLabelKeys(true);
|
||||
const fetchedLabelKeys = await fetchLabelKeys(selector);
|
||||
const newSelectedLabelKeys = selectedLabelKeys.filter((slk) => fetchedLabelKeys.includes(slk));
|
||||
|
||||
setIsLoadingLabelValues(true);
|
||||
const [transformedLabelValues, newSelectedLabelValues] = await fetchLabelValues(
|
||||
newSelectedLabelKeys,
|
||||
newSelectedMetric === '' ? undefined : selector
|
||||
@@ -219,8 +228,10 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
setMetrics(fetchedMetrics);
|
||||
setSelectedMetric(newSelectedMetric);
|
||||
setLabelKeys(fetchedLabelKeys);
|
||||
setIsLoadingLabelKeys(false);
|
||||
setSelectedLabelKeys(newSelectedLabelKeys);
|
||||
setLabelValues(transformedLabelValues);
|
||||
setIsLoadingLabelValues(false);
|
||||
setSelectedLabelValues(newSelectedLabelValues);
|
||||
} catch (e: unknown) {
|
||||
handleError(e, 'Error fetching labels');
|
||||
@@ -240,6 +251,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
// Label key is not in the selectedLabelKeys. Let's add it.
|
||||
newSelectedLabelKeys.push(labelKey);
|
||||
const safeSelector = buildSafeSelector(selectedMetric, selectedLabelValues);
|
||||
setIsLoadingLabelValues(true);
|
||||
const [values] = await fetchLabelValues([labelKey], safeSelector);
|
||||
newLabelValues[labelKey] = values[labelKey];
|
||||
} else {
|
||||
@@ -252,6 +264,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
localStorage.setItem(LAST_USED_LABELS_KEY, JSON.stringify(newSelectedLabelKeys));
|
||||
setSelectedLabelKeys(newSelectedLabelKeys);
|
||||
setLabelValues(newLabelValues);
|
||||
setIsLoadingLabelValues(false);
|
||||
setSelectedLabelValues(newSelectedLabelValues);
|
||||
};
|
||||
|
||||
@@ -289,6 +302,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
// Fetch new values
|
||||
let newLabelValues: Record<string, string[]> = {};
|
||||
if (selectedLabelKeys.length !== 0) {
|
||||
setIsLoadingLabelValues(true);
|
||||
for (const lk of selectedLabelKeys) {
|
||||
try {
|
||||
const fetchedLabelValues = await languageProvider.queryLabelValues(
|
||||
@@ -326,6 +340,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
|
||||
// Fetch label keys
|
||||
// If there is no metric or label value selected fetch all the keys instead of creating a selector
|
||||
setIsLoadingLabelKeys(true);
|
||||
let newLabelKeys: string[] = [];
|
||||
if (!safeSelector) {
|
||||
newLabelKeys = await fetchLabelKeys(undefined);
|
||||
@@ -337,9 +352,11 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
|
||||
setMetrics(newMetrics);
|
||||
setLabelKeys(newLabelKeys);
|
||||
setIsLoadingLabelKeys(false);
|
||||
setSelectedLabelKeys(newSelectedLabelKeys);
|
||||
setLastSelectedLabelKey(newLastSelectedLabelKey);
|
||||
setLabelValues(newLabelValues);
|
||||
setIsLoadingLabelValues(false);
|
||||
setSelectedLabelValues(newSelectedLabelValues);
|
||||
};
|
||||
|
||||
@@ -384,6 +401,8 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
metrics,
|
||||
labelKeys,
|
||||
labelValues,
|
||||
isLoadingLabelKeys,
|
||||
isLoadingLabelValues,
|
||||
selectedMetric,
|
||||
selectedLabelKeys,
|
||||
selectedLabelValues,
|
||||
|
||||
@@ -92,6 +92,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
}),
|
||||
hint: css({
|
||||
marginRight: theme.spacing(1),
|
||||
marginBottom: theme.spacing(1),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user