The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/public/app/features/variables/pickers/OptionsPicker/reducer.ts

272 lines
8.2 KiB

import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { cloneDeep, isString, trim } from 'lodash';
import { VariableOption, VariableTag, VariableWithMultiSupport } from '../../types';
import { ALL_VARIABLE_VALUE } from '../../state/types';
import { isQuery } from '../../guard';
import { applyStateChanges } from '../../../../core/utils/applyStateChanges';
import { containsSearchFilter } from '../../utils';
export interface ToggleOption {
option: VariableOption;
forceSelect: boolean;
clearOthers: boolean;
}
export interface OptionsPickerState {
id: string;
selectedValues: VariableOption[];
selectedTags: VariableTag[];
queryValue: string;
highlightIndex: number;
tags: VariableTag[];
options: VariableOption[];
multi: boolean;
}
export const initialState: OptionsPickerState = {
id: '',
highlightIndex: -1,
queryValue: '',
selectedTags: [],
selectedValues: [],
tags: [],
options: [],
multi: false,
};
export const OPTIONS_LIMIT = 1000;
const getTags = (model: VariableWithMultiSupport) => {
if (isQuery(model) && Array.isArray(model.tags)) {
return cloneDeep(model.tags);
}
return [];
};
const optionsToRecord = (options: VariableOption[]): Record<string, VariableOption> => {
if (!Array.isArray(options)) {
return {};
}
return options.reduce((all: Record<string, VariableOption>, option) => {
if (isString(option.value)) {
all[option.value] = option;
}
return all;
}, {});
};
const updateOptions = (state: OptionsPickerState): OptionsPickerState => {
if (!Array.isArray(state.options)) {
state.options = [];
return state;
}
const selectedOptions = optionsToRecord(state.selectedValues);
state.selectedValues = Object.values(selectedOptions);
state.options = state.options.map((option) => {
if (!isString(option.value)) {
return option;
}
const selected = !!selectedOptions[option.value];
if (option.selected === selected) {
return option;
}
return { ...option, selected };
});
state.options = applyLimit(state.options);
return state;
};
const applyLimit = (options: VariableOption[]): VariableOption[] => {
if (!Array.isArray(options)) {
return [];
}
if (options.length <= OPTIONS_LIMIT) {
return options;
}
return options.slice(0, OPTIONS_LIMIT);
};
const updateDefaultSelection = (state: OptionsPickerState): OptionsPickerState => {
const { options, selectedValues } = state;
if (options.length === 0 || selectedValues.length > 0) {
return state;
}
if (!options[0] || options[0].value !== ALL_VARIABLE_VALUE) {
return state;
}
state.selectedValues = [{ ...options[0], selected: true }];
return state;
};
const updateAllSelection = (state: OptionsPickerState): OptionsPickerState => {
const { selectedValues } = state;
if (selectedValues.length > 1) {
state.selectedValues = selectedValues.filter((option) => option.value !== ALL_VARIABLE_VALUE);
}
return state;
};
const optionsPickerSlice = createSlice({
name: 'templating/optionsPicker',
initialState,
reducers: {
showOptions: (state, action: PayloadAction<VariableWithMultiSupport>): OptionsPickerState => {
const { query, options, multi } = action.payload;
state.highlightIndex = -1;
state.options = cloneDeep(options);
state.tags = getTags(action.payload);
state.multi = multi ?? false;
state.id = action.payload.id;
state.queryValue = '';
if (isQuery(action.payload)) {
const { queryValue } = action.payload;
const queryHasSearchFilter = containsSearchFilter(query);
state.queryValue = queryHasSearchFilter && queryValue ? queryValue : '';
}
state.selectedValues = state.options.filter((option) => option.selected);
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
hideOptions: (state, action: PayloadAction): OptionsPickerState => {
return { ...initialState };
},
toggleOption: (state, action: PayloadAction<ToggleOption>): OptionsPickerState => {
const { option, clearOthers, forceSelect } = action.payload;
const { multi, selectedValues } = state;
const selected = !selectedValues.find((o) => o.value === option.value);
if (option.value === ALL_VARIABLE_VALUE || !multi || clearOthers) {
if (selected || forceSelect) {
state.selectedValues = [{ ...option, selected: true }];
} else {
state.selectedValues = [];
}
return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
}
if (forceSelect || selected) {
state.selectedValues.push({ ...option, selected: true });
return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
}
state.selectedValues = selectedValues.filter((o) => o.value !== option.value);
return applyStateChanges(state, updateDefaultSelection, updateAllSelection, updateOptions);
},
toggleTag: (state, action: PayloadAction<VariableTag>): OptionsPickerState => {
const tag = action.payload;
const values = tag.values || [];
const selected = !tag.selected;
state.tags = state.tags.map((t) => {
if (t.text !== tag.text) {
return t;
}
t.selected = selected;
t.values = values;
if (selected) {
t.valuesText = values.join(' + ');
} else {
delete t.valuesText;
}
return t;
});
const availableOptions = optionsToRecord(state.options);
if (!selected) {
state.selectedValues = state.selectedValues.filter(
(option) => !isString(option.value) || !availableOptions[option.value]
);
return applyStateChanges(state, updateDefaultSelection, updateOptions);
}
const optionsFromTag = values
.filter((value) => value !== ALL_VARIABLE_VALUE && !!availableOptions[value])
.map((value) => ({ selected, value, text: value }));
state.selectedValues.push.apply(state.selectedValues, optionsFromTag);
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
moveOptionsHighlight: (state, action: PayloadAction<number>): OptionsPickerState => {
let nextIndex = state.highlightIndex + action.payload;
if (nextIndex < 0) {
nextIndex = 0;
} else if (nextIndex >= state.options.length) {
nextIndex = state.options.length - 1;
}
return {
...state,
highlightIndex: nextIndex,
};
},
toggleAllOptions: (state, action: PayloadAction): OptionsPickerState => {
if (state.selectedValues.length > 0) {
state.selectedValues = [];
return applyStateChanges(state, updateOptions);
}
state.selectedValues = state.options
.filter((option) => option.value !== ALL_VARIABLE_VALUE)
.map((option) => ({
...option,
selected: true,
}));
return applyStateChanges(state, updateOptions);
},
updateSearchQuery: (state, action: PayloadAction<string>): OptionsPickerState => {
state.queryValue = action.payload;
return state;
},
updateOptionsAndFilter: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
const searchQuery = trim((state.queryValue ?? '').toLowerCase());
state.options = action.payload.filter((option) => {
const text = Array.isArray(option.text) ? option.text.toString() : option.text;
return text.toLowerCase().indexOf(searchQuery) !== -1;
});
state.highlightIndex = 0;
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
updateOptionsFromSearch: (state, action: PayloadAction<VariableOption[]>): OptionsPickerState => {
state.options = action.payload;
state.highlightIndex = 0;
return applyStateChanges(state, updateDefaultSelection, updateOptions);
},
cleanPickerState: () => initialState,
},
});
export const {
toggleOption,
showOptions,
hideOptions,
toggleTag,
moveOptionsHighlight,
toggleAllOptions,
updateSearchQuery,
updateOptionsAndFilter,
updateOptionsFromSearch,
cleanPickerState,
} = optionsPickerSlice.actions;
export const optionsPickerReducer = optionsPickerSlice.reducer;