2019-01-18 11:14:27 -06:00
|
|
|
// Libraries
|
2018-10-30 10:14:01 -05:00
|
|
|
import _ from 'lodash';
|
|
|
|
|
2019-01-18 11:14:27 -06:00
|
|
|
// Services & Utils
|
|
|
|
import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils';
|
2019-11-15 09:38:25 -06:00
|
|
|
import syntax, { FUNCTIONS } from './syntax';
|
2019-01-18 11:14:27 -06:00
|
|
|
|
|
|
|
// Types
|
|
|
|
import { LokiQuery } from './types';
|
2019-10-31 04:48:05 -05:00
|
|
|
import { dateTime, AbsoluteTimeRange, LanguageProvider, HistoryItem } from '@grafana/data';
|
2019-06-03 07:54:32 -05:00
|
|
|
import { PromQuery } from '../prometheus/types';
|
2019-11-15 09:38:25 -06:00
|
|
|
import { RATE_RANGES } from '../prometheus/promql';
|
2019-09-23 06:26:05 -05:00
|
|
|
|
|
|
|
import LokiDatasource from './datasource';
|
2019-10-31 04:48:05 -05:00
|
|
|
import { CompletionItem, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
|
2020-01-24 08:07:45 -06:00
|
|
|
import { Grammar } from 'prismjs';
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2018-10-31 11:48:36 -05:00
|
|
|
const DEFAULT_KEYS = ['job', 'namespace'];
|
2018-10-30 10:14:01 -05:00
|
|
|
const EMPTY_SELECTOR = '{}';
|
2018-10-31 11:48:36 -05:00
|
|
|
const HISTORY_ITEM_COUNT = 10;
|
2018-10-30 10:14:01 -05:00
|
|
|
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
2019-07-10 04:03:06 -05:00
|
|
|
const NS_IN_MS = 1000000;
|
2019-03-25 06:08:28 -05:00
|
|
|
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec
|
2018-10-30 10:14:01 -05:00
|
|
|
|
|
|
|
const wrapLabel = (label: string) => ({ label });
|
2019-07-08 10:14:48 -05:00
|
|
|
export const rangeToParams = (range: AbsoluteTimeRange) => ({ start: range.from * NS_IN_MS, end: range.to * NS_IN_MS });
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2019-09-12 03:02:49 -05:00
|
|
|
export type LokiHistoryItem = HistoryItem<LokiQuery>;
|
|
|
|
|
|
|
|
type TypeaheadContext = {
|
|
|
|
history?: LokiHistoryItem[];
|
|
|
|
absoluteRange?: AbsoluteTimeRange;
|
|
|
|
};
|
2019-01-18 11:14:27 -06:00
|
|
|
|
|
|
|
export function addHistoryMetadata(item: CompletionItem, history: LokiHistoryItem[]): CompletionItem {
|
2018-10-30 10:14:01 -05:00
|
|
|
const cutoffTs = Date.now() - HISTORY_COUNT_CUTOFF;
|
2019-11-15 09:38:25 -06:00
|
|
|
const historyForItem = history.filter(h => h.ts > cutoffTs && h.query.expr === item.label);
|
|
|
|
let hint = `Queried ${historyForItem.length} times in the last 24h.`;
|
2018-10-30 10:14:01 -05:00
|
|
|
const recent = historyForItem[0];
|
2019-11-15 09:38:25 -06:00
|
|
|
|
2018-10-30 10:14:01 -05:00
|
|
|
if (recent) {
|
2019-05-08 06:51:44 -05:00
|
|
|
const lastQueried = dateTime(recent.ts).fromNow();
|
2018-10-30 10:14:01 -05:00
|
|
|
hint = `${hint} Last queried ${lastQueried}.`;
|
|
|
|
}
|
2019-11-15 09:38:25 -06:00
|
|
|
|
2018-10-30 10:14:01 -05:00
|
|
|
return {
|
|
|
|
...item,
|
|
|
|
documentation: hint,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2018-12-05 16:13:57 -06:00
|
|
|
export default class LokiLanguageProvider extends LanguageProvider {
|
2018-10-30 10:14:01 -05:00
|
|
|
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
|
|
|
|
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
|
|
|
|
logLabelOptions: any[];
|
2019-03-25 06:08:28 -05:00
|
|
|
logLabelFetchTs?: number;
|
2018-10-30 10:14:01 -05:00
|
|
|
started: boolean;
|
2019-07-08 10:14:48 -05:00
|
|
|
initialRange: AbsoluteTimeRange;
|
2019-09-23 06:26:05 -05:00
|
|
|
datasource: LokiDatasource;
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
constructor(datasource: LokiDatasource, initialValues?: any) {
|
2018-10-30 10:14:01 -05:00
|
|
|
super();
|
|
|
|
|
|
|
|
this.datasource = datasource;
|
|
|
|
this.labelKeys = {};
|
|
|
|
this.labelValues = {};
|
|
|
|
|
|
|
|
Object.assign(this, initialValues);
|
|
|
|
}
|
2019-09-23 06:26:05 -05:00
|
|
|
|
2018-10-30 10:14:01 -05:00
|
|
|
// Strip syntax chars
|
2019-04-01 00:38:00 -05:00
|
|
|
cleanText = (s: string) => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim();
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2020-01-24 08:07:45 -06:00
|
|
|
getSyntax(): Grammar {
|
2018-11-30 08:13:53 -06:00
|
|
|
return syntax;
|
2018-10-30 10:14:01 -05:00
|
|
|
}
|
|
|
|
|
2019-11-15 09:38:25 -06:00
|
|
|
request = (url: string, params?: any): Promise<{ data: { data: string[] } }> => {
|
2019-07-08 10:14:48 -05:00
|
|
|
return this.datasource.metadataRequest(url, params);
|
2018-10-30 10:14:01 -05:00
|
|
|
};
|
|
|
|
|
2019-09-12 03:02:49 -05:00
|
|
|
/**
|
|
|
|
* Initialise the language provider by fetching set of labels. Without this initialisation the provider would return
|
|
|
|
* just a set of hardcoded default labels on provideCompletionItems or a recent queries from history.
|
|
|
|
*/
|
2018-10-30 10:14:01 -05:00
|
|
|
start = () => {
|
2018-11-01 03:36:09 -05:00
|
|
|
if (!this.startTask) {
|
2019-09-16 00:17:34 -05:00
|
|
|
this.startTask = this.fetchLogLabels(this.initialRange).then(() => {
|
|
|
|
this.started = true;
|
|
|
|
return [];
|
|
|
|
});
|
2018-10-30 10:14:01 -05:00
|
|
|
}
|
2019-11-15 09:38:25 -06:00
|
|
|
|
2018-11-01 03:36:09 -05:00
|
|
|
return this.startTask;
|
2018-10-30 10:14:01 -05:00
|
|
|
};
|
|
|
|
|
2019-09-16 00:17:34 -05:00
|
|
|
getLabelKeys(): string[] {
|
|
|
|
return this.labelKeys[EMPTY_SELECTOR];
|
|
|
|
}
|
|
|
|
|
|
|
|
async getLabelValues(key: string): Promise<string[]> {
|
|
|
|
await this.fetchLabelValues(key, this.initialRange);
|
|
|
|
return this.labelValues[EMPTY_SELECTOR][key];
|
|
|
|
}
|
|
|
|
|
2019-09-12 03:02:49 -05:00
|
|
|
/**
|
|
|
|
* Return suggestions based on input that can be then plugged into a typeahead dropdown.
|
|
|
|
* Keep this DOM-free for testing
|
|
|
|
* @param input
|
|
|
|
* @param context Is optional in types but is required in case we are doing getLabelCompletionItems
|
|
|
|
* @param context.absoluteRange Required in case we are doing getLabelCompletionItems
|
|
|
|
* @param context.history Optional used only in getEmptyCompletionItems
|
|
|
|
*/
|
2019-09-23 06:26:05 -05:00
|
|
|
async provideCompletionItems(input: TypeaheadInput, context?: TypeaheadContext): Promise<TypeaheadOutput> {
|
2019-11-15 09:38:25 -06:00
|
|
|
const { wrapperClasses, value, prefix, text } = input;
|
|
|
|
|
|
|
|
// Local text properties
|
|
|
|
const empty = value.document.text.length === 0;
|
|
|
|
const selectedLines = value.document.getTextsAtRange(value.selection);
|
|
|
|
const currentLine = selectedLines.size === 1 ? selectedLines.first().getText() : null;
|
|
|
|
|
|
|
|
const nextCharacter = currentLine ? currentLine[value.selection.anchor.offset] : null;
|
|
|
|
|
|
|
|
// Syntax spans have 3 classes by default. More indicate a recognized token
|
|
|
|
const tokenRecognized = wrapperClasses.length > 3;
|
|
|
|
|
|
|
|
// Non-empty prefix, but not inside known token
|
|
|
|
const prefixUnrecognized = prefix && !tokenRecognized;
|
|
|
|
|
|
|
|
// Prevent suggestions in `function(|suffix)`
|
|
|
|
const noSuffix = !nextCharacter || nextCharacter === ')';
|
|
|
|
|
2019-12-31 02:53:30 -06:00
|
|
|
// Prefix is safe if it does not immediately follow a complete expression and has no text after it
|
|
|
|
const safePrefix = prefix && !text.match(/^['"~=\]})\s]+$/) && noSuffix;
|
2019-11-15 09:38:25 -06:00
|
|
|
|
|
|
|
// About to type next operand if preceded by binary operator
|
|
|
|
const operatorsPattern = /[+\-*/^%]/;
|
|
|
|
const isNextOperand = text.match(operatorsPattern);
|
|
|
|
|
|
|
|
// Determine candidates by CSS context
|
|
|
|
if (wrapperClasses.includes('context-range')) {
|
|
|
|
// Suggestions for metric[|]
|
|
|
|
return this.getRangeCompletionItems();
|
|
|
|
} else if (wrapperClasses.includes('context-labels')) {
|
|
|
|
// Suggestions for {|} and {foo=|}
|
|
|
|
return await this.getLabelCompletionItems(input, context);
|
|
|
|
} else if (empty) {
|
2019-12-31 02:53:30 -06:00
|
|
|
// Suggestions for empty query field
|
|
|
|
return this.getEmptyCompletionItems(context);
|
|
|
|
} else if (prefixUnrecognized && noSuffix && !isNextOperand) {
|
|
|
|
// Show term suggestions in a couple of scenarios
|
|
|
|
return this.getBeginningCompletionItems(context);
|
|
|
|
} else if (prefixUnrecognized && safePrefix) {
|
2019-11-15 09:38:25 -06:00
|
|
|
// Show term suggestions in a couple of scenarios
|
|
|
|
return this.getTermCompletionItems();
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
|
|
|
suggestions: [],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-12-31 02:53:30 -06:00
|
|
|
getBeginningCompletionItems = (context: TypeaheadContext): TypeaheadOutput => {
|
|
|
|
return {
|
|
|
|
suggestions: [...this.getEmptyCompletionItems(context).suggestions, ...this.getTermCompletionItems().suggestions],
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
getEmptyCompletionItems(context: TypeaheadContext): TypeaheadOutput {
|
|
|
|
const history = context?.history;
|
2019-09-23 06:26:05 -05:00
|
|
|
const suggestions = [];
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2019-11-15 09:38:25 -06:00
|
|
|
if (history && history.length) {
|
2018-10-30 10:14:01 -05:00
|
|
|
const historyItems = _.chain(history)
|
2019-11-15 09:38:25 -06:00
|
|
|
.map(h => h.query.expr)
|
2018-11-23 05:15:26 -06:00
|
|
|
.filter()
|
|
|
|
.uniq()
|
|
|
|
.take(HISTORY_ITEM_COUNT)
|
2018-10-30 10:14:01 -05:00
|
|
|
.map(wrapLabel)
|
2019-04-01 00:38:00 -05:00
|
|
|
.map((item: CompletionItem) => addHistoryMetadata(item, history))
|
2018-10-30 10:14:01 -05:00
|
|
|
.value();
|
|
|
|
|
|
|
|
suggestions.push({
|
|
|
|
prefixMatch: true,
|
|
|
|
skipSort: true,
|
|
|
|
label: 'History',
|
|
|
|
items: historyItems,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return { suggestions };
|
|
|
|
}
|
|
|
|
|
2019-11-15 09:38:25 -06:00
|
|
|
getTermCompletionItems = (): TypeaheadOutput => {
|
|
|
|
const suggestions = [];
|
|
|
|
|
|
|
|
suggestions.push({
|
|
|
|
prefixMatch: true,
|
|
|
|
label: 'Functions',
|
|
|
|
items: FUNCTIONS.map(suggestion => ({ ...suggestion, kind: 'function' })),
|
|
|
|
});
|
|
|
|
|
|
|
|
return { suggestions };
|
|
|
|
};
|
|
|
|
|
|
|
|
getRangeCompletionItems(): TypeaheadOutput {
|
|
|
|
return {
|
|
|
|
context: 'context-range',
|
|
|
|
suggestions: [
|
|
|
|
{
|
|
|
|
label: 'Range vector',
|
|
|
|
items: [...RATE_RANGES],
|
|
|
|
},
|
|
|
|
],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
async getLabelCompletionItems(
|
2019-07-08 10:14:48 -05:00
|
|
|
{ text, wrapperClasses, labelKey, value }: TypeaheadInput,
|
|
|
|
{ absoluteRange }: any
|
2019-09-23 06:26:05 -05:00
|
|
|
): Promise<TypeaheadOutput> {
|
2018-10-30 10:14:01 -05:00
|
|
|
let context: string;
|
2019-09-23 06:26:05 -05:00
|
|
|
const suggestions = [];
|
2018-10-30 10:14:01 -05:00
|
|
|
const line = value.anchorBlock.getText();
|
2019-09-23 06:26:05 -05:00
|
|
|
const cursorOffset: number = value.selection.anchor.offset;
|
2018-10-30 10:14:01 -05:00
|
|
|
|
2018-10-31 11:48:36 -05:00
|
|
|
// Use EMPTY_SELECTOR until series API is implemented for facetting
|
|
|
|
const selector = EMPTY_SELECTOR;
|
2018-10-30 10:14:01 -05:00
|
|
|
let parsedSelector;
|
|
|
|
try {
|
|
|
|
parsedSelector = parseSelector(line, cursorOffset);
|
2018-10-31 11:48:36 -05:00
|
|
|
} catch {}
|
2018-10-30 10:14:01 -05:00
|
|
|
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
|
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) {
|
2018-10-30 10:14:01 -05:00
|
|
|
// Label values
|
2018-10-31 11:48:36 -05:00
|
|
|
if (labelKey && this.labelValues[selector]) {
|
2019-09-23 06:26:05 -05:00
|
|
|
let labelValues = this.labelValues[selector][labelKey];
|
|
|
|
if (!labelValues) {
|
|
|
|
await this.fetchLabelValues(labelKey, absoluteRange);
|
|
|
|
labelValues = this.labelValues[selector][labelKey];
|
2018-10-31 11:48:36 -05:00
|
|
|
}
|
2019-09-23 06:26:05 -05:00
|
|
|
|
|
|
|
context = 'context-label-values';
|
|
|
|
suggestions.push({
|
|
|
|
label: `Label values for "${labelKey}"`,
|
|
|
|
items: labelValues.map(wrapLabel),
|
|
|
|
});
|
2018-10-30 10:14:01 -05:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Label keys
|
2018-10-31 11:48:36 -05:00
|
|
|
const labelKeys = this.labelKeys[selector] || DEFAULT_KEYS;
|
2018-10-30 10:14:01 -05:00
|
|
|
if (labelKeys) {
|
|
|
|
const possibleKeys = _.difference(labelKeys, existingKeys);
|
2019-11-15 09:38:25 -06:00
|
|
|
if (possibleKeys.length) {
|
2018-10-30 10:14:01 -05:00
|
|
|
context = 'context-labels';
|
|
|
|
suggestions.push({ label: `Labels`, items: possibleKeys.map(wrapLabel) });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-23 06:26:05 -05:00
|
|
|
return { context, suggestions };
|
2018-10-30 10:14:01 -05:00
|
|
|
}
|
|
|
|
|
2019-01-18 11:14:27 -06:00
|
|
|
async importQueries(queries: LokiQuery[], datasourceType: string): Promise<LokiQuery[]> {
|
2018-11-13 09:35:20 -06:00
|
|
|
if (datasourceType === 'prometheus') {
|
|
|
|
return Promise.all(
|
2018-11-21 09:28:30 -06:00
|
|
|
queries.map(async query => {
|
|
|
|
const expr = await this.importPrometheusQuery(query.expr);
|
2019-12-27 02:11:16 -06:00
|
|
|
const { ...rest } = query as PromQuery;
|
2018-11-13 09:35:20 -06:00
|
|
|
return {
|
2019-06-03 07:54:32 -05:00
|
|
|
...rest,
|
2018-11-13 09:35:20 -06:00
|
|
|
expr,
|
|
|
|
};
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
2019-01-28 08:27:35 -06:00
|
|
|
// Return a cleaned LokiQuery
|
2018-11-21 09:28:30 -06:00
|
|
|
return queries.map(query => ({
|
2019-01-28 08:27:35 -06:00
|
|
|
refId: query.refId,
|
2018-11-13 09:35:20 -06:00
|
|
|
expr: '',
|
|
|
|
}));
|
|
|
|
}
|
|
|
|
|
|
|
|
async importPrometheusQuery(query: string): Promise<string> {
|
2018-11-21 07:45:57 -06:00
|
|
|
if (!query) {
|
|
|
|
return '';
|
|
|
|
}
|
|
|
|
|
2018-11-13 09:35:20 -06:00
|
|
|
// Consider only first selector in query
|
|
|
|
const selectorMatch = query.match(selectorRegexp);
|
2019-11-15 09:38:25 -06:00
|
|
|
if (!selectorMatch) {
|
|
|
|
return '';
|
|
|
|
}
|
2018-11-13 09:35:20 -06:00
|
|
|
|
2019-11-15 09:38:25 -06:00
|
|
|
const selector = selectorMatch[0];
|
|
|
|
const labels: { [key: string]: { value: any; operator: any } } = {};
|
|
|
|
selector.replace(labelRegexp, (_, key, operator, value) => {
|
|
|
|
labels[key] = { value, operator };
|
|
|
|
return '';
|
|
|
|
});
|
|
|
|
|
|
|
|
// Keep only labels that exist on origin and target datasource
|
|
|
|
await this.start(); // fetches all existing label keys
|
|
|
|
const existingKeys = this.labelKeys[EMPTY_SELECTOR];
|
|
|
|
let labelsToKeep: { [key: string]: { value: any; operator: any } } = {};
|
|
|
|
if (existingKeys && existingKeys.length) {
|
|
|
|
// Check for common labels
|
|
|
|
for (const key in labels) {
|
|
|
|
if (existingKeys && existingKeys.includes(key)) {
|
|
|
|
// Should we check for label value equality here?
|
|
|
|
labelsToKeep[key] = labels[key];
|
2018-11-13 09:35:20 -06:00
|
|
|
}
|
|
|
|
}
|
2019-11-15 09:38:25 -06:00
|
|
|
} else {
|
|
|
|
// Keep all labels by default
|
|
|
|
labelsToKeep = labels;
|
2018-11-13 09:35:20 -06:00
|
|
|
}
|
|
|
|
|
2019-11-15 09:38:25 -06:00
|
|
|
const labelKeys = Object.keys(labelsToKeep).sort();
|
|
|
|
const cleanSelector = labelKeys
|
|
|
|
.map(key => `${key}${labelsToKeep[key].operator}${labelsToKeep[key].value}`)
|
|
|
|
.join(',');
|
|
|
|
|
|
|
|
return ['{', cleanSelector, '}'].join('');
|
2018-11-13 09:35:20 -06:00
|
|
|
}
|
|
|
|
|
2019-07-08 10:14:48 -05:00
|
|
|
async fetchLogLabels(absoluteRange: AbsoluteTimeRange): Promise<any> {
|
2018-10-30 10:14:01 -05:00
|
|
|
const url = '/api/prom/label';
|
|
|
|
try {
|
2019-03-25 06:08:28 -05:00
|
|
|
this.logLabelFetchTs = Date.now();
|
2019-12-10 08:29:32 -06:00
|
|
|
const rangeParams = absoluteRange ? rangeToParams(absoluteRange) : {};
|
|
|
|
const res = await this.request(url, rangeParams);
|
2019-11-15 09:38:25 -06:00
|
|
|
const labelKeys = res.data.data.slice().sort();
|
|
|
|
|
2018-10-31 11:48:36 -05:00
|
|
|
this.labelKeys = {
|
2018-10-30 10:14:01 -05:00
|
|
|
...this.labelKeys,
|
|
|
|
[EMPTY_SELECTOR]: labelKeys,
|
|
|
|
};
|
2019-09-12 03:02:49 -05:00
|
|
|
this.labelValues = {
|
|
|
|
[EMPTY_SELECTOR]: {},
|
|
|
|
};
|
2019-04-01 00:38:00 -05:00
|
|
|
this.logLabelOptions = labelKeys.map((key: string) => ({ label: key, value: key, isLeaf: false }));
|
2018-10-30 10:14:01 -05:00
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
2018-10-31 11:48:36 -05:00
|
|
|
return [];
|
2018-10-30 10:14:01 -05:00
|
|
|
}
|
|
|
|
|
2019-07-08 10:14:48 -05:00
|
|
|
async refreshLogLabels(absoluteRange: AbsoluteTimeRange, forceRefresh?: boolean) {
|
2019-04-01 00:38:00 -05:00
|
|
|
if ((this.labelKeys && Date.now() - this.logLabelFetchTs > LABEL_REFRESH_INTERVAL) || forceRefresh) {
|
2019-07-08 10:14:48 -05:00
|
|
|
await this.fetchLogLabels(absoluteRange);
|
2019-03-25 06:08:28 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-08 10:14:48 -05:00
|
|
|
async fetchLabelValues(key: string, absoluteRange: AbsoluteTimeRange) {
|
2018-10-30 10:14:01 -05:00
|
|
|
const url = `/api/prom/label/${key}/values`;
|
|
|
|
try {
|
2019-12-10 08:29:32 -06:00
|
|
|
const rangeParams = absoluteRange ? rangeToParams(absoluteRange) : {};
|
|
|
|
const res = await this.request(url, rangeParams);
|
2019-11-15 09:38:25 -06:00
|
|
|
const values = res.data.data.slice().sort();
|
2018-10-31 11:48:36 -05:00
|
|
|
|
|
|
|
// Add to label options
|
|
|
|
this.logLabelOptions = this.logLabelOptions.map(keyOption => {
|
|
|
|
if (keyOption.value === key) {
|
|
|
|
return {
|
|
|
|
...keyOption,
|
2019-11-15 09:38:25 -06:00
|
|
|
children: values.map(value => ({ label: value, value })),
|
2018-10-31 11:48:36 -05:00
|
|
|
};
|
|
|
|
}
|
|
|
|
return keyOption;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add to key map
|
2018-10-30 10:14:01 -05:00
|
|
|
const exisingValues = this.labelValues[EMPTY_SELECTOR];
|
2018-10-31 11:48:36 -05:00
|
|
|
const nextValues = {
|
2018-10-30 10:14:01 -05:00
|
|
|
...exisingValues,
|
2018-10-31 11:48:36 -05:00
|
|
|
[key]: values,
|
2018-10-30 10:14:01 -05:00
|
|
|
};
|
|
|
|
this.labelValues = {
|
|
|
|
...this.labelValues,
|
2018-10-31 11:48:36 -05:00
|
|
|
[EMPTY_SELECTOR]: nextValues,
|
2018-10-30 10:14:01 -05:00
|
|
|
};
|
|
|
|
} catch (e) {
|
|
|
|
console.error(e);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|