/usr/share/grafana/public/app/plugins/datasource/azuremonitor/components/MetricsQueryEditor
import { useEffect, useMemo, useState } from 'react'; import { SelectableValue, DataFrame, PanelData, Labels } from '@grafana/data'; import { t } from '@grafana/i18n'; import { EditorList, AccessoryButton } from '@grafana/plugin-ui'; import { Select, Stack, MultiSelect } from '@grafana/ui'; import { AzureMetricDimension, AzureMonitorQuery } from '../../types/query'; import { AzureMonitorOption, AzureQueryEditorFieldProps } from '../../types/types'; import { Field } from '../shared/Field'; import { setDimensionFilters } from './setQueryValue'; interface DimensionFieldsProps extends AzureQueryEditorFieldProps { dimensionOptions: AzureMonitorOption[]; } interface DimensionLabels { [key: string]: Set<string>; } const useDimensionLabels = (data: PanelData | undefined, query: AzureMonitorQuery) => { const [dimensionLabels, setDimensionLabels] = useState<DimensionLabels>({}); useEffect(() => { let labelsObj: DimensionLabels = {}; if (data?.series?.length) { // Identify which series' in the dataframe are relevant to the current query const series: DataFrame[] = data.series.flat().filter((series) => series.refId === query.refId); const fields = series.flatMap((series) => series.fields); // Retrieve labels for series fields const labels = fields .map((fields) => fields.labels) .flat() .filter((item): item is Labels => item !== null && item !== undefined); for (const label of labels) { // Labels only exist for series that have a dimension selected for (const [dimension, value] of Object.entries(label)) { const dimensionLower = dimension.toLowerCase(); if (labelsObj[dimensionLower]) { labelsObj[dimensionLower].add(value); } else { labelsObj[dimensionLower] = new Set([value]); } } } } setDimensionLabels((prevLabels) => { const newLabels: DimensionLabels = {}; const currentLabels = Object.keys(labelsObj); if (currentLabels.length === 0) { return prevLabels; } for (const label of currentLabels) { if (prevLabels[label] && labelsObj[label].size < prevLabels[label].size) { newLabels[label] = prevLabels[label]; } else { newLabels[label] = labelsObj[label]; } } return newLabels; }); }, [data?.series, query.refId]); return dimensionLabels; }; const DimensionFields = ({ data, query, dimensionOptions, onQueryChange }: DimensionFieldsProps) => { const dimensionFilters = useMemo( () => query.azureMonitor?.dimensionFilters ?? [], [query.azureMonitor?.dimensionFilters] ); const dimensionLabels = useDimensionLabels(data, query); const dimensionOperators: Array<SelectableValue<string>> = [ { label: '==', value: 'eq' }, { label: '!=', value: 'ne' }, { label: 'starts with', value: 'sw' }, ]; const validDimensionOptions = useMemo(() => { // We filter out any dimensions that have already been used in a filter as the API doesn't support having multiple filters with the same dimension name. // The Azure portal also doesn't support this feature so it makes sense for consistency. let t = dimensionOptions; if (dimensionFilters.length) { t = dimensionOptions.filter( (val) => !dimensionFilters.some((dimensionFilter) => dimensionFilter.dimension === val.value) ); } return t; }, [dimensionFilters, dimensionOptions]); const onFieldChange = <Key extends keyof AzureMetricDimension>( fieldName: Key, item: Partial<AzureMetricDimension>, value: AzureMetricDimension[Key], onChange: (item: Partial<AzureMetricDimension>) => void ) => { item[fieldName] = value; onChange(item); }; const getValidDimensionOptions = (selectedDimension: string) => { return validDimensionOptions.concat(dimensionOptions.filter((item) => item.value === selectedDimension)); }; const getValidFilterOptions = (selectedFilter: string | undefined, dimension: string) => { const dimensionFilters = Array.from(dimensionLabels[dimension.toLowerCase()] ?? []); if (dimensionFilters.find((filter) => filter === selectedFilter)) { return dimensionFilters.map((filter) => ({ value: filter, label: filter })); } return [...dimensionFilters, ...(selectedFilter && selectedFilter !== '*' ? [selectedFilter] : [])].map((item) => ({ value: item, label: item, })); }; const getValidMultiSelectOptions = (selectedFilters: string[] | undefined, dimension: string) => { const labelOptions = getValidFilterOptions(undefined, dimension); if (selectedFilters) { for (const filter of selectedFilters) { if (!labelOptions.find((label) => label.value === filter)) { labelOptions.push({ value: filter, label: filter }); } } } return labelOptions; }; const getValidOperators = (selectedOperator: string) => { if (dimensionOperators.find((operator: SelectableValue) => operator.value === selectedOperator)) { return dimensionOperators; } return [...dimensionOperators, ...(selectedOperator ? [{ label: selectedOperator, value: selectedOperator }] : [])]; }; const changedFunc = (changed: Array<Partial<AzureMetricDimension>>) => { const properData: AzureMetricDimension[] = changed.map((x) => { return { dimension: x.dimension ?? '', operator: x.operator ?? 'eq', filters: x.filters ?? [], }; }); onQueryChange(setDimensionFilters(query, properData)); }; const renderFilters = ( item: Partial<AzureMetricDimension>, onChange: (item: Partial<AzureMetricDimension>) => void, onDelete: () => void ) => { return ( <Stack gap={0}> <Select menuShouldPortal placeholder={t('components.dimension-fields.placeholder-field', 'Field')} value={item.dimension} options={getValidDimensionOptions(item.dimension || '')} onChange={(e) => onFieldChange('dimension', item, e.value ?? '', onChange)} /> <Select menuShouldPortal placeholder={t('components.dimension-fields.placeholder-operation', 'Operation')} value={item.operator} options={getValidOperators(item.operator || 'eq')} onChange={(e) => onFieldChange('operator', item, e.value ?? '', onChange)} allowCustomValue /> {item.operator === 'eq' || item.operator === 'ne' ? ( <MultiSelect menuShouldPortal placeholder={t('components.dimension-fields.placeholder-select-values', 'Select value(s)')} value={item.filters} options={getValidMultiSelectOptions(item.filters, item.dimension ?? '')} onChange={(e) => onFieldChange( 'filters', item, e.map((x) => x.value ?? ''), onChange ) } data-testid="dimension-labels-select" allowCustomValue /> ) : ( // The API does not currently allow for multiple "starts with" clauses to be used. <Select menuShouldPortal placeholder={t('components.dimension-fields.placeholder-select-value', 'Select value')} value={item.filters ? item.filters[0] : ''} allowCustomValue options={getValidFilterOptions(item.filters ? item.filters[0] : '', item.dimension ?? '')} onChange={(e) => onFieldChange('filters', item, [e?.value ?? ''], onChange)} isClearable /> )} <AccessoryButton aria-label={t('components.dimension-fields.aria-label-remove', 'Remove')} icon="times" variant="secondary" onClick={onDelete} type="button" /> </Stack> ); }; return ( <Field label={t('components.dimension-fields.label-dimensions', 'Dimensions')}> <EditorList items={dimensionFilters} onChange={changedFunc} renderItem={renderFilters} /> </Field> ); }; export default DimensionFields;
.
Edit
..
Edit
AdvancedResourcePicker.test.tsx
Edit
AdvancedResourcePicker.tsx
Edit
AggregationField.test.tsx
Edit
AggregationField.tsx
Edit
DimensionFields.test.tsx
Edit
DimensionFields.tsx
Edit
LegendFormatField.tsx
Edit
MetricNameField.test.tsx
Edit
MetricNameField.tsx
Edit
MetricNamespaceField.test.tsx
Edit
MetricNamespaceField.tsx
Edit
MetricsQueryEditor.test.tsx
Edit
MetricsQueryEditor.tsx
Edit
TimeGrainField.test.tsx
Edit
TimeGrainField.tsx
Edit
TopField.tsx
Edit
dataHooks.test.ts
Edit
dataHooks.ts
Edit
setQueryValue.test.ts
Edit
setQueryValue.ts
Edit