Graphite: Migrate to React (part 1: move state to redux) (#36484)
* Add UMLs * Add rendered diagrams * Move QueryCtrl to flux * Remove redundant param in the reducer * Use named imports for lodash and fix typing for GraphiteTagOperator * Add missing async/await * Extract providers to a separate file * Clean up async await * Rename controller functions back to main * Simplify creating actions * Re-order controller functions * Separate helpers from actions * Rename vars * Simplify helpers * Move controller methods to state reducers * Remove docs (they are added in design doc) * Move actions.ts to state folder * Add docs * Add old methods stubs for easier review * Check how state dependencies will be mapped * Rename state to store * Rename state to store * Rewrite spec tests for Graphite Query Controller * Update docs * Update docs * Update public/app/plugins/datasource/graphite/state/helpers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/helpers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/helpers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Update public/app/plugins/datasource/graphite/state/providers.ts Co-authored-by: Giordano Ricci <me@giordanoricci.com> * Add more type definitions * Load function definitions before parsing the target on initial load * Change targetChanged to updateQuery to avoid mutating state directly It's also needed for extra refresh/runQuery execution as handleTargetChanged doesn't handle changing the raw query * Fix updating query after adding a function * Simplify updating function params * Remove redundant awaits * Use redux Action * Use more specific type for GraphiteTag Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
co-authored by
Giordano Ricci
parent
71a9f7ce3c
commit
dbe7d3298d
@@ -25,6 +25,7 @@ export function graphiteAddFunc($compile: any) {
|
|||||||
$input.appendTo(elem);
|
$input.appendTo(elem);
|
||||||
$button.appendTo(elem);
|
$button.appendTo(elem);
|
||||||
|
|
||||||
|
// TODO: ctrl.state is not ready yet when link() is called. This will be moved to a separate provider.
|
||||||
ctrl.datasource.getFuncDefs().then((funcDefs: FuncDef[]) => {
|
ctrl.datasource.getFuncDefs().then((funcDefs: FuncDef[]) => {
|
||||||
const allFunctions = map(funcDefs, 'name').sort();
|
const allFunctions = map(funcDefs, 'name').sort();
|
||||||
|
|
||||||
@@ -36,7 +37,7 @@ export function graphiteAddFunc($compile: any) {
|
|||||||
minLength: 1,
|
minLength: 1,
|
||||||
items: 10,
|
items: 10,
|
||||||
updater: (value: any) => {
|
updater: (value: any) => {
|
||||||
let funcDef: any = ctrl.datasource.getFuncDef(value);
|
let funcDef: any = ctrl.state.datasource.getFuncDef(value);
|
||||||
if (!funcDef) {
|
if (!funcDef) {
|
||||||
// try find close match
|
// try find close match
|
||||||
value = value.toLowerCase();
|
value = value.toLowerCase();
|
||||||
@@ -96,7 +97,7 @@ export function graphiteAddFunc($compile: any) {
|
|||||||
|
|
||||||
let funcDef;
|
let funcDef;
|
||||||
try {
|
try {
|
||||||
funcDef = ctrl.datasource.getFuncDef($('a', this).text());
|
funcDef = ctrl.state.datasource.getFuncDef($('a', this).text());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { assign, clone, each, last, map, partial } from 'lodash';
|
|||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
import { actions } from './state/actions';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
||||||
@@ -28,15 +29,15 @@ export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
|||||||
let cancelBlur: any = null;
|
let cancelBlur: any = null;
|
||||||
|
|
||||||
ctrl.handleRemoveFunction = (func: any) => {
|
ctrl.handleRemoveFunction = (func: any) => {
|
||||||
ctrl.removeFunction(func);
|
ctrl.dispatch(actions.removeFunction({ func }));
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.handleMoveLeft = (func: any) => {
|
ctrl.handleMoveLeft = (func: any) => {
|
||||||
ctrl.moveFunction(func, -1);
|
ctrl.dispatch(actions.moveFunction({ func, offset: -1 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
ctrl.handleMoveRight = (func: any) => {
|
ctrl.handleMoveRight = (func: any) => {
|
||||||
ctrl.moveFunction(func, 1);
|
ctrl.dispatch(actions.moveFunction({ func, offset: 1 }));
|
||||||
};
|
};
|
||||||
|
|
||||||
function clickFuncParam(this: any, paramIndex: any) {
|
function clickFuncParam(this: any, paramIndex: any) {
|
||||||
@@ -102,7 +103,10 @@ export function graphiteFuncEditor($compile: any, templateSrv: TemplateSrv) {
|
|||||||
scheduledRelinkIfNeeded();
|
scheduledRelinkIfNeeded();
|
||||||
|
|
||||||
$scope.$apply(() => {
|
$scope.$apply(() => {
|
||||||
ctrl.targetChanged();
|
// WIP: at the moment function params are mutated directly by func_editor
|
||||||
|
// after migrating to react it will be done by passing param value to
|
||||||
|
// updateFunctionParam action
|
||||||
|
ctrl.dispatch(actions.updateFunctionParam({ func }));
|
||||||
});
|
});
|
||||||
|
|
||||||
if ($link.hasClass('query-part__last') && newValue === '') {
|
if ($link.hasClass('query-part__last') && newValue === '') {
|
||||||
|
|||||||
@@ -974,6 +974,13 @@ export class FuncInstance {
|
|||||||
params: any;
|
params: any;
|
||||||
text: any;
|
text: any;
|
||||||
added: boolean;
|
added: boolean;
|
||||||
|
/**
|
||||||
|
* Hidden functions are not displayed in UI but available in text editor
|
||||||
|
* This is used for seriesByTagUsed function which when used switches
|
||||||
|
* the editor to tag-only mode. Defined tags are provided to seriesByTagUsed
|
||||||
|
* as parameters.
|
||||||
|
*/
|
||||||
|
hidden?: boolean;
|
||||||
|
|
||||||
constructor(funcDef: any, options?: { withDefaultParams: any }) {
|
constructor(funcDef: any, options?: { withDefaultParams: any }) {
|
||||||
this.def = funcDef;
|
this.def = funcDef;
|
||||||
|
|||||||
@@ -3,6 +3,9 @@ import { arrayMove } from 'app/core/utils/arrayMove';
|
|||||||
import { Parser } from './parser';
|
import { Parser } from './parser';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { ScopedVars } from '@grafana/data';
|
import { ScopedVars } from '@grafana/data';
|
||||||
|
import { FuncInstance } from './gfunc';
|
||||||
|
import { GraphiteSegment } from './types';
|
||||||
|
import { GraphiteDatasource } from './datasource';
|
||||||
|
|
||||||
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
|
export type GraphiteTagOperator = '=' | '=~' | '!=' | '!=~';
|
||||||
|
|
||||||
@@ -12,11 +15,22 @@ export type GraphiteTag = {
|
|||||||
value: string;
|
value: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type GraphiteTarget = {
|
||||||
|
refId: string | number;
|
||||||
|
target: string;
|
||||||
|
/**
|
||||||
|
* Contains full query after interpolating sub-queries (e.g. "function(#A)" referencing query with refId=A)
|
||||||
|
*/
|
||||||
|
targetFull: string;
|
||||||
|
textEditor: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export default class GraphiteQuery {
|
export default class GraphiteQuery {
|
||||||
datasource: any;
|
datasource: GraphiteDatasource;
|
||||||
target: any;
|
target: GraphiteTarget;
|
||||||
functions: any[] = [];
|
functions: FuncInstance[] = [];
|
||||||
segments: any[] = [];
|
segments: GraphiteSegment[] = [];
|
||||||
tags: GraphiteTag[] = [];
|
tags: GraphiteTag[] = [];
|
||||||
error: any;
|
error: any;
|
||||||
seriesByTagUsed = false;
|
seriesByTagUsed = false;
|
||||||
@@ -272,12 +286,12 @@ export default class GraphiteQuery {
|
|||||||
|
|
||||||
addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) {
|
addTag(tag: { key: any; operator: GraphiteTagOperator; value: string }) {
|
||||||
const newTagParam = renderTagString(tag);
|
const newTagParam = renderTagString(tag);
|
||||||
this.getSeriesByTagFunc().params.push(newTagParam);
|
this.getSeriesByTagFunc()!.params.push(newTagParam);
|
||||||
this.tags.push(tag);
|
this.tags.push(tag);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTag(index: number) {
|
removeTag(index: number) {
|
||||||
this.getSeriesByTagFunc().params.splice(index, 1);
|
this.getSeriesByTagFunc()!.params.splice(index, 1);
|
||||||
this.tags.splice(index, 1);
|
this.tags.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,7 +304,7 @@ export default class GraphiteQuery {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newTagParam = renderTagString(tag);
|
const newTagParam = renderTagString(tag);
|
||||||
this.getSeriesByTagFunc().params[tagIndex] = newTagParam;
|
this.getSeriesByTagFunc()!.params[tagIndex] = newTagParam;
|
||||||
this.tags[tagIndex] = tag;
|
this.tags[tagIndex] = tag;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.target.textEditor">
|
<div class="gf-form" ng-show="ctrl.state.target.textEditor">
|
||||||
<input type="text" class="gf-form-input" style="font-family: monospace;" ng-model="ctrl.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged()"></input>
|
<input type="text" class="gf-form-input" style="font-family: monospace;" ng-value="ctrl.state.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged($event)"></input>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-hide="ctrl.target.textEditor">
|
<div ng-hide="ctrl.target.textEditor">
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<label class="gf-form-label width-6 query-keyword">Series</label>
|
<label class="gf-form-label width-6 query-keyword">Series</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.queryModel.tags" class="gf-form">
|
<div ng-if="ctrl.state.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.state.queryModel.tags" class="gf-form">
|
||||||
<gf-form-dropdown
|
<gf-form-dropdown
|
||||||
model="tag.key"
|
model="tag.key"
|
||||||
allow-custom="true"
|
allow-custom="true"
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
get-options="ctrl.getTags($index, $query)"
|
get-options="ctrl.getTags($index, $query)"
|
||||||
on-change="ctrl.tagChanged(tag, $index)"
|
on-change="ctrl.tagChanged(tag, $index)"
|
||||||
></gf-form-dropdown>
|
></gf-form-dropdown>
|
||||||
|
|
||||||
<gf-form-dropdown
|
<gf-form-dropdown
|
||||||
model="tag.operator"
|
model="tag.operator"
|
||||||
label-mode="true"
|
label-mode="true"
|
||||||
@@ -39,19 +39,19 @@
|
|||||||
placeholder="Tag value"
|
placeholder="Tag value"
|
||||||
get-options="ctrl.getTagValues(tag, $index, $query)"
|
get-options="ctrl.getTagValues(tag, $index, $query)"
|
||||||
on-change="ctrl.tagChanged(tag, $index)"
|
on-change="ctrl.tagChanged(tag, $index)"
|
||||||
/>
|
></gf-form-dropdown>
|
||||||
<label class="gf-form-label query-keyword" ng-if="ctrl.showDelimiter($index)">AND</label>
|
<label class="gf-form-label query-keyword" ng-if="$index !== ctrl.state.queryModel.tags.length - 1">AND</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.addTagSegments" role="menuitem" class="gf-form">
|
<div ng-if="ctrl.state.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.state.addTagSegments" role="menuitem" class="gf-form">
|
||||||
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" debounce="true" />
|
<metric-segment segment="segment" get-options="ctrl.getTagsAsSegments($query)" on-change="ctrl.addNewTag(segment)" debounce="true" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="!ctrl.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.segments" role="menuitem" class="gf-form">
|
<div ng-if="!ctrl.state.queryModel.seriesByTagUsed" ng-repeat="segment in ctrl.state.segments" role="menuitem" class="gf-form">
|
||||||
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
|
<metric-segment segment="segment" get-options="ctrl.getAltSegments($index, $query)" on-change="ctrl.segmentValueChanged(segment, $index)" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.paused" class="gf-form">
|
<div ng-if="ctrl.state.paused" class="gf-form">
|
||||||
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><icon name="'play'"></icon></a>
|
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><icon name="'play'"></icon></a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<label class="gf-form-label width-6 query-keyword">Functions</label>
|
<label class="gf-form-label width-6 query-keyword">Functions</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-repeat="func in ctrl.queryModel.functions" class="gf-form">
|
<div ng-repeat="func in ctrl.state.queryModel.functions" class="gf-form">
|
||||||
<span graphite-func-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
|
<span graphite-func-editor class="gf-form-label query-part" ng-hide="func.hidden"></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -78,5 +78,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</query-editor-row>
|
</query-editor-row>
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
import './add_graphite_func';
|
import './add_graphite_func';
|
||||||
import './func_editor';
|
import './func_editor';
|
||||||
|
|
||||||
import { each, eachRight, map, remove } from 'lodash';
|
import GraphiteQuery from './graphite_query';
|
||||||
import GraphiteQuery, { GraphiteTagOperator } from './graphite_query';
|
|
||||||
import { QueryCtrl } from 'app/plugins/sdk';
|
import { QueryCtrl } from 'app/plugins/sdk';
|
||||||
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
|
|
||||||
import { auto } from 'angular';
|
import { auto } from 'angular';
|
||||||
import { TemplateSrv } from '@grafana/runtime';
|
import { TemplateSrv } from '@grafana/runtime';
|
||||||
import { dispatch } from 'app/store/store';
|
import { actions } from './state/actions';
|
||||||
import { notifyApp } from 'app/core/actions';
|
import { getAltSegments, getTagOperators, getTags, getTagsAsSegments, getTagValues } from './state/providers';
|
||||||
import { createErrorNotification } from 'app/core/copy/appNotification';
|
import { createStore, GraphiteQueryEditorState } from './state/store';
|
||||||
|
import {
|
||||||
const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
|
AngularDropdownOptions,
|
||||||
const TAG_PREFIX = 'tag: ';
|
GraphiteActionDispatcher,
|
||||||
|
GraphiteQueryEditorAngularDependencies,
|
||||||
|
GraphiteSegment,
|
||||||
|
GraphiteTag,
|
||||||
|
} from './types';
|
||||||
|
import { ChangeEvent } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Moved to state/store
|
||||||
|
*
|
||||||
|
* Note: methods marked with WIP are kept for easier diffing with previous changes. They will be removed when
|
||||||
|
* GraphiteQueryCtrl is replaced with a react component.
|
||||||
|
*/
|
||||||
export class GraphiteQueryCtrl extends QueryCtrl {
|
export class GraphiteQueryCtrl extends QueryCtrl {
|
||||||
static templateUrl = 'partials/query.editor.html';
|
static templateUrl = 'partials/query.editor.html';
|
||||||
|
|
||||||
@@ -24,437 +33,227 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
|||||||
supportsTags = false;
|
supportsTags = false;
|
||||||
paused = false;
|
paused = false;
|
||||||
|
|
||||||
// to avoid error flooding, these errors are shown only once per session
|
private state: GraphiteQueryEditorState;
|
||||||
private _tagsAutoCompleteErrorShown = false;
|
private readonly dispatch: GraphiteActionDispatcher;
|
||||||
private _metricAutoCompleteErrorShown = false;
|
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
$scope: any,
|
$scope: any,
|
||||||
$injector: auto.IInjectorService,
|
$injector: auto.IInjectorService,
|
||||||
private uiSegmentSrv: any,
|
private uiSegmentSrv: any,
|
||||||
private templateSrv: TemplateSrv,
|
private templateSrv: TemplateSrv
|
||||||
$timeout: any
|
|
||||||
) {
|
) {
|
||||||
super($scope, $injector);
|
super($scope, $injector);
|
||||||
this.supportsTags = this.datasource.supportsTags;
|
|
||||||
this.paused = false;
|
|
||||||
this.target.target = this.target.target || '';
|
|
||||||
|
|
||||||
this.datasource.waitForFuncDefsLoaded().then(() => {
|
// This controller will be removed once it's root partial (query.editor.html) renders only React components.
|
||||||
this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
|
// All component will be wrapped in ReactQueryEditor receiving DataSourceApi in QueryRow.renderQueryEditor
|
||||||
this.buildSegments(false);
|
// The init() action will be removed and the store will be created in ReactQueryEditor. Note that properties
|
||||||
|
// passed to React component in QueryRow.renderQueryEditor are different than properties passed to Angular editor
|
||||||
|
// and will be mapped/provided in a way described below:
|
||||||
|
const deps = {
|
||||||
|
// WIP: to be removed. It's not passed to ReactQueryEditor but it's used only to:
|
||||||
|
// - get refId of the query (refId be passed in query property),
|
||||||
|
// - and to refresh changes (this will be handled by onChange passed to ReactQueryEditor)
|
||||||
|
// - it's needed to get other targets to interpolate the query (this will be added in QueryRow)
|
||||||
|
panelCtrl: this.panelCtrl,
|
||||||
|
|
||||||
|
// WIP: to be replaced with query property passed to ReactQueryEditor
|
||||||
|
target: this.target,
|
||||||
|
|
||||||
|
// WIP: same object will be passed to ReactQueryEditor
|
||||||
|
datasource: this.datasource,
|
||||||
|
|
||||||
|
// This is used to create view models for Angular <metric-segment> component (view models are MetricSegment objects)
|
||||||
|
// It will be simplified to produce data needed by React <SegmentAsync/> component
|
||||||
|
uiSegmentSrv: this.uiSegmentSrv,
|
||||||
|
|
||||||
|
// WIP: will be replaced with:
|
||||||
|
// import { getTemplateSrv } from 'app/features/templating/template_srv';
|
||||||
|
templateSrv: this.templateSrv,
|
||||||
|
};
|
||||||
|
|
||||||
|
const [dispatch, state] = createStore((state) => {
|
||||||
|
this.state = state;
|
||||||
|
// HACK: inefficient but not invoked frequently. It's needed to inform angular watcher about state changes
|
||||||
|
// for state shared between React/AngularJS. Actions invoked from React component will not mark the scope
|
||||||
|
// as dirty and the view won't be updated. It has to happen manually on each state change.
|
||||||
|
this.$scope.$digest();
|
||||||
});
|
});
|
||||||
|
|
||||||
this.removeTagValue = '-- remove tag --';
|
this.state = state;
|
||||||
|
this.dispatch = dispatch;
|
||||||
|
|
||||||
|
this.dispatch(actions.init(deps as GraphiteQueryEditorAngularDependencies));
|
||||||
}
|
}
|
||||||
|
|
||||||
parseTarget() {
|
parseTarget() {
|
||||||
this.queryModel.parseTarget();
|
// WIP: moved to state/helpers (the same name)
|
||||||
this.buildSegments();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleEditorMode() {
|
async toggleEditorMode() {
|
||||||
this.target.textEditor = !this.target.textEditor;
|
await this.dispatch(actions.toggleEditorMode());
|
||||||
this.parseTarget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildSegments(modifyLastSegment = true) {
|
buildSegments(modifyLastSegment = true) {
|
||||||
this.segments = map(this.queryModel.segments, (segment) => {
|
// WIP: moved to state/helpers (the same name)
|
||||||
return this.uiSegmentSrv.newSegment(segment);
|
|
||||||
});
|
|
||||||
|
|
||||||
const checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0;
|
|
||||||
|
|
||||||
promiseToDigest(this.$scope)(this.checkOtherSegments(checkOtherSegmentsIndex, modifyLastSegment));
|
|
||||||
|
|
||||||
if (this.queryModel.seriesByTagUsed) {
|
|
||||||
this.fixTagSegments();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addSelectMetricSegment() {
|
addSelectMetricSegment() {
|
||||||
this.queryModel.addSelectMetricSegment();
|
// WIP: moved to state/helpers (the same name)
|
||||||
this.segments.push(this.uiSegmentSrv.newSelectMetric());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
|
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
|
||||||
if (this.queryModel.segments.length === 1 && this.queryModel.segments[0].type === 'series-ref') {
|
// WIP: moved to state/helpers (the same name)
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fromIndex === 0) {
|
|
||||||
this.addSelectMetricSegment();
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = this.queryModel.getSegmentPathUpTo(fromIndex + 1);
|
|
||||||
if (path === '') {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.metricFindQuery(path)
|
|
||||||
.then((segments: any) => {
|
|
||||||
if (segments.length === 0) {
|
|
||||||
if (path !== '' && modifyLastSegment) {
|
|
||||||
this.queryModel.segments = this.queryModel.segments.splice(0, fromIndex);
|
|
||||||
this.segments = this.segments.splice(0, fromIndex);
|
|
||||||
this.addSelectMetricSegment();
|
|
||||||
}
|
|
||||||
} else if (segments[0].expandable) {
|
|
||||||
if (this.segments.length === fromIndex) {
|
|
||||||
this.addSelectMetricSegment();
|
|
||||||
} else {
|
|
||||||
return this.checkOtherSegments(fromIndex + 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
this.handleMetricsAutoCompleteError(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setSegmentFocus(segmentIndex: any) {
|
setSegmentFocus(segmentIndex: any) {
|
||||||
each(this.segments, (segment, index) => {
|
// WIP: moved to state/helpers (the same name)
|
||||||
segment.focus = segmentIndex === index;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAltSegments(index: number, prefix: string) {
|
/**
|
||||||
let query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
|
* Get list of options for an empty segment or a segment with metric when it's clicked/opened.
|
||||||
if (index > 0) {
|
*
|
||||||
query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
|
* This is used for new segments and segments with metrics selected.
|
||||||
}
|
*/
|
||||||
const options = {
|
async getAltSegments(index: number, text: string): Promise<GraphiteSegment[]> {
|
||||||
range: this.panelCtrl.range,
|
return await getAltSegments(this.state, index, text);
|
||||||
requestId: 'get-alt-segments',
|
|
||||||
};
|
|
||||||
|
|
||||||
return this.datasource
|
|
||||||
.metricFindQuery(query, options)
|
|
||||||
.then((segments: any[]) => {
|
|
||||||
const altSegments = map(segments, (segment) => {
|
|
||||||
return this.uiSegmentSrv.newSegment({
|
|
||||||
value: segment.text,
|
|
||||||
expandable: segment.expandable,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
if (index > 0 && altSegments.length === 0) {
|
|
||||||
return altSegments;
|
|
||||||
}
|
|
||||||
|
|
||||||
// add query references
|
|
||||||
if (index === 0) {
|
|
||||||
eachRight(this.panelCtrl.panel.targets, (target) => {
|
|
||||||
if (target.refId === this.queryModel.target.refId) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
altSegments.unshift(
|
|
||||||
this.uiSegmentSrv.newSegment({
|
|
||||||
type: 'series-ref',
|
|
||||||
value: '#' + target.refId,
|
|
||||||
expandable: false,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// add template variables
|
|
||||||
eachRight(this.templateSrv.getVariables(), (variable) => {
|
|
||||||
altSegments.unshift(
|
|
||||||
this.uiSegmentSrv.newSegment({
|
|
||||||
type: 'template',
|
|
||||||
value: '$' + variable.name,
|
|
||||||
expandable: true,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// add wildcard option
|
|
||||||
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
|
|
||||||
|
|
||||||
if (this.supportsTags && index === 0) {
|
|
||||||
this.removeTaggedEntry(altSegments);
|
|
||||||
return this.addAltTagSegments(prefix, altSegments);
|
|
||||||
} else {
|
|
||||||
return altSegments;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((err: any): any[] => {
|
|
||||||
this.handleMetricsAutoCompleteError(err);
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addAltTagSegments(prefix: string, altSegments: any[]) {
|
addAltTagSegments(prefix: string, altSegments: any[]) {
|
||||||
return this.getTagsAsSegments(prefix).then((tagSegments: any[]) => {
|
// WIP: moved to state/providers (the same name)
|
||||||
tagSegments = map(tagSegments, (segment) => {
|
|
||||||
segment.value = TAG_PREFIX + segment.value;
|
|
||||||
return segment;
|
|
||||||
});
|
|
||||||
return altSegments.concat(...tagSegments);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTaggedEntry(altSegments: any[]) {
|
removeTaggedEntry(altSegments: any[]) {
|
||||||
altSegments = remove(altSegments, (s) => s.value === '_tagged');
|
// WIP: moved to state/providers (the same name)
|
||||||
}
|
}
|
||||||
|
|
||||||
segmentValueChanged(segment: { type: string; value: string; expandable: any }, segmentIndex: number) {
|
/**
|
||||||
this.error = null;
|
* Apply changes to a given metric segment
|
||||||
this.queryModel.updateSegmentValue(segment, segmentIndex);
|
*/
|
||||||
|
async segmentValueChanged(segment: GraphiteSegment, index: number) {
|
||||||
if (this.queryModel.functions.length > 0 && this.queryModel.functions[0].def.fake) {
|
await this.dispatch(actions.segmentValueChanged({ segment, index }));
|
||||||
this.queryModel.functions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.type === 'tag') {
|
|
||||||
const tag = removeTagPrefix(segment.value);
|
|
||||||
this.pause();
|
|
||||||
this.addSeriesByTagFunc(tag);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (segment.expandable) {
|
|
||||||
return promiseToDigest(this.$scope)(
|
|
||||||
this.checkOtherSegments(segmentIndex + 1).then(() => {
|
|
||||||
this.setSegmentFocus(segmentIndex + 1);
|
|
||||||
this.targetChanged();
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
this.spliceSegments(segmentIndex + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setSegmentFocus(segmentIndex + 1);
|
|
||||||
this.targetChanged();
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
spliceSegments(index: any) {
|
spliceSegments(index: any) {
|
||||||
this.segments = this.segments.splice(0, index);
|
// WIP: moved to state/helpers (the same name)
|
||||||
this.queryModel.segments = this.queryModel.segments.splice(0, index);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
emptySegments() {
|
emptySegments() {
|
||||||
this.queryModel.segments = [];
|
// WIP: moved to state/helpers (the same name)
|
||||||
this.segments = [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
targetTextChanged() {
|
async targetTextChanged(event: ChangeEvent<HTMLInputElement>) {
|
||||||
this.updateModelTarget();
|
await this.dispatch(actions.updateQuery({ query: event.target.value }));
|
||||||
this.refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateModelTarget() {
|
updateModelTarget() {
|
||||||
this.queryModel.updateModelTarget(this.panelCtrl.panel.targets);
|
// WIP: moved to state/helpers as handleTargetChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
targetChanged() {
|
async addFunction(name: string) {
|
||||||
if (this.queryModel.error) {
|
await this.dispatch(actions.addFunction({ name }));
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldTarget = this.queryModel.target.target;
|
|
||||||
this.updateModelTarget();
|
|
||||||
|
|
||||||
if (this.queryModel.target !== oldTarget && !this.paused) {
|
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addFunction(funcDef: any) {
|
|
||||||
const newFunc = this.datasource.createFuncInstance(funcDef, {
|
|
||||||
withDefaultParams: true,
|
|
||||||
});
|
|
||||||
newFunc.added = true;
|
|
||||||
this.queryModel.addFunction(newFunc);
|
|
||||||
this.smartlyHandleNewAliasByNode(newFunc);
|
|
||||||
|
|
||||||
if (this.segments.length === 1 && this.segments[0].fake) {
|
|
||||||
this.emptySegments();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newFunc.params.length && newFunc.added) {
|
|
||||||
this.targetChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newFunc.def.name === 'seriesByTag') {
|
|
||||||
this.parseTarget();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeFunction(func: any) {
|
removeFunction(func: any) {
|
||||||
this.queryModel.removeFunction(func);
|
// WIP: converted to "removeFunction" action and handled in state/store reducer
|
||||||
this.targetChanged();
|
// It's now dispatched in func_editor
|
||||||
}
|
}
|
||||||
|
|
||||||
moveFunction(func: any, offset: any) {
|
moveFunction(func: any, offset: any) {
|
||||||
this.queryModel.moveFunction(func, offset);
|
// WIP: converted to "moveFunction" action and handled in state/store reducer
|
||||||
this.targetChanged();
|
// It's now dispatched in func_editor
|
||||||
}
|
}
|
||||||
|
|
||||||
addSeriesByTagFunc(tag: string) {
|
addSeriesByTagFunc(tag: string) {
|
||||||
const newFunc = this.datasource.createFuncInstance('seriesByTag', {
|
// WIP: moved to state/helpers (the same name)
|
||||||
withDefaultParams: false,
|
// It's now dispatched in func_editor
|
||||||
});
|
|
||||||
const tagParam = `${tag}=`;
|
|
||||||
newFunc.params = [tagParam];
|
|
||||||
this.queryModel.addFunction(newFunc);
|
|
||||||
newFunc.added = true;
|
|
||||||
|
|
||||||
this.emptySegments();
|
|
||||||
this.targetChanged();
|
|
||||||
this.parseTarget();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
smartlyHandleNewAliasByNode(func: { def: { name: string }; params: number[]; added: boolean }) {
|
smartlyHandleNewAliasByNode(func: { def: { name: string }; params: number[]; added: boolean }) {
|
||||||
if (func.def.name !== 'aliasByNode') {
|
// WIP: moved to state/helpers (the same name)
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < this.segments.length; i++) {
|
|
||||||
if (this.segments[i].value.indexOf('*') >= 0) {
|
|
||||||
func.params[0] = i;
|
|
||||||
func.added = false;
|
|
||||||
this.targetChanged();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllTags() {
|
getAllTags() {
|
||||||
return this.datasource.getTags().then((values: any[]) => {
|
// WIP: removed. It was not used.
|
||||||
const altTags = map(values, 'text');
|
|
||||||
altTags.splice(0, 0, this.removeTagValue);
|
|
||||||
return mapToDropdownOptions(altTags);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTags(index: number, tagPrefix: any) {
|
/**
|
||||||
const tagExpressions = this.queryModel.renderTagExpressions(index);
|
* Get list of tags for editing exiting tag with <gf-form-dropdown>
|
||||||
return this.datasource
|
*/
|
||||||
.getTagsAutoComplete(tagExpressions, tagPrefix)
|
async getTags(index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||||
.then((values: any) => {
|
return await getTags(this.state, index, query);
|
||||||
const altTags = map(values, 'text');
|
|
||||||
altTags.splice(0, 0, this.removeTagValue);
|
|
||||||
return mapToDropdownOptions(altTags);
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
this.handleTagsAutoCompleteError(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagsAsSegments(tagPrefix: string) {
|
/**
|
||||||
const tagExpressions = this.queryModel.renderTagExpressions();
|
* Get tag list when adding a new tag with <metric-segment>
|
||||||
return this.datasource
|
*/
|
||||||
.getTagsAutoComplete(tagExpressions, tagPrefix)
|
async getTagsAsSegments(query: string): Promise<GraphiteSegment[]> {
|
||||||
.then((values: any) => {
|
return await getTagsAsSegments(this.state, query);
|
||||||
return map(values, (val) => {
|
|
||||||
return this.uiSegmentSrv.newSegment({
|
|
||||||
value: val.text,
|
|
||||||
type: 'tag',
|
|
||||||
expandable: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err: any) => {
|
|
||||||
this.handleTagsAutoCompleteError(err);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagOperators() {
|
/**
|
||||||
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
* Get list of available tag operators
|
||||||
|
*/
|
||||||
|
getTagOperators(): AngularDropdownOptions[] {
|
||||||
|
return getTagOperators();
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllTagValues(tag: { key: any }) {
|
getAllTagValues(tag: { key: any }) {
|
||||||
const tagKey = tag.key;
|
// WIP: removed. It was not used.
|
||||||
return this.datasource.getTagValues(tagKey).then((values: any[]) => {
|
|
||||||
const altValues = map(values, 'text');
|
|
||||||
return mapToDropdownOptions(altValues);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getTagValues(tag: { key: any }, index: number, valuePrefix: any) {
|
/**
|
||||||
const tagExpressions = this.queryModel.renderTagExpressions(index);
|
* Get list of available tag values
|
||||||
const tagKey = tag.key;
|
*/
|
||||||
return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then((values: any[]) => {
|
async getTagValues(tag: GraphiteTag, index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||||
const altValues = map(values, 'text');
|
return await getTagValues(this.state, tag, index, query);
|
||||||
// Add template variables as additional values
|
|
||||||
eachRight(this.templateSrv.getVariables(), (variable) => {
|
|
||||||
altValues.push('${' + variable.name + ':regex}');
|
|
||||||
});
|
|
||||||
return mapToDropdownOptions(altValues);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tagChanged(tag: any, tagIndex: any) {
|
/**
|
||||||
this.queryModel.updateTag(tag, tagIndex);
|
* Apply changes when a tag is changed
|
||||||
this.targetChanged();
|
*/
|
||||||
|
async tagChanged(tag: GraphiteTag, index: number) {
|
||||||
|
await this.dispatch(actions.tagChanged({ tag, index }));
|
||||||
}
|
}
|
||||||
|
|
||||||
addNewTag(segment: { value: any }) {
|
async addNewTag(segment: GraphiteSegment) {
|
||||||
const newTagKey = segment.value;
|
await this.dispatch(actions.addNewTag({ segment }));
|
||||||
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
|
|
||||||
this.queryModel.addTag(newTag);
|
|
||||||
this.targetChanged();
|
|
||||||
this.fixTagSegments();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
removeTag(index: any) {
|
removeTag(index: any) {
|
||||||
this.queryModel.removeTag(index);
|
// WIP: removed. It was not used.
|
||||||
this.targetChanged();
|
// Tags are removed by selecting the segment called "-- remove tag --"
|
||||||
}
|
}
|
||||||
|
|
||||||
fixTagSegments() {
|
fixTagSegments() {
|
||||||
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
|
// WIP: moved to state/helpers (the same name)
|
||||||
this.addTagSegments = [this.uiSegmentSrv.newPlusButton()];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
showDelimiter(index: number) {
|
showDelimiter(index: number) {
|
||||||
return index !== this.queryModel.tags.length - 1;
|
// WIP: removed. It was not used because of broken syntax in the template. The logic has been moved directly to the template
|
||||||
}
|
}
|
||||||
|
|
||||||
pause() {
|
pause() {
|
||||||
this.paused = true;
|
// WIP: moved to state/helpers (the same name)
|
||||||
}
|
}
|
||||||
|
|
||||||
unpause() {
|
async unpause() {
|
||||||
this.paused = false;
|
await this.dispatch(actions.unpause());
|
||||||
this.panelCtrl.refresh();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getCollapsedText() {
|
getCollapsedText() {
|
||||||
return this.target.target;
|
// WIP: removed. It was not used.
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleTagsAutoCompleteError(error: Error): void {
|
handleTagsAutoCompleteError(error: Error): void {
|
||||||
console.error(error);
|
// WIP: moved to state/helpers (the same name)
|
||||||
if (!this._tagsAutoCompleteErrorShown) {
|
|
||||||
this._tagsAutoCompleteErrorShown = true;
|
|
||||||
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleMetricsAutoCompleteError(error: Error): void {
|
handleMetricsAutoCompleteError(error: Error): void {
|
||||||
console.error(error);
|
// WIP: moved to state/helpers (the same name)
|
||||||
if (!this._metricAutoCompleteErrorShown) {
|
|
||||||
this._metricAutoCompleteErrorShown = true;
|
|
||||||
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function mapToDropdownOptions(results: any[]) {
|
// WIP: moved to state/providers (the same names)
|
||||||
return map(results, (value) => {
|
// function mapToDropdownOptions(results: any[]) {}
|
||||||
return { text: value, value: value };
|
// function removeTagPrefix(value: string): string {}
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function removeTagPrefix(value: string): string {
|
|
||||||
return value.replace(TAG_PREFIX, '');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -16,6 +16,15 @@ jest.mock('app/store/store', () => ({
|
|||||||
}));
|
}));
|
||||||
const mockDispatch = dispatch as jest.Mock;
|
const mockDispatch = dispatch as jest.Mock;
|
||||||
|
|
||||||
|
async function changeTarget(ctx: any, target: string, refId?: string): Promise<void> {
|
||||||
|
await ctx.ctrl.toggleEditorMode();
|
||||||
|
ctx.ctrl.state.target.target = target;
|
||||||
|
if (refId) {
|
||||||
|
ctx.ctrl.state.target.refId = refId;
|
||||||
|
}
|
||||||
|
await ctx.ctrl.toggleEditorMode();
|
||||||
|
}
|
||||||
|
|
||||||
describe('GraphiteQueryCtrl', () => {
|
describe('GraphiteQueryCtrl', () => {
|
||||||
const ctx = {
|
const ctx = {
|
||||||
datasource: {
|
datasource: {
|
||||||
@@ -36,21 +45,23 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
targets: [ctx.target],
|
targets: [ctx.target],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
GraphiteQueryCtrl.prototype.target = ctx.target;
|
GraphiteQueryCtrl.prototype.target = ctx.target;
|
||||||
GraphiteQueryCtrl.prototype.datasource = ctx.datasource;
|
GraphiteQueryCtrl.prototype.datasource = ctx.datasource;
|
||||||
GraphiteQueryCtrl.prototype.panelCtrl = ctx.panelCtrl;
|
GraphiteQueryCtrl.prototype.panelCtrl = ctx.panelCtrl;
|
||||||
|
|
||||||
ctx.ctrl = new GraphiteQueryCtrl(
|
ctx.ctrl = new GraphiteQueryCtrl(
|
||||||
{},
|
{ $digest: jest.fn() },
|
||||||
{} as any,
|
{} as any,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
new uiSegmentSrv({ trustAsHtml: (html) => html }, { highlightVariablesAsHtml: () => {} }),
|
new uiSegmentSrv({ trustAsHtml: (html) => html }, { highlightVariablesAsHtml: () => {} }),
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
new TemplateSrvStub(),
|
new TemplateSrvStub()
|
||||||
{}
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// resolve async code called by the constructor
|
||||||
|
await Promise.resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('init', () => {
|
describe('init', () => {
|
||||||
@@ -59,19 +70,19 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not delete last segment if no metrics are found', () => {
|
it('should not delete last segment if no metrics are found', () => {
|
||||||
expect(ctx.ctrl.segments[2].value).not.toBe('select metric');
|
expect(ctx.ctrl.state.segments[2].value).not.toBe('select metric');
|
||||||
expect(ctx.ctrl.segments[2].value).toBe('*');
|
expect(ctx.ctrl.state.segments[2].value).toBe('*');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse expression and build function model', () => {
|
it('should parse expression and build function model', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when toggling edit mode to raw and back again', () => {
|
describe('when toggling edit mode to raw and back again', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.toggleEditorMode();
|
await ctx.ctrl.toggleEditorMode();
|
||||||
ctx.ctrl.toggleEditorMode();
|
await ctx.ctrl.toggleEditorMode();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate metric key exists', () => {
|
it('should validate metric key exists', () => {
|
||||||
@@ -80,20 +91,20 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete last segment if no metrics are found', () => {
|
it('should delete last segment if no metrics are found', () => {
|
||||||
expect(ctx.ctrl.segments[0].value).toBe('test');
|
expect(ctx.ctrl.state.segments[0].value).toBe('test');
|
||||||
expect(ctx.ctrl.segments[1].value).toBe('prod');
|
expect(ctx.ctrl.state.segments[1].value).toBe('prod');
|
||||||
expect(ctx.ctrl.segments[2].value).toBe('select metric');
|
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse expression and build function model', () => {
|
it('should parse expression and build function model', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when middle segment value of test.prod.* is changed', () => {
|
describe('when middle segment value of test.prod.* is changed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
const segment = { type: 'segment', value: 'test', expandable: true };
|
const segment = { type: 'segment', value: 'test', expandable: true };
|
||||||
ctx.ctrl.segmentValueChanged(segment, 1);
|
await ctx.ctrl.segmentValueChanged(segment, 1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should validate metric key exists', () => {
|
it('should validate metric key exists', () => {
|
||||||
@@ -102,30 +113,29 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should delete last segment if no metrics are found', () => {
|
it('should delete last segment if no metrics are found', () => {
|
||||||
expect(ctx.ctrl.segments[0].value).toBe('test');
|
expect(ctx.ctrl.state.segments[0].value).toBe('test');
|
||||||
expect(ctx.ctrl.segments[1].value).toBe('test');
|
expect(ctx.ctrl.state.segments[1].value).toBe('test');
|
||||||
expect(ctx.ctrl.segments[2].value).toBe('select metric');
|
expect(ctx.ctrl.state.segments[2].value).toBe('select metric');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse expression and build function model', () => {
|
it('should parse expression and build function model', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions.length).toBe(2);
|
expect(ctx.ctrl.state.queryModel.functions.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when adding function', () => {
|
describe('when adding function', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'test.prod.*.count';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, 'test.prod.*.count');
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
|
||||||
ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add function with correct node number', () => {
|
it('should add function with correct node number', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(2);
|
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update target', () => {
|
it('should update target', () => {
|
||||||
expect(ctx.ctrl.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
|
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(test.prod.*.count, 2)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh', () => {
|
it('should call refresh', () => {
|
||||||
@@ -134,58 +144,52 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when adding function before any metric segment', () => {
|
describe('when adding function before any metric segment', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = '';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
|
await changeTarget(ctx, '');
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
|
||||||
ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add function and remove select metric link', () => {
|
it('should add function and remove select metric link', () => {
|
||||||
expect(ctx.ctrl.segments.length).toBe(0);
|
expect(ctx.ctrl.state.segments.length).toBe(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when initializing a target with single param func using variable', () => {
|
describe('when initializing a target with single param func using variable', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'movingAverage(prod.count, $var)';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
await changeTarget(ctx, 'movingAverage(prod.count, $var)');
|
||||||
ctx.ctrl.parseTarget();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add 2 segments', () => {
|
it('should add 2 segments', () => {
|
||||||
expect(ctx.ctrl.segments.length).toBe(2);
|
expect(ctx.ctrl.state.segments.length).toBe(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add function param', () => {
|
it('should add function param', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when initializing target without metric expression and function with series-ref', () => {
|
describe('when initializing target without metric expression and function with series-ref', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'asPercent(metric.node.count, #A)';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
await changeTarget(ctx, 'asPercent(metric.node.count, #A)');
|
||||||
ctx.ctrl.parseTarget();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add segments', () => {
|
it('should add segments', () => {
|
||||||
expect(ctx.ctrl.segments.length).toBe(3);
|
expect(ctx.ctrl.state.segments.length).toBe(3);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct func params', () => {
|
it('should have correct func params', () => {
|
||||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getting altSegments and metricFindQuery returns empty array', () => {
|
describe('when getting altSegments and metricFindQuery returns empty array', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'test.count';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([]);
|
await changeTarget(ctx, 'test.count');
|
||||||
ctx.ctrl.parseTarget();
|
ctx.altSegments = await ctx.ctrl.getAltSegments(1, '');
|
||||||
ctx.ctrl.getAltSegments(1).then((results: any) => {
|
|
||||||
ctx.altSegments = results;
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have no segments', () => {
|
it('should have no segments', () => {
|
||||||
@@ -196,8 +200,8 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
describe('when autocomplete for metric names is not available', () => {
|
describe('when autocomplete for metric names is not available', () => {
|
||||||
silenceConsoleOutput();
|
silenceConsoleOutput();
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctx.ctrl.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
ctx.ctrl.state.datasource.getTagsAutoComplete = jest.fn().mockReturnValue(Promise.resolve([]));
|
||||||
ctx.ctrl.datasource.metricFindQuery = jest.fn().mockReturnValue(
|
ctx.ctrl.state.datasource.metricFindQuery = jest.fn().mockReturnValue(
|
||||||
new Promise(() => {
|
new Promise(() => {
|
||||||
throw new Error();
|
throw new Error();
|
||||||
})
|
})
|
||||||
@@ -222,25 +226,6 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
await ctx.ctrl.getAltSegments(0, 'any');
|
await ctx.ctrl.getAltSegments(0, 'any');
|
||||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('checkOtherSegments should handle autocomplete errors', async () => {
|
|
||||||
await expect(async () => {
|
|
||||||
await ctx.ctrl.checkOtherSegments(1, false);
|
|
||||||
expect(mockDispatch).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'appNotifications/notifyApp',
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}).not.toThrow();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('checkOtherSegments should display the error message only once', async () => {
|
|
||||||
await ctx.ctrl.checkOtherSegments(1, false);
|
|
||||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
|
||||||
|
|
||||||
await ctx.ctrl.checkOtherSegments(1, false);
|
|
||||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when autocomplete for tags is not available', () => {
|
describe('when autocomplete for tags is not available', () => {
|
||||||
@@ -294,16 +279,17 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('targetChanged', () => {
|
describe('targetChanged', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
|
const newQuery = 'aliasByNode(scaleToSeconds(test.prod.*, 1), 2)';
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.parseTarget();
|
await changeTarget(ctx, newQuery);
|
||||||
ctx.ctrl.target.target = '';
|
await ctx.ctrl.targetTextChanged({
|
||||||
ctx.ctrl.targetChanged();
|
target: { value: newQuery },
|
||||||
|
} as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should rebuild target after expression model', () => {
|
it('should rebuild target after expression model', () => {
|
||||||
expect(ctx.ctrl.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
|
expect(ctx.ctrl.state.target.target).toBe('aliasByNode(scaleToSeconds(test.prod.*, 1), 2)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call panelCtrl.refresh', () => {
|
it('should call panelCtrl.refresh', () => {
|
||||||
@@ -312,75 +298,71 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when updating targets with nested query', () => {
|
describe('when updating targets with nested query', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'scaleToSeconds(#A, 60)';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, 'scaleToSeconds(#A, 60)');
|
||||||
ctx.ctrl.parseTarget();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add function params', () => {
|
it('should add function params', () => {
|
||||||
expect(ctx.ctrl.queryModel.segments.length).toBe(1);
|
expect(ctx.ctrl.state.queryModel.segments.length).toBe(1);
|
||||||
expect(ctx.ctrl.queryModel.segments[0].value).toBe('#A');
|
expect(ctx.ctrl.state.queryModel.segments[0].value).toBe('#A');
|
||||||
|
|
||||||
expect(ctx.ctrl.queryModel.functions[0].params.length).toBe(1);
|
expect(ctx.ctrl.state.queryModel.functions[0].params.length).toBe(1);
|
||||||
expect(ctx.ctrl.queryModel.functions[0].params[0]).toBe(60);
|
expect(ctx.ctrl.state.queryModel.functions[0].params[0]).toBe(60);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('target should remain the same', () => {
|
it('target should remain the same', () => {
|
||||||
expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
|
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('targetFull should include nested queries', () => {
|
it('targetFull should include nested queries', async () => {
|
||||||
ctx.ctrl.panelCtrl.panel.targets = [
|
ctx.ctrl.state.panelCtrl.panel.targets = [
|
||||||
{
|
{
|
||||||
target: 'nested.query.count',
|
target: 'nested.query.count',
|
||||||
refId: 'A',
|
refId: 'A',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
ctx.ctrl.updateModelTarget();
|
await ctx.ctrl.targetTextChanged({ target: { value: 'nested.query.count' } } as any);
|
||||||
|
|
||||||
expect(ctx.ctrl.target.target).toBe('scaleToSeconds(#A, 60)');
|
expect(ctx.ctrl.state.target.target).toBe('scaleToSeconds(#A, 60)');
|
||||||
|
|
||||||
expect(ctx.ctrl.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
|
expect(ctx.ctrl.state.target.targetFull).toBe('scaleToSeconds(nested.query.count, 60)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when updating target used in other query', () => {
|
describe('when updating target used in other query', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'metrics.a.count';
|
|
||||||
ctx.ctrl.target.refId = 'A';
|
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.parseTarget();
|
await changeTarget(ctx, 'metrics.a.count', 'A');
|
||||||
|
|
||||||
ctx.ctrl.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
|
ctx.ctrl.state.panelCtrl.panel.targets = [ctx.ctrl.target, { target: 'sumSeries(#A)', refId: 'B' }];
|
||||||
|
|
||||||
ctx.ctrl.updateModelTarget();
|
await ctx.ctrl.targetTextChanged({ target: { value: 'metrics.a.count' } } as any);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('targetFull of other query should update', () => {
|
it('targetFull of other query should update', () => {
|
||||||
expect(ctx.ctrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.a.count)');
|
expect(ctx.ctrl.state.panelCtrl.panel.targets[1].targetFull).toBe('sumSeries(metrics.a.count)');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when adding seriesByTag function', () => {
|
describe('when adding seriesByTag function', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = '';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, '');
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
|
||||||
ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update functions', () => {
|
it('should update functions', () => {
|
||||||
expect(ctx.ctrl.queryModel.getSeriesByTagFuncIndex()).toBe(0);
|
expect(ctx.ctrl.state.queryModel.getSeriesByTagFuncIndex()).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update seriesByTagUsed flag', () => {
|
it('should update seriesByTagUsed flag', () => {
|
||||||
expect(ctx.ctrl.queryModel.seriesByTagUsed).toBe(true);
|
expect(ctx.ctrl.state.queryModel.seriesByTagUsed).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update target', () => {
|
it('should update target', () => {
|
||||||
expect(ctx.ctrl.target.target).toBe('seriesByTag()');
|
expect(ctx.ctrl.state.target.target).toBe('seriesByTag()');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call refresh', () => {
|
it('should call refresh', () => {
|
||||||
@@ -389,10 +371,9 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when parsing seriesByTag function', () => {
|
describe('when parsing seriesByTag function', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||||
ctx.ctrl.parseTarget();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add tags', () => {
|
it('should add tags', () => {
|
||||||
@@ -400,39 +381,37 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
{ key: 'tag1', operator: '=', value: 'value1' },
|
{ key: 'tag1', operator: '=', value: 'value1' },
|
||||||
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
||||||
];
|
];
|
||||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add plus button', () => {
|
it('should add plus button', () => {
|
||||||
expect(ctx.ctrl.addTagSegments.length).toBe(1);
|
expect(ctx.ctrl.state.addTagSegments.length).toBe(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when tag added', () => {
|
describe('when tag added', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = 'seriesByTag()';
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, 'seriesByTag()');
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.addNewTag({ value: 'tag1' });
|
||||||
ctx.ctrl.addNewTag({ value: 'tag1' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update tags with default value', () => {
|
it('should update tags with default value', () => {
|
||||||
const expected = [{ key: 'tag1', operator: '=', value: '' }];
|
const expected = [{ key: 'tag1', operator: '=', value: '' }];
|
||||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update target', () => {
|
it('should update target', () => {
|
||||||
const expected = "seriesByTag('tag1=')";
|
const expected = "seriesByTag('tag1=')";
|
||||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when tag changed', () => {
|
describe('when tag changed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
|
||||||
ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update tags', () => {
|
it('should update tags', () => {
|
||||||
@@ -440,31 +419,30 @@ describe('GraphiteQueryCtrl', () => {
|
|||||||
{ key: 'tag1', operator: '=', value: 'new_value' },
|
{ key: 'tag1', operator: '=', value: 'new_value' },
|
||||||
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
{ key: 'tag2', operator: '!=~', value: 'value2' },
|
||||||
];
|
];
|
||||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update target', () => {
|
it('should update target', () => {
|
||||||
const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
|
const expected = "seriesByTag('tag1=new_value', 'tag2!=~value2')";
|
||||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when tag removed', () => {
|
describe('when tag removed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(async () => {
|
||||||
ctx.ctrl.target.target = "seriesByTag('tag1=value1', 'tag2!=~value2')";
|
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||||
ctx.ctrl.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||||
ctx.ctrl.parseTarget();
|
await ctx.ctrl.tagChanged({ key: ctx.ctrl.state.removeTagValue });
|
||||||
ctx.ctrl.tagChanged({ key: ctx.ctrl.removeTagValue });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update tags', () => {
|
it('should update tags', () => {
|
||||||
const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
|
const expected = [{ key: 'tag2', operator: '!=~', value: 'value2' }];
|
||||||
expect(ctx.ctrl.queryModel.tags).toEqual(expected);
|
expect(ctx.ctrl.state.queryModel.tags).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update target', () => {
|
it('should update target', () => {
|
||||||
const expected = "seriesByTag('tag2!=~value2')";
|
const expected = "seriesByTag('tag2!=~value2')";
|
||||||
expect(ctx.ctrl.target.target).toEqual(expected);
|
expect(ctx.ctrl.state.target.target).toEqual(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { GraphiteQueryEditorAngularDependencies, GraphiteSegment, GraphiteTag } from '../types';
|
||||||
|
import { createAction } from '@reduxjs/toolkit';
|
||||||
|
import { FuncInstance } from '../gfunc';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of possible actions changing the state of QueryEditor
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is used only during the transition to react. It will be removed after migrating all components.
|
||||||
|
*/
|
||||||
|
const init = createAction<GraphiteQueryEditorAngularDependencies>('init');
|
||||||
|
|
||||||
|
// Metrics & Tags
|
||||||
|
const segmentValueChanged = createAction<{ segment: GraphiteSegment; index: number }>('segment-value-changed');
|
||||||
|
|
||||||
|
// Tags
|
||||||
|
const addNewTag = createAction<{ segment: GraphiteSegment }>('add-new-tag');
|
||||||
|
const tagChanged = createAction<{ tag: GraphiteTag; index: number }>('tag-changed');
|
||||||
|
const unpause = createAction('unpause');
|
||||||
|
|
||||||
|
// Functions
|
||||||
|
const addFunction = createAction<{ name: string }>('add-function');
|
||||||
|
const removeFunction = createAction<{ func: FuncInstance }>('remove-function');
|
||||||
|
const moveFunction = createAction<{ func: FuncInstance; offset: number }>('move-function');
|
||||||
|
// TODO: at the moment parameters are modified directly, new value of the param will be passed in the action
|
||||||
|
const updateFunctionParam = createAction<{ func: FuncInstance }>('update-function-param');
|
||||||
|
|
||||||
|
// Text editor
|
||||||
|
const updateQuery = createAction<{ query: string }>('update-query');
|
||||||
|
const toggleEditorMode = createAction('toggle-editor');
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
init,
|
||||||
|
segmentValueChanged,
|
||||||
|
tagChanged,
|
||||||
|
addNewTag,
|
||||||
|
unpause,
|
||||||
|
addFunction,
|
||||||
|
removeFunction,
|
||||||
|
moveFunction,
|
||||||
|
updateFunctionParam,
|
||||||
|
updateQuery,
|
||||||
|
toggleEditorMode,
|
||||||
|
};
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
import { GraphiteQueryEditorState } from './store';
|
||||||
|
import { each, map } from 'lodash';
|
||||||
|
import { dispatch } from '../../../../store/store';
|
||||||
|
import { notifyApp } from '../../../../core/reducers/appNotification';
|
||||||
|
import { createErrorNotification } from '../../../../core/copy/appNotification';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helpers used by reducers and providers. They modify state object directly so should operate on a copy of the state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tag names and metric names are displayed in a single dropdown. This prefix is used to
|
||||||
|
* distinguish both in the UI.
|
||||||
|
*/
|
||||||
|
export const TAG_PREFIX = 'tag: ';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new AST based on new query.
|
||||||
|
* Build segments from parsed metric name and functions.
|
||||||
|
*/
|
||||||
|
export async function parseTarget(state: GraphiteQueryEditorState): Promise<void> {
|
||||||
|
state.queryModel.parseTarget();
|
||||||
|
await buildSegments(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create segments out of the current metric path + add "select metrics" if it's possible to add more to the path
|
||||||
|
*/
|
||||||
|
export async function buildSegments(state: GraphiteQueryEditorState, modifyLastSegment = true): Promise<void> {
|
||||||
|
state.segments = map(state.queryModel.segments, (segment) => {
|
||||||
|
return state.uiSegmentSrv.newSegment(segment);
|
||||||
|
});
|
||||||
|
|
||||||
|
const checkOtherSegmentsIndex = state.queryModel.checkOtherSegmentsIndex || 0;
|
||||||
|
|
||||||
|
await checkOtherSegments(state, checkOtherSegmentsIndex, modifyLastSegment);
|
||||||
|
|
||||||
|
if (state.queryModel.seriesByTagUsed) {
|
||||||
|
fixTagSegments(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add "select metric" segment at the end
|
||||||
|
*/
|
||||||
|
export function addSelectMetricSegment(state: GraphiteQueryEditorState): void {
|
||||||
|
state.queryModel.addSelectMetricSegment();
|
||||||
|
state.segments.push(state.uiSegmentSrv.newSelectMetric());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the state after adding or changing a segment:
|
||||||
|
* - adds "select metric" only when more segments can be added to the metric name
|
||||||
|
* - check if subsequent segments are still valid if in-between segment changes and
|
||||||
|
* removes invalid segments.
|
||||||
|
*/
|
||||||
|
export async function checkOtherSegments(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
fromIndex: number,
|
||||||
|
modifyLastSegment = true
|
||||||
|
): Promise<void> {
|
||||||
|
if (state.queryModel.segments.length === 1 && state.queryModel.segments[0].type === 'series-ref') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fromIndex === 0) {
|
||||||
|
addSelectMetricSegment(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = state.queryModel.getSegmentPathUpTo(fromIndex + 1);
|
||||||
|
if (path === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const segments = await state.datasource.metricFindQuery(path);
|
||||||
|
if (segments.length === 0) {
|
||||||
|
if (path !== '' && modifyLastSegment) {
|
||||||
|
state.queryModel.segments = state.queryModel.segments.splice(0, fromIndex);
|
||||||
|
state.segments = state.segments.splice(0, fromIndex);
|
||||||
|
addSelectMetricSegment(state);
|
||||||
|
}
|
||||||
|
} else if (segments[0].expandable) {
|
||||||
|
if (state.segments.length === fromIndex) {
|
||||||
|
addSelectMetricSegment(state);
|
||||||
|
} else {
|
||||||
|
await checkOtherSegments(state, fromIndex + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleMetricsAutoCompleteError(state, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes segment being in focus. After changing the value, next segment gets focus.
|
||||||
|
*
|
||||||
|
* Note: It's a bit hidden feature. After selecting one metric, and pressing down arrow the dropdown can be expanded.
|
||||||
|
* But there's nothing indicating what's in focus and how to expand the dropdown.
|
||||||
|
*/
|
||||||
|
export function setSegmentFocus(state: GraphiteQueryEditorState, segmentIndex: number): void {
|
||||||
|
each(state.segments, (segment, index) => {
|
||||||
|
segment.focus = segmentIndex === index;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function spliceSegments(state: GraphiteQueryEditorState, index: number): void {
|
||||||
|
state.segments = state.segments.splice(0, index);
|
||||||
|
state.queryModel.segments = state.queryModel.segments.splice(0, index);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function emptySegments(state: GraphiteQueryEditorState): void {
|
||||||
|
state.queryModel.segments = [];
|
||||||
|
state.segments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When seriesByTag function is added the UI changes it's state and only tags can be added from now.
|
||||||
|
*/
|
||||||
|
export async function addSeriesByTagFunc(state: GraphiteQueryEditorState, tag: string): Promise<void> {
|
||||||
|
const newFunc = state.datasource.createFuncInstance('seriesByTag', {
|
||||||
|
withDefaultParams: false,
|
||||||
|
});
|
||||||
|
const tagParam = `${tag}=`;
|
||||||
|
newFunc.params = [tagParam];
|
||||||
|
state.queryModel.addFunction(newFunc);
|
||||||
|
newFunc.added = true;
|
||||||
|
|
||||||
|
emptySegments(state);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
await parseTarget(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function smartlyHandleNewAliasByNode(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
func: { def: { name: string }; params: number[]; added: boolean }
|
||||||
|
): void {
|
||||||
|
if (func.def.name !== 'aliasByNode') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < state.segments.length; i++) {
|
||||||
|
if (state.segments[i].value.indexOf('*') >= 0) {
|
||||||
|
func.params[0] = i;
|
||||||
|
func.added = false;
|
||||||
|
handleTargetChanged(state);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add "+" button for adding tags once at least one tag is selected
|
||||||
|
*/
|
||||||
|
export function fixTagSegments(state: GraphiteQueryEditorState): void {
|
||||||
|
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
|
||||||
|
state.addTagSegments = [state.uiSegmentSrv.newPlusButton()];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pauses running the query to allow selecting tag value. This is to prevent getting errors if the query is run
|
||||||
|
* for a tag with no selected value.
|
||||||
|
*/
|
||||||
|
export function pause(state: GraphiteQueryEditorState): void {
|
||||||
|
state.paused = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeTagPrefix(value: string): string {
|
||||||
|
return value.replace(TAG_PREFIX, '');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleTargetChanged(state: GraphiteQueryEditorState): void {
|
||||||
|
if (state.queryModel.error) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldTarget = state.queryModel.target.target;
|
||||||
|
state.queryModel.updateModelTarget(state.panelCtrl.panel.targets);
|
||||||
|
|
||||||
|
if (state.queryModel.target.target !== oldTarget && !state.paused) {
|
||||||
|
state.panelCtrl.refresh();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When metrics autocomplete fails - the error is shown, but only once per page view
|
||||||
|
*/
|
||||||
|
export function handleMetricsAutoCompleteError(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
error: Error
|
||||||
|
): GraphiteQueryEditorState {
|
||||||
|
console.error(error);
|
||||||
|
if (!state.metricAutoCompleteErrorShown) {
|
||||||
|
state.metricAutoCompleteErrorShown = true;
|
||||||
|
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When tags autocomplete fails - the error is shown, but only once per page view
|
||||||
|
*/
|
||||||
|
export function handleTagsAutoCompleteError(state: GraphiteQueryEditorState, error: Error): GraphiteQueryEditorState {
|
||||||
|
console.error(error);
|
||||||
|
if (!state.tagsAutoCompleteErrorShown) {
|
||||||
|
state.tagsAutoCompleteErrorShown = true;
|
||||||
|
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import { GraphiteQueryEditorState } from './store';
|
||||||
|
import { eachRight, map, remove } from 'lodash';
|
||||||
|
import {
|
||||||
|
TAG_PREFIX,
|
||||||
|
GRAPHITE_TAG_OPERATORS,
|
||||||
|
handleMetricsAutoCompleteError,
|
||||||
|
handleTagsAutoCompleteError,
|
||||||
|
} from './helpers';
|
||||||
|
import { AngularDropdownOptions, GraphiteSegment, GraphiteTag } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Providers are hooks for views to provide temporal data for autocomplete. They don't modify the state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of available options for a segment with given index
|
||||||
|
*
|
||||||
|
* It may be:
|
||||||
|
* - mixed list of metrics and tags (only when nothing was selected)
|
||||||
|
* - list of metric names (if a metric name was selected for this segment)
|
||||||
|
*/
|
||||||
|
export async function getAltSegments(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
index: number,
|
||||||
|
prefix: string
|
||||||
|
): Promise<GraphiteSegment[]> {
|
||||||
|
let query = prefix.length > 0 ? '*' + prefix + '*' : '*';
|
||||||
|
if (index > 0) {
|
||||||
|
query = state.queryModel.getSegmentPathUpTo(index) + '.' + query;
|
||||||
|
}
|
||||||
|
const options = {
|
||||||
|
range: state.panelCtrl.range,
|
||||||
|
requestId: 'get-alt-segments',
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const segments = await state.datasource.metricFindQuery(query, options);
|
||||||
|
const altSegments = map(segments, (segment) => {
|
||||||
|
return state.uiSegmentSrv.newSegment({
|
||||||
|
value: segment.text,
|
||||||
|
expandable: segment.expandable,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (index > 0 && altSegments.length === 0) {
|
||||||
|
return altSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
// add query references
|
||||||
|
if (index === 0) {
|
||||||
|
eachRight(state.panelCtrl.panel.targets, (target) => {
|
||||||
|
if (target.refId === state.queryModel.target.refId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
altSegments.unshift(
|
||||||
|
state.uiSegmentSrv.newSegment({
|
||||||
|
type: 'series-ref',
|
||||||
|
value: '#' + target.refId,
|
||||||
|
expandable: false,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// add template variables
|
||||||
|
eachRight(state.templateSrv.getVariables(), (variable) => {
|
||||||
|
altSegments.unshift(
|
||||||
|
state.uiSegmentSrv.newSegment({
|
||||||
|
type: 'template',
|
||||||
|
value: '$' + variable.name,
|
||||||
|
expandable: true,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// add wildcard option
|
||||||
|
altSegments.unshift(state.uiSegmentSrv.newSegment('*'));
|
||||||
|
|
||||||
|
if (state.supportsTags && index === 0) {
|
||||||
|
removeTaggedEntry(altSegments);
|
||||||
|
return await addAltTagSegments(state, prefix, altSegments);
|
||||||
|
} else {
|
||||||
|
return altSegments;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
handleMetricsAutoCompleteError(state, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTagOperators(): AngularDropdownOptions[] {
|
||||||
|
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns tags as dropdown options
|
||||||
|
*/
|
||||||
|
export async function getTags(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
index: number,
|
||||||
|
tagPrefix: string
|
||||||
|
): Promise<AngularDropdownOptions[]> {
|
||||||
|
try {
|
||||||
|
const tagExpressions = state.queryModel.renderTagExpressions(index);
|
||||||
|
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
|
||||||
|
|
||||||
|
const altTags = map(values, 'text');
|
||||||
|
altTags.splice(0, 0, state.removeTagValue);
|
||||||
|
return mapToDropdownOptions(altTags);
|
||||||
|
} catch (err) {
|
||||||
|
handleTagsAutoCompleteError(state, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of tags when a tag is added. getTags is used for editing.
|
||||||
|
* When adding - segment is used. When editing - dropdown is used.
|
||||||
|
*/
|
||||||
|
export async function getTagsAsSegments(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
tagPrefix: string
|
||||||
|
): Promise<GraphiteSegment[]> {
|
||||||
|
let tagsAsSegments: GraphiteSegment[] = [];
|
||||||
|
try {
|
||||||
|
const tagExpressions = state.queryModel.renderTagExpressions();
|
||||||
|
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
|
||||||
|
tagsAsSegments = map(values, (val) => {
|
||||||
|
return state.uiSegmentSrv.newSegment({
|
||||||
|
value: val.text,
|
||||||
|
type: 'tag',
|
||||||
|
expandable: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
tagsAsSegments = [];
|
||||||
|
handleTagsAutoCompleteError(state, err);
|
||||||
|
}
|
||||||
|
|
||||||
|
return tagsAsSegments;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getTagValues(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
tag: GraphiteTag,
|
||||||
|
index: number,
|
||||||
|
valuePrefix: string
|
||||||
|
): Promise<AngularDropdownOptions[]> {
|
||||||
|
const tagExpressions = state.queryModel.renderTagExpressions(index);
|
||||||
|
const tagKey = tag.key;
|
||||||
|
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {});
|
||||||
|
const altValues = map(values, 'text');
|
||||||
|
// Add template variables as additional values
|
||||||
|
eachRight(state.templateSrv.getVariables(), (variable) => {
|
||||||
|
altValues.push('${' + variable.name + ':regex}');
|
||||||
|
});
|
||||||
|
|
||||||
|
return mapToDropdownOptions(altValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add segments with tags prefixed with "tag: " to include them in the same list as metrics
|
||||||
|
*/
|
||||||
|
async function addAltTagSegments(
|
||||||
|
state: GraphiteQueryEditorState,
|
||||||
|
prefix: string,
|
||||||
|
altSegments: GraphiteSegment[]
|
||||||
|
): Promise<GraphiteSegment[]> {
|
||||||
|
let tagSegments = await getTagsAsSegments(state, prefix);
|
||||||
|
|
||||||
|
tagSegments = map(tagSegments, (segment) => {
|
||||||
|
segment.value = TAG_PREFIX + segment.value;
|
||||||
|
return segment;
|
||||||
|
});
|
||||||
|
|
||||||
|
return altSegments.concat(...tagSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeTaggedEntry(altSegments: GraphiteSegment[]) {
|
||||||
|
remove(altSegments, (s) => s.value === '_tagged');
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapToDropdownOptions(results: string[]) {
|
||||||
|
return map(results, (value) => {
|
||||||
|
return { text: value, value: value };
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import GraphiteQuery from '../graphite_query';
|
||||||
|
import { GraphiteActionDispatcher, GraphiteSegment, GraphiteTagOperator } from '../types';
|
||||||
|
import { GraphiteDatasource } from '../datasource';
|
||||||
|
import { TemplateSrv } from '../../../../features/templating/template_srv';
|
||||||
|
import { actions } from './actions';
|
||||||
|
import { getTemplateSrv } from '@grafana/runtime';
|
||||||
|
import {
|
||||||
|
addSeriesByTagFunc,
|
||||||
|
buildSegments,
|
||||||
|
checkOtherSegments,
|
||||||
|
emptySegments,
|
||||||
|
fixTagSegments,
|
||||||
|
handleTargetChanged,
|
||||||
|
parseTarget,
|
||||||
|
pause,
|
||||||
|
removeTagPrefix,
|
||||||
|
setSegmentFocus,
|
||||||
|
smartlyHandleNewAliasByNode,
|
||||||
|
spliceSegments,
|
||||||
|
} from './helpers';
|
||||||
|
import { Action } from 'redux';
|
||||||
|
|
||||||
|
export type GraphiteQueryEditorState = {
|
||||||
|
/**
|
||||||
|
* Extra segment with plus button when tags are rendered
|
||||||
|
*/
|
||||||
|
addTagSegments: GraphiteSegment[];
|
||||||
|
|
||||||
|
supportsTags: boolean;
|
||||||
|
paused: boolean;
|
||||||
|
removeTagValue: string;
|
||||||
|
|
||||||
|
datasource: GraphiteDatasource;
|
||||||
|
|
||||||
|
uiSegmentSrv: any;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
panelCtrl: any;
|
||||||
|
|
||||||
|
target: { target: string; textEditor: boolean };
|
||||||
|
|
||||||
|
segments: GraphiteSegment[];
|
||||||
|
queryModel: GraphiteQuery;
|
||||||
|
|
||||||
|
error: Error | null;
|
||||||
|
|
||||||
|
tagsAutoCompleteErrorShown: boolean;
|
||||||
|
metricAutoCompleteErrorShown: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise<GraphiteQueryEditorState> => {
|
||||||
|
state = { ...state };
|
||||||
|
|
||||||
|
if (actions.init.match(action)) {
|
||||||
|
const deps = action.payload;
|
||||||
|
deps.target.target = deps.target.target || '';
|
||||||
|
|
||||||
|
await deps.datasource.waitForFuncDefsLoaded();
|
||||||
|
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
...deps,
|
||||||
|
queryModel: new GraphiteQuery(deps.datasource, deps.target, getTemplateSrv()),
|
||||||
|
supportsTags: deps.datasource.supportsTags,
|
||||||
|
paused: false,
|
||||||
|
removeTagValue: '-- remove tag --',
|
||||||
|
};
|
||||||
|
|
||||||
|
await buildSegments(state, false);
|
||||||
|
}
|
||||||
|
if (actions.segmentValueChanged.match(action)) {
|
||||||
|
const { segment, index: segmentIndex } = action.payload;
|
||||||
|
|
||||||
|
state.error = null;
|
||||||
|
state.queryModel.updateSegmentValue(segment, segmentIndex);
|
||||||
|
|
||||||
|
if (state.queryModel.functions.length > 0 && state.queryModel.functions[0].def.fake) {
|
||||||
|
state.queryModel.functions = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.type === 'tag') {
|
||||||
|
const tag = removeTagPrefix(segment.value);
|
||||||
|
pause(state);
|
||||||
|
await addSeriesByTagFunc(state, tag);
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (segment.expandable) {
|
||||||
|
await checkOtherSegments(state, segmentIndex + 1);
|
||||||
|
setSegmentFocus(state, segmentIndex + 1);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
} else {
|
||||||
|
spliceSegments(state, segmentIndex + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
setSegmentFocus(state, segmentIndex + 1);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
if (actions.tagChanged.match(action)) {
|
||||||
|
const { tag, index: tagIndex } = action.payload;
|
||||||
|
state.queryModel.updateTag(tag, tagIndex);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
if (actions.addNewTag.match(action)) {
|
||||||
|
const segment = action.payload.segment;
|
||||||
|
const newTagKey = segment.value;
|
||||||
|
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
|
||||||
|
state.queryModel.addTag(newTag);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
fixTagSegments(state);
|
||||||
|
}
|
||||||
|
if (actions.unpause.match(action)) {
|
||||||
|
state.paused = false;
|
||||||
|
state.panelCtrl.refresh();
|
||||||
|
}
|
||||||
|
if (actions.addFunction.match(action)) {
|
||||||
|
const newFunc = state.datasource.createFuncInstance(action.payload.name, {
|
||||||
|
withDefaultParams: true,
|
||||||
|
});
|
||||||
|
newFunc.added = true;
|
||||||
|
state.queryModel.addFunction(newFunc);
|
||||||
|
smartlyHandleNewAliasByNode(state, newFunc);
|
||||||
|
|
||||||
|
if (state.segments.length === 1 && state.segments[0].fake) {
|
||||||
|
emptySegments(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!newFunc.params.length && newFunc.added) {
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newFunc.def.name === 'seriesByTag') {
|
||||||
|
await parseTarget(state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (actions.removeFunction.match(action)) {
|
||||||
|
state.queryModel.removeFunction(action.payload.func);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
if (actions.moveFunction.match(action)) {
|
||||||
|
const { func, offset } = action.payload;
|
||||||
|
state.queryModel.moveFunction(func, offset);
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
if (actions.updateFunctionParam.match(action)) {
|
||||||
|
handleTargetChanged(state);
|
||||||
|
}
|
||||||
|
if (actions.updateQuery.match(action)) {
|
||||||
|
state.target.target = action.payload.query;
|
||||||
|
handleTargetChanged(state);
|
||||||
|
// handleTargetChanged() builds target from segments/tags/functions only,
|
||||||
|
// it doesn't handle refresh when target is change explicitly
|
||||||
|
state.panelCtrl.refresh();
|
||||||
|
}
|
||||||
|
if (actions.toggleEditorMode.match(action)) {
|
||||||
|
state.target.textEditor = !state.target.textEditor;
|
||||||
|
await parseTarget(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ...state };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createStore = (
|
||||||
|
onChange: (state: GraphiteQueryEditorState) => void
|
||||||
|
): [GraphiteActionDispatcher, GraphiteQueryEditorState] => {
|
||||||
|
let state = {} as GraphiteQueryEditorState;
|
||||||
|
|
||||||
|
const dispatch = async (action: Action) => {
|
||||||
|
state = await reducer(action, state);
|
||||||
|
onChange(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
return [dispatch, state];
|
||||||
|
};
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
import { DataQuery, DataSourceJsonData } from '@grafana/data';
|
||||||
|
import { GraphiteDatasource } from './datasource';
|
||||||
|
import { TemplateSrv } from '../../../features/templating/template_srv';
|
||||||
|
|
||||||
export interface GraphiteQuery extends DataQuery {
|
export interface GraphiteQuery extends DataQuery {
|
||||||
target?: string;
|
target?: string;
|
||||||
@@ -53,3 +55,34 @@ export type GraphiteMetricLokiMatcher = {
|
|||||||
value: string;
|
value: string;
|
||||||
labelName?: string;
|
labelName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GraphiteSegment = {
|
||||||
|
value: string;
|
||||||
|
type?: 'tag' | 'metric' | 'series-ref';
|
||||||
|
expandable?: boolean;
|
||||||
|
focus?: boolean;
|
||||||
|
fake?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GraphiteTagOperator = '=' | '!=' | '=~' | '!=~';
|
||||||
|
|
||||||
|
export type GraphiteTag = {
|
||||||
|
key: string;
|
||||||
|
operator: GraphiteTagOperator;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GraphiteActionDispatcher = (action: any) => Promise<void>;
|
||||||
|
|
||||||
|
export type GraphiteQueryEditorAngularDependencies = {
|
||||||
|
panelCtrl: any;
|
||||||
|
target: any;
|
||||||
|
datasource: GraphiteDatasource;
|
||||||
|
uiSegmentSrv: any;
|
||||||
|
templateSrv: TemplateSrv;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AngularDropdownOptions = {
|
||||||
|
text: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user