1import { createSlice, PayloadAction } from '@reduxjs/toolkit';
2import { isNumber, sortBy, toLower, uniqBy } from 'lodash';
3import { DataSourceApi, MetricFindValue, stringToJsRegex } from '@grafana/data';
4
5import {
6  initialVariableModelState,
7  QueryVariableModel,
8  VariableOption,
9  VariableQueryEditorType,
10  VariableRefresh,
11  VariableSort,
12} from '../types';
13
14import {
15  ALL_VARIABLE_TEXT,
16  ALL_VARIABLE_VALUE,
17  getInstanceState,
18  initialVariablesState,
19  NONE_VARIABLE_TEXT,
20  NONE_VARIABLE_VALUE,
21  VariablePayload,
22  VariablesState,
23} from '../state/types';
24
25interface VariableOptionsUpdate {
26  templatedRegex: string;
27  results: MetricFindValue[];
28}
29
30export interface QueryVariableEditorState {
31  VariableQueryEditor: VariableQueryEditorType;
32  dataSource: DataSourceApi | null;
33}
34
35export const initialQueryVariableModelState: QueryVariableModel = {
36  ...initialVariableModelState,
37  type: 'query',
38  datasource: null,
39  query: '',
40  regex: '',
41  sort: VariableSort.disabled,
42  refresh: VariableRefresh.onDashboardLoad,
43  multi: false,
44  includeAll: false,
45  allValue: null,
46  options: [],
47  current: {} as VariableOption,
48  definition: '',
49};
50
51export const sortVariableValues = (options: any[], sortOrder: VariableSort) => {
52  if (sortOrder === VariableSort.disabled) {
53    return options;
54  }
55
56  const sortType = Math.ceil(sortOrder / 2);
57  const reverseSort = sortOrder % 2 === 0;
58
59  if (sortType === 1) {
60    options = sortBy(options, 'text');
61  } else if (sortType === 2) {
62    options = sortBy(options, (opt) => {
63      if (!opt.text) {
64        return -1;
65      }
66
67      const matches = opt.text.match(/.*?(\d+).*/);
68      if (!matches || matches.length < 2) {
69        return -1;
70      } else {
71        return parseInt(matches[1], 10);
72      }
73    });
74  } else if (sortType === 3) {
75    options = sortBy(options, (opt) => {
76      return toLower(opt.text);
77    });
78  }
79
80  if (reverseSort) {
81    options = options.reverse();
82  }
83
84  return options;
85};
86
87const getAllMatches = (str: string, regex: RegExp): RegExpExecArray[] => {
88  const results: RegExpExecArray[] = [];
89  let matches = null;
90
91  regex.lastIndex = 0;
92
93  do {
94    matches = regex.exec(str);
95    if (matches) {
96      results.push(matches);
97    }
98  } while (regex.global && matches && matches[0] !== '' && matches[0] !== undefined);
99
100  return results;
101};
102
103export const metricNamesToVariableValues = (variableRegEx: string, sort: VariableSort, metricNames: any[]) => {
104  let regex;
105  let options: VariableOption[] = [];
106
107  if (variableRegEx) {
108    regex = stringToJsRegex(variableRegEx);
109  }
110
111  for (let i = 0; i < metricNames.length; i++) {
112    const item = metricNames[i];
113    let text = item.text === undefined || item.text === null ? item.value : item.text;
114    let value = item.value === undefined || item.value === null ? item.text : item.value;
115
116    if (isNumber(value)) {
117      value = value.toString();
118    }
119
120    if (isNumber(text)) {
121      text = text.toString();
122    }
123
124    if (regex) {
125      const matches = getAllMatches(value, regex);
126      if (!matches.length) {
127        continue;
128      }
129
130      const valueGroup = matches.find((m) => m.groups && m.groups.value);
131      const textGroup = matches.find((m) => m.groups && m.groups.text);
132      const firstMatch = matches.find((m) => m.length > 1);
133      const manyMatches = matches.length > 1 && firstMatch;
134
135      if (valueGroup || textGroup) {
136        value = valueGroup?.groups?.value ?? textGroup?.groups?.text;
137        text = textGroup?.groups?.text ?? valueGroup?.groups?.value;
138      } else if (manyMatches) {
139        for (let j = 0; j < matches.length; j++) {
140          const match = matches[j];
141          options.push({ text: match[1], value: match[1], selected: false });
142        }
143        continue;
144      } else if (firstMatch) {
145        text = firstMatch[1];
146        value = firstMatch[1];
147      }
148    }
149
150    options.push({ text: text, value: value, selected: false });
151  }
152
153  options = uniqBy(options, 'value');
154  return sortVariableValues(options, sort);
155};
156
157export const queryVariableSlice = createSlice({
158  name: 'templating/query',
159  initialState: initialVariablesState,
160  reducers: {
161    updateVariableOptions: (state: VariablesState, action: PayloadAction<VariablePayload<VariableOptionsUpdate>>) => {
162      const { results, templatedRegex } = action.payload.data;
163      const instanceState = getInstanceState<QueryVariableModel>(state, action.payload.id);
164      const { includeAll, sort } = instanceState;
165      const options = metricNamesToVariableValues(templatedRegex, sort, results);
166
167      if (includeAll) {
168        options.unshift({ text: ALL_VARIABLE_TEXT, value: ALL_VARIABLE_VALUE, selected: false });
169      }
170
171      if (!options.length) {
172        options.push({ text: NONE_VARIABLE_TEXT, value: NONE_VARIABLE_VALUE, isNone: true, selected: false });
173      }
174
175      instanceState.options = options;
176    },
177  },
178});
179
180export const queryVariableReducer = queryVariableSlice.reducer;
181
182export const { updateVariableOptions } = queryVariableSlice.actions;
183