mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Graphite: Migrate to React (part 3: migrate segments) (#37309)
* 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 * Add GraphiteTextEditor * Add play button * Add AddGraphiteFunction * Use Segment to simplify AddGraphiteFunction * Memoize function defs * Fix useCallback deps * 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 * Remove submitOnClickAwayOption This behavior is actually needed to remove parameters in functions * Load function definitions before parsing the target on initial load * Add button padding * Fix loading function definitions * 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 * Migrate function editor to react * Simplify setting Segment Select min width * Remove unnecessary changes to SegmentInput * Extract view logic to a helper and update types definitions * Clean up types * Update FuncDef types and add tests * Show red border for unknown functions * Autofocus on new params * Extract params mapping to a helper * Split code between params and function editor * Focus on the first param when a function is added even if it's an optional argument * Add function editor tests * Remove todo marker * Fix adding new functions * Allow empty value in selects for removing function params * Add placeholders and fix styling * Add more docs * Create basic implementation for metrics and tags * Post merge fixes These files are not .ts * Remove mapping to Angular dropdowns * Simplify mapping tag names, values and operators * Simplify mapping metrics * Fix removing tags and autocomplete * Simplify debouncing providers * Ensure options are loaded twice and segment is opened * Remove focusing new segments logic (not supported by React's segment) * Clean up * Move debouncing to components * Simplify mapping to selectable options * Add docs * use getStyles * remove redundant async/await * Remove * remove redundant async/await * Remove console.log and silent test console output * Do not display the name of the selected dropdown option * Use block docs for better doc generation * Handle undefined values provided for autocomplete Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { SegmentAsync } from '@grafana/ui';
|
||||
import { actions } from '../state/actions';
|
||||
import { Dispatch } from 'redux';
|
||||
import { GraphiteSegment } from '../types';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { getAltSegmentsSelectables } from '../state/providers';
|
||||
import { debounce } from 'lodash';
|
||||
import { GraphiteQueryEditorState } from '../state/store';
|
||||
|
||||
type Props = {
|
||||
segment: GraphiteSegment;
|
||||
metricIndex: number;
|
||||
dispatch: Dispatch;
|
||||
state: GraphiteQueryEditorState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Represents a single metric node in the metric path at the given index. Allows to change the metric name to one of the
|
||||
* provided options or a custom value.
|
||||
*
|
||||
* Options for tag names and metric names are reloaded while user is typing with backend taking care of auto-complete
|
||||
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
||||
*
|
||||
* getAltSegmentsSelectables() also returns list of tags for segment with index=0. Once a tag is selected the editor
|
||||
* enters tag-adding mode (see SeriesSection and GraphiteQueryModel.seriesByTagUsed).
|
||||
*/
|
||||
export function MetricSegment({ dispatch, metricIndex, segment, state }: Props) {
|
||||
const loadOptions = useCallback(
|
||||
(value: string | undefined) => {
|
||||
return getAltSegmentsSelectables(state, metricIndex, value || '');
|
||||
},
|
||||
[state, metricIndex]
|
||||
);
|
||||
const debouncedLoadOptions = useMemo(() => debounce(loadOptions, 200, { leading: true }), [loadOptions]);
|
||||
|
||||
const onSegmentChanged = useCallback(
|
||||
(selectableValue: SelectableValue<GraphiteSegment | string>) => {
|
||||
// selectableValue.value is always defined because emptyValues are not allowed in SegmentAsync by default
|
||||
dispatch(actions.segmentValueChanged({ segment: selectableValue.value!, index: metricIndex }));
|
||||
},
|
||||
[dispatch, metricIndex]
|
||||
);
|
||||
|
||||
// segmentValueChanged action will destroy SegmentAsync immediately if a tag is selected. To give time
|
||||
// for the clean up the action is debounced.
|
||||
const onSegmentChangedDebounced = useMemo(() => debounce(onSegmentChanged, 100), [onSegmentChanged]);
|
||||
|
||||
return (
|
||||
<SegmentAsync<GraphiteSegment | string>
|
||||
value={segment.value}
|
||||
inputMinWidth={150}
|
||||
allowCustomValue={true}
|
||||
loadOptions={debouncedLoadOptions}
|
||||
reloadOptionsOnChange={true}
|
||||
onChange={onSegmentChangedDebounced}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { GraphiteSegment } from '../types';
|
||||
import { GraphiteQueryEditorState } from '../state/store';
|
||||
import { MetricSegment } from './MetricSegment';
|
||||
import { css } from '@emotion/css';
|
||||
import { useStyles2 } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
segments: GraphiteSegment[];
|
||||
dispatch: Dispatch;
|
||||
state: GraphiteQueryEditorState;
|
||||
};
|
||||
|
||||
export function MetricsSection({ dispatch, segments = [], state }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{segments.map((segment, index) => {
|
||||
return <MetricSegment segment={segment} metricIndex={index} key={index} dispatch={dispatch} state={state} />;
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles() {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`,
|
||||
};
|
||||
}
|
@@ -0,0 +1,23 @@
|
||||
import React from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { GraphiteQueryEditorState } from '../state/store';
|
||||
import { TagsSection } from './TagsSection';
|
||||
import { MetricsSection } from './MetricsSection';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
state: GraphiteQueryEditorState;
|
||||
};
|
||||
|
||||
export function SeriesSection({ dispatch, state }: Props) {
|
||||
return state.queryModel?.seriesByTagUsed ? (
|
||||
<TagsSection
|
||||
dispatch={dispatch}
|
||||
tags={state.queryModel?.tags}
|
||||
addTagSegments={state.addTagSegments}
|
||||
state={state}
|
||||
/>
|
||||
) : (
|
||||
<MetricsSection dispatch={dispatch} segments={state.segments} state={state} />
|
||||
);
|
||||
}
|
@@ -0,0 +1,91 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { Segment, SegmentAsync } from '@grafana/ui';
|
||||
import { actions } from '../state/actions';
|
||||
import { GraphiteTag, GraphiteTagOperator } from '../types';
|
||||
import { getTagOperatorsSelectables, getTagsSelectables, getTagValuesSelectables } from '../state/providers';
|
||||
import { GraphiteQueryEditorState } from '../state/store';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
tag: GraphiteTag;
|
||||
tagIndex: number;
|
||||
dispatch: Dispatch;
|
||||
state: GraphiteQueryEditorState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Editor for a tag at given index. Allows changing the name of the tag, operator or value. Tag names are provided with
|
||||
* getTagsSelectables and contain only valid tags (it may depend on currently used tags). The dropdown for tag names is
|
||||
* also used for removing tag (with a special "--remove tag--" option provided by getTagsSelectables).
|
||||
*
|
||||
* Options for tag names and values are reloaded while user is typing with backend taking care of auto-complete
|
||||
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
||||
*/
|
||||
export function TagEditor({ dispatch, tag, tagIndex, state }: Props) {
|
||||
const getTagsOptions = useCallback(
|
||||
(inputValue: string | undefined) => {
|
||||
return getTagsSelectables(state, tagIndex, inputValue || '');
|
||||
},
|
||||
[state, tagIndex]
|
||||
);
|
||||
const debouncedGetTagsOptions = useMemo(() => debounce(getTagsOptions, 200, { leading: true }), [getTagsOptions]);
|
||||
|
||||
const getTagValueOptions = useCallback(
|
||||
(inputValue: string | undefined) => {
|
||||
return getTagValuesSelectables(state, tag, tagIndex, inputValue || '');
|
||||
},
|
||||
[state, tagIndex, tag]
|
||||
);
|
||||
const debouncedGetTagValueOptions = useMemo(() => debounce(getTagValueOptions, 200, { leading: true }), [
|
||||
getTagValueOptions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<SegmentAsync
|
||||
inputMinWidth={150}
|
||||
value={tag.key}
|
||||
loadOptions={debouncedGetTagsOptions}
|
||||
reloadOptionsOnChange={true}
|
||||
onChange={(value) => {
|
||||
dispatch(
|
||||
actions.tagChanged({
|
||||
tag: { ...tag, key: value.value! },
|
||||
index: tagIndex,
|
||||
})
|
||||
);
|
||||
}}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
<Segment<GraphiteTagOperator>
|
||||
inputMinWidth={50}
|
||||
value={tag.operator}
|
||||
options={getTagOperatorsSelectables()}
|
||||
onChange={(value) => {
|
||||
dispatch(
|
||||
actions.tagChanged({
|
||||
tag: { ...tag, operator: value.value! },
|
||||
index: tagIndex,
|
||||
})
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<SegmentAsync
|
||||
inputMinWidth={150}
|
||||
value={tag.value}
|
||||
loadOptions={debouncedGetTagValueOptions}
|
||||
reloadOptionsOnChange={true}
|
||||
onChange={(value) => {
|
||||
dispatch(
|
||||
actions.tagChanged({
|
||||
tag: { ...tag, value: value.value! },
|
||||
index: tagIndex,
|
||||
})
|
||||
);
|
||||
}}
|
||||
allowCustomValue={true}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,75 @@
|
||||
import React, { useCallback, useMemo } from 'react';
|
||||
import { Dispatch } from 'redux';
|
||||
import { GraphiteSegment } from '../types';
|
||||
import { GraphiteTag } from '../graphite_query';
|
||||
import { GraphiteQueryEditorState } from '../state/store';
|
||||
import { getTagsAsSegmentsSelectables } from '../state/providers';
|
||||
import { Button, SegmentAsync, useStyles2 } from '@grafana/ui';
|
||||
import { actions } from '../state/actions';
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { css } from '@emotion/css';
|
||||
import { mapSegmentsToSelectables } from './helpers';
|
||||
import { TagEditor } from './TagEditor';
|
||||
import { debounce } from 'lodash';
|
||||
|
||||
type Props = {
|
||||
dispatch: Dispatch;
|
||||
tags: GraphiteTag[];
|
||||
addTagSegments: GraphiteSegment[];
|
||||
state: GraphiteQueryEditorState;
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders all tags and a button allowing to add more tags.
|
||||
*
|
||||
* Options for tag names are reloaded while user is typing with backend taking care of auto-complete
|
||||
* (auto-complete cannot be implemented in front-end because backend returns only limited number of entries)
|
||||
*/
|
||||
export function TagsSection({ dispatch, tags, state, addTagSegments }: Props) {
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const newTagsOptions = mapSegmentsToSelectables(addTagSegments || []);
|
||||
|
||||
// Options are reloaded while user is typing with backend taking care of auto-complete (auto-complete cannot be
|
||||
// implemented in front-end because backend returns only limited number of entries)
|
||||
const getTagsAsSegmentsOptions = useCallback(
|
||||
(inputValue?: string) => {
|
||||
return getTagsAsSegmentsSelectables(state, inputValue || '');
|
||||
},
|
||||
[state]
|
||||
);
|
||||
const debouncedGetTagsAsSegments = useMemo(() => debounce(getTagsAsSegmentsOptions, 200, { leading: true }), [
|
||||
getTagsAsSegmentsOptions,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{tags.map((tag, index) => {
|
||||
return <TagEditor key={index} tagIndex={index} tag={tag} dispatch={dispatch} state={state} />;
|
||||
})}
|
||||
{newTagsOptions.length && (
|
||||
<SegmentAsync<GraphiteSegment>
|
||||
inputMinWidth={150}
|
||||
onChange={(value) => {
|
||||
dispatch(actions.addNewTag({ segment: value.value! }));
|
||||
}}
|
||||
loadOptions={debouncedGetTagsAsSegments}
|
||||
reloadOptionsOnChange={true}
|
||||
Component={<Button icon="plus" variant="secondary" className={styles.button} />}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getStyles(theme: GrafanaTheme2) {
|
||||
return {
|
||||
container: css`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
`,
|
||||
button: css`
|
||||
margin-right: ${theme.spacing(0.5)};
|
||||
`,
|
||||
};
|
||||
}
|
@@ -2,6 +2,21 @@ import { FuncDefs, FuncInstance, ParamDef } from '../gfunc';
|
||||
import { forEach, sortBy } from 'lodash';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { EditableParam } from './FunctionParamEditor';
|
||||
import { GraphiteSegment } from '../types';
|
||||
|
||||
export function mapStringsToSelectables<T extends string>(values: T[]): Array<SelectableValue<T>> {
|
||||
return values.map((value) => ({
|
||||
value,
|
||||
label: value,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapSegmentsToSelectables(segments: GraphiteSegment[]): Array<SelectableValue<GraphiteSegment>> {
|
||||
return segments.map((segment) => ({
|
||||
label: segment.value,
|
||||
value: segment,
|
||||
}));
|
||||
}
|
||||
|
||||
export function mapFuncDefsToSelectables(funcDefs: FuncDefs): Array<SelectableValue<string>> {
|
||||
const categories: any = {};
|
||||
|
@@ -303,11 +303,15 @@ export default class GraphiteQuery {
|
||||
|
||||
if (tag.key === this.removeTagValue) {
|
||||
this.removeTag(tagIndex);
|
||||
if (this.tags.length === 0) {
|
||||
this.removeFunction(this.getSeriesByTagFunc());
|
||||
this.checkOtherSegmentsIndex = 0;
|
||||
this.seriesByTagUsed = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const newTagParam = renderTagString(tag);
|
||||
this.getSeriesByTagFunc()!.params[tagIndex] = newTagParam;
|
||||
this.getSeriesByTagFunc()!.params[tagIndex] = renderTagString(tag);
|
||||
this.tags[tagIndex] = tag;
|
||||
}
|
||||
|
||||
|
@@ -10,46 +10,7 @@
|
||||
<label class="gf-form-label width-6 query-keyword">Series</label>
|
||||
</div>
|
||||
|
||||
<div ng-if="ctrl.state.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.state.queryModel.tags" class="gf-form">
|
||||
<gf-form-dropdown
|
||||
model="tag.key"
|
||||
allow-custom="true"
|
||||
label-mode="true"
|
||||
debounce="true"
|
||||
placeholder="Tag key"
|
||||
css-class="query-segment-key"
|
||||
get-options="ctrl.getTags($index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)"
|
||||
></gf-form-dropdown>
|
||||
|
||||
<gf-form-dropdown
|
||||
model="tag.operator"
|
||||
label-mode="true"
|
||||
css-class="query-segment-operator"
|
||||
get-options="ctrl.getTagOperators()"
|
||||
on-change="ctrl.tagChanged(tag, $index)"
|
||||
min-input-width="30"
|
||||
></gf-form-dropdown>
|
||||
<gf-form-dropdown
|
||||
model="tag.value"
|
||||
allow-custom="true"
|
||||
label-mode="true"
|
||||
debounce="true"
|
||||
css-class="query-segment-value"
|
||||
placeholder="Tag value"
|
||||
get-options="ctrl.getTagValues(tag, $index, $query)"
|
||||
on-change="ctrl.tagChanged(tag, $index)"
|
||||
></gf-form-dropdown>
|
||||
<label class="gf-form-label query-keyword" ng-if="$index !== ctrl.state.queryModel.tags.length - 1">AND</label>
|
||||
</div>
|
||||
|
||||
<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" />
|
||||
</div>
|
||||
|
||||
<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)" />
|
||||
</div>
|
||||
<series-section dispatch="ctrl.dispatch" tags="ctrl.state.queryModel.tags" addTagSegments="ctrl.state.addTagSegments" state="ctrl.state" segments="ctrl.state.segments" rawQuery="ctrl.state.target.target"></series-section>
|
||||
|
||||
<div ng-if="ctrl.state.paused" class="gf-form">
|
||||
<play-button dispatch="ctrl.dispatch" />
|
||||
|
@@ -3,10 +3,8 @@ import { QueryCtrl } from 'app/plugins/sdk';
|
||||
import { auto } from 'angular';
|
||||
import { TemplateSrv } from '@grafana/runtime';
|
||||
import { actions } from './state/actions';
|
||||
import { getAltSegments, getTagOperators, getTags, getTagsAsSegments, getTagValues } from './state/providers';
|
||||
import { createStore, GraphiteQueryEditorState } from './state/store';
|
||||
import {
|
||||
AngularDropdownOptions,
|
||||
GraphiteActionDispatcher,
|
||||
GraphiteQueryEditorAngularDependencies,
|
||||
GraphiteSegment,
|
||||
@@ -30,8 +28,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
supportsTags = false;
|
||||
paused = false;
|
||||
|
||||
private state: GraphiteQueryEditorState;
|
||||
private readonly dispatch: GraphiteActionDispatcher;
|
||||
state: GraphiteQueryEditorState;
|
||||
readonly dispatch: GraphiteActionDispatcher;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(
|
||||
@@ -104,7 +102,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
setSegmentFocus(segmentIndex: any) {
|
||||
// WIP: moved to state/helpers (the same name)
|
||||
// WIP: removed
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,8 +110,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
*
|
||||
* This is used for new segments and segments with metrics selected.
|
||||
*/
|
||||
async getAltSegments(index: number, text: string): Promise<GraphiteSegment[]> {
|
||||
return await getAltSegments(this.state, index, text);
|
||||
getAltSegments(index: number, text: string): void {
|
||||
// WIP: moved to state/providers (the same name)
|
||||
}
|
||||
|
||||
addAltTagSegments(prefix: string, altSegments: any[]) {
|
||||
@@ -128,7 +126,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
* Apply changes to a given metric segment
|
||||
*/
|
||||
async segmentValueChanged(segment: GraphiteSegment, index: number) {
|
||||
await this.dispatch(actions.segmentValueChanged({ segment, index }));
|
||||
// WIP: moved to MetricsSegment
|
||||
}
|
||||
|
||||
spliceSegments(index: any) {
|
||||
@@ -148,7 +146,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
async addFunction(name: string) {
|
||||
await this.dispatch(actions.addFunction({ name }));
|
||||
// WIP: removed, called from AddGraphiteFunction
|
||||
}
|
||||
|
||||
removeFunction(func: any) {
|
||||
@@ -177,22 +175,22 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
/**
|
||||
* Get list of tags for editing exiting tag with <gf-form-dropdown>
|
||||
*/
|
||||
async getTags(index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||
return await getTags(this.state, index, query);
|
||||
getTags(index: number, query: string): void {
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Get tag list when adding a new tag with <metric-segment>
|
||||
*/
|
||||
async getTagsAsSegments(query: string): Promise<GraphiteSegment[]> {
|
||||
return await getTagsAsSegments(this.state, query);
|
||||
getTagsAsSegments(query: string): void {
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available tag operators
|
||||
*/
|
||||
getTagOperators(): AngularDropdownOptions[] {
|
||||
return getTagOperators();
|
||||
getTagOperators(): void {
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
getAllTagValues(tag: { key: any }) {
|
||||
@@ -202,19 +200,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
/**
|
||||
* Get list of available tag values
|
||||
*/
|
||||
async getTagValues(tag: GraphiteTag, index: number, query: string): Promise<AngularDropdownOptions[]> {
|
||||
return await getTagValues(this.state, tag, index, query);
|
||||
getTagValues(tag: GraphiteTag, index: number, query: string): void {
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply changes when a tag is changed
|
||||
*/
|
||||
async tagChanged(tag: GraphiteTag, index: number) {
|
||||
await this.dispatch(actions.tagChanged({ tag, index }));
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
async addNewTag(segment: GraphiteSegment) {
|
||||
await this.dispatch(actions.addNewTag({ segment }));
|
||||
// WIP: removed, called from TagsSection
|
||||
}
|
||||
|
||||
removeTag(index: any) {
|
||||
@@ -235,7 +233,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
||||
}
|
||||
|
||||
async unpause() {
|
||||
await this.dispatch(actions.unpause());
|
||||
// WIP: removed, called from PlayButton
|
||||
}
|
||||
|
||||
getCollapsedText() {
|
||||
|
@@ -5,6 +5,8 @@ import { GraphiteQueryCtrl } from '../query_ctrl';
|
||||
import { TemplateSrvStub } from 'test/specs/helpers';
|
||||
import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput';
|
||||
import { actions } from '../state/actions';
|
||||
import { getAltSegmentsSelectables, getTagsSelectables, getTagsAsSegmentsSelectables } from '../state/providers';
|
||||
import { GraphiteSegment } from '../types';
|
||||
|
||||
jest.mock('app/core/utils/promiseToDigest', () => ({
|
||||
promiseToDigest: (scope: any) => {
|
||||
@@ -105,8 +107,8 @@ describe('GraphiteQueryCtrl', () => {
|
||||
|
||||
describe('when middle segment value of test.prod.* is changed', () => {
|
||||
beforeEach(async () => {
|
||||
const segment = { type: 'segment', value: 'test', expandable: true };
|
||||
await ctx.ctrl.segmentValueChanged(segment, 1);
|
||||
const segment: GraphiteSegment = { type: 'metric', value: 'test', expandable: true };
|
||||
await ctx.ctrl.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
|
||||
});
|
||||
|
||||
it('should validate metric key exists', () => {
|
||||
@@ -129,7 +131,7 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, 'test.prod.*.count');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('aliasByNode'));
|
||||
await ctx.ctrl.dispatch(actions.addFunction({ name: 'aliasByNode' }));
|
||||
});
|
||||
|
||||
it('should add function with correct node number', () => {
|
||||
@@ -149,7 +151,7 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
|
||||
await changeTarget(ctx, '');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('asPercent'));
|
||||
await ctx.ctrl.dispatch(actions.addFunction({ name: 'asPercent' }));
|
||||
});
|
||||
|
||||
it('should add function and remove select metric link', () => {
|
||||
@@ -191,7 +193,7 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
|
||||
await changeTarget(ctx, 'test.count');
|
||||
ctx.altSegments = await ctx.ctrl.getAltSegments(1, '');
|
||||
ctx.altSegments = await getAltSegmentsSelectables(ctx.ctrl.state, 1, '');
|
||||
});
|
||||
|
||||
it('should have no segments', () => {
|
||||
@@ -210,9 +212,9 @@ describe('GraphiteQueryCtrl', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getAltSegments should handle autocomplete errors', async () => {
|
||||
it('getAltSegmentsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await ctx.ctrl.getAltSegments(0, 'any');
|
||||
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
@@ -221,11 +223,11 @@ describe('GraphiteQueryCtrl', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getAltSegments should display the error message only once', async () => {
|
||||
await ctx.ctrl.getAltSegments(0, 'any');
|
||||
it('getAltSegmentsSelectables should display the error message only once', async () => {
|
||||
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
|
||||
await ctx.ctrl.getAltSegments(0, 'any');
|
||||
await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -241,9 +243,9 @@ describe('GraphiteQueryCtrl', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('getTags should handle autocomplete errors', async () => {
|
||||
it('getTagsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await ctx.ctrl.getTags(0, 'any');
|
||||
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
@@ -252,17 +254,17 @@ describe('GraphiteQueryCtrl', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getTags should display the error message only once', async () => {
|
||||
await ctx.ctrl.getTags(0, 'any');
|
||||
it('getTagsSelectables should display the error message only once', async () => {
|
||||
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
|
||||
await ctx.ctrl.getTags(0, 'any');
|
||||
await getTagsSelectables(ctx.ctrl.state, 0, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
|
||||
it('getTagsAsSegments should handle autocomplete errors', async () => {
|
||||
it('getTagsAsSegmentsSelectables should handle autocomplete errors', async () => {
|
||||
await expect(async () => {
|
||||
await ctx.ctrl.getTagsAsSegments('any');
|
||||
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
|
||||
expect(mockDispatch).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'appNotifications/notifyApp',
|
||||
@@ -271,11 +273,11 @@ describe('GraphiteQueryCtrl', () => {
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
it('getTagsAsSegments should display the error message only once', async () => {
|
||||
await ctx.ctrl.getTagsAsSegments('any');
|
||||
it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
|
||||
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
|
||||
await ctx.ctrl.getTagsAsSegments('any');
|
||||
await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
|
||||
expect(mockDispatch.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -350,7 +352,7 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, '');
|
||||
await ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag'));
|
||||
await ctx.ctrl.dispatch(actions.addFunction({ name: 'seriesByTag' }));
|
||||
});
|
||||
|
||||
it('should update functions', () => {
|
||||
@@ -393,7 +395,7 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, 'seriesByTag()');
|
||||
await ctx.ctrl.addNewTag({ value: 'tag1' });
|
||||
await ctx.ctrl.dispatch(actions.addNewTag({ segment: { value: 'tag1' } }));
|
||||
});
|
||||
|
||||
it('should update tags with default value', () => {
|
||||
@@ -411,7 +413,9 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||
await ctx.ctrl.tagChanged({ key: 'tag1', operator: '=', value: 'new_value' }, 0);
|
||||
await ctx.ctrl.dispatch(
|
||||
actions.tagChanged({ tag: { key: 'tag1', operator: '=', value: 'new_value' }, index: 0 })
|
||||
);
|
||||
});
|
||||
|
||||
it('should update tags', () => {
|
||||
@@ -432,7 +436,9 @@ describe('GraphiteQueryCtrl', () => {
|
||||
beforeEach(async () => {
|
||||
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
|
||||
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')");
|
||||
await ctx.ctrl.tagChanged({ key: ctx.ctrl.state.removeTagValue });
|
||||
await ctx.ctrl.dispatch(
|
||||
actions.tagChanged({ tag: { key: ctx.ctrl.state.removeTagValue, operator: '=', value: '' }, index: 0 })
|
||||
);
|
||||
});
|
||||
|
||||
it('should update tags', () => {
|
||||
|
@@ -12,7 +12,7 @@ import { FuncInstance } from '../gfunc';
|
||||
const init = createAction<GraphiteQueryEditorAngularDependencies>('init');
|
||||
|
||||
// Metrics & Tags
|
||||
const segmentValueChanged = createAction<{ segment: GraphiteSegment; index: number }>('segment-value-changed');
|
||||
const segmentValueChanged = createAction<{ segment: GraphiteSegment | string; index: number }>('segment-value-changed');
|
||||
|
||||
// Tags
|
||||
const addNewTag = createAction<{ segment: GraphiteSegment }>('add-new-tag');
|
||||
|
@@ -1,15 +1,16 @@
|
||||
import { GraphiteQueryEditorState } from './store';
|
||||
import { each, map } from 'lodash';
|
||||
import { map } from 'lodash';
|
||||
import { dispatch } from '../../../../store/store';
|
||||
import { notifyApp } from '../../../../core/reducers/appNotification';
|
||||
import { createErrorNotification } from '../../../../core/copy/appNotification';
|
||||
import { FuncInstance } from '../gfunc';
|
||||
import { GraphiteTagOperator } from '../types';
|
||||
|
||||
/**
|
||||
* 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 = ['=', '!=', '=~', '!=~'];
|
||||
export const GRAPHITE_TAG_OPERATORS: GraphiteTagOperator[] = ['=', '!=', '=~', '!=~'];
|
||||
|
||||
/**
|
||||
* Tag names and metric names are displayed in a single dropdown. This prefix is used to
|
||||
@@ -96,18 +97,6 @@ export async function checkOtherSegments(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
|
@@ -6,7 +6,9 @@ import {
|
||||
handleMetricsAutoCompleteError,
|
||||
handleTagsAutoCompleteError,
|
||||
} from './helpers';
|
||||
import { AngularDropdownOptions, GraphiteSegment, GraphiteTag } from '../types';
|
||||
import { GraphiteSegment, GraphiteTag, GraphiteTagOperator } from '../types';
|
||||
import { mapSegmentsToSelectables, mapStringsToSelectables } from '../components/helpers';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
|
||||
/**
|
||||
* Providers are hooks for views to provide temporal data for autocomplete. They don't modify the state.
|
||||
@@ -19,7 +21,7 @@ import { AngularDropdownOptions, GraphiteSegment, GraphiteTag } from '../types';
|
||||
* - 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(
|
||||
async function getAltSegments(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
prefix: string
|
||||
@@ -90,25 +92,29 @@ export async function getAltSegments(
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getTagOperators(): AngularDropdownOptions[] {
|
||||
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
|
||||
export async function getAltSegmentsSelectables(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
prefix: string
|
||||
): Promise<Array<SelectableValue<GraphiteSegment>>> {
|
||||
return mapSegmentsToSelectables(await getAltSegments(state, index, prefix));
|
||||
}
|
||||
|
||||
export function getTagOperatorsSelectables(): Array<SelectableValue<GraphiteTagOperator>> {
|
||||
return mapStringsToSelectables(GRAPHITE_TAG_OPERATORS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns tags as dropdown options
|
||||
*/
|
||||
export async function getTags(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
tagPrefix: string
|
||||
): Promise<AngularDropdownOptions[]> {
|
||||
async function getTags(state: GraphiteQueryEditorState, index: number, tagPrefix: string): Promise<string[]> {
|
||||
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);
|
||||
return altTags;
|
||||
} catch (err) {
|
||||
handleTagsAutoCompleteError(state, err);
|
||||
}
|
||||
@@ -116,15 +122,20 @@ export async function getTags(
|
||||
return [];
|
||||
}
|
||||
|
||||
export async function getTagsSelectables(
|
||||
state: GraphiteQueryEditorState,
|
||||
index: number,
|
||||
tagPrefix: string
|
||||
): Promise<Array<SelectableValue<string>>> {
|
||||
return mapStringsToSelectables(await getTags(state, index, tagPrefix));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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[] = [];
|
||||
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);
|
||||
@@ -143,12 +154,19 @@ export async function getTagsAsSegments(
|
||||
return tagsAsSegments;
|
||||
}
|
||||
|
||||
export async function getTagValues(
|
||||
export async function getTagsAsSegmentsSelectables(
|
||||
state: GraphiteQueryEditorState,
|
||||
tagPrefix: string
|
||||
): Promise<Array<SelectableValue<GraphiteSegment>>> {
|
||||
return mapSegmentsToSelectables(await getTagsAsSegments(state, tagPrefix));
|
||||
}
|
||||
|
||||
async function getTagValues(
|
||||
state: GraphiteQueryEditorState,
|
||||
tag: GraphiteTag,
|
||||
index: number,
|
||||
valuePrefix: string
|
||||
): Promise<AngularDropdownOptions[]> {
|
||||
): Promise<string[]> {
|
||||
const tagExpressions = state.queryModel.renderTagExpressions(index);
|
||||
const tagKey = tag.key;
|
||||
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {});
|
||||
@@ -158,7 +176,16 @@ export async function getTagValues(
|
||||
altValues.push('${' + variable.name + ':regex}');
|
||||
});
|
||||
|
||||
return mapToDropdownOptions(altValues);
|
||||
return altValues;
|
||||
}
|
||||
|
||||
export async function getTagValuesSelectables(
|
||||
state: GraphiteQueryEditorState,
|
||||
tag: GraphiteTag,
|
||||
index: number,
|
||||
valuePrefix: string
|
||||
): Promise<Array<SelectableValue<string>>> {
|
||||
return mapStringsToSelectables(await getTagValues(state, tag, index, valuePrefix));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,9 +209,3 @@ async function addAltTagSegments(
|
||||
function removeTaggedEntry(altSegments: GraphiteSegment[]) {
|
||||
remove(altSegments, (s) => s.value === '_tagged');
|
||||
}
|
||||
|
||||
function mapToDropdownOptions(results: string[]) {
|
||||
return map(results, (value) => {
|
||||
return { text: value, value: value };
|
||||
});
|
||||
}
|
||||
|
@@ -14,7 +14,6 @@ import {
|
||||
parseTarget,
|
||||
pause,
|
||||
removeTagPrefix,
|
||||
setSegmentFocus,
|
||||
smartlyHandleNewAliasByNode,
|
||||
spliceSegments,
|
||||
} from './helpers';
|
||||
@@ -72,9 +71,22 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
|
||||
await buildSegments(state, false);
|
||||
}
|
||||
if (actions.segmentValueChanged.match(action)) {
|
||||
const { segment, index: segmentIndex } = action.payload;
|
||||
const { segment: segmentOrString, index: segmentIndex } = action.payload;
|
||||
|
||||
let segment;
|
||||
// is segment was changed to a string - create a new segment
|
||||
if (typeof segmentOrString === 'string') {
|
||||
segment = {
|
||||
value: segmentOrString,
|
||||
expandable: true,
|
||||
fake: false,
|
||||
};
|
||||
} else {
|
||||
segment = segmentOrString as GraphiteSegment;
|
||||
}
|
||||
|
||||
state.error = null;
|
||||
state.segments[segmentIndex] = segment;
|
||||
state.queryModel.updateSegmentValue(segment, segmentIndex);
|
||||
|
||||
if (state.queryModel.functions.length > 0 && state.queryModel.functions[0].def.fake) {
|
||||
@@ -88,21 +100,24 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
|
||||
return state;
|
||||
}
|
||||
|
||||
// if newly selected segment can be expanded -> check if the path is correct
|
||||
if (segment.expandable) {
|
||||
await checkOtherSegments(state, segmentIndex + 1);
|
||||
setSegmentFocus(state, segmentIndex + 1);
|
||||
handleTargetChanged(state);
|
||||
} else {
|
||||
// if not expandable -> remove all other segments
|
||||
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 (state.queryModel.tags.length === 0) {
|
||||
await checkOtherSegments(state, 0);
|
||||
state.paused = false;
|
||||
}
|
||||
}
|
||||
if (actions.addNewTag.match(action)) {
|
||||
const segment = action.payload.segment;
|
||||
|
@@ -60,7 +60,6 @@ export type GraphiteSegment = {
|
||||
value: string;
|
||||
type?: 'tag' | 'metric' | 'series-ref';
|
||||
expandable?: boolean;
|
||||
focus?: boolean;
|
||||
fake?: boolean;
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user