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:
Piotr Jamróz
2021-08-11 09:13:55 +02:00
committed by GitHub
parent 2a1363f175
commit 31bb3522c8
18 changed files with 448 additions and 134 deletions

View File

@@ -13,6 +13,11 @@ import { useStyles } from '../../themes';
export interface SegmentAsyncProps<T> extends SegmentProps<T>, Omit<HTMLProps<HTMLDivElement>, 'value' | 'onChange'> { export interface SegmentAsyncProps<T> extends SegmentProps<T>, Omit<HTMLProps<HTMLDivElement>, 'value' | 'onChange'> {
value?: T | SelectableValue<T>; value?: T | SelectableValue<T>;
loadOptions: (query?: string) => Promise<Array<SelectableValue<T>>>; loadOptions: (query?: string) => Promise<Array<SelectableValue<T>>>;
/**
* If true options will be reloaded when user changes the value in the input,
* otherwise, options will be loaded when the segment is clicked
*/
reloadOptionsOnChange?: boolean;
onChange: (item: SelectableValue<T>) => void; onChange: (item: SelectableValue<T>) => void;
noOptionMessageHandler?: (state: AsyncState<Array<SelectableValue<T>>>) => string; noOptionMessageHandler?: (state: AsyncState<Array<SelectableValue<T>>>) => string;
inputMinWidth?: number; inputMinWidth?: number;
@@ -22,6 +27,7 @@ export function SegmentAsync<T>({
value, value,
onChange, onChange,
loadOptions, loadOptions,
reloadOptionsOnChange = false,
Component, Component,
className, className,
allowCustomValue, allowCustomValue,
@@ -45,7 +51,7 @@ export function SegmentAsync<T>({
return ( return (
<Label <Label
onClick={fetchOptions} onClick={reloadOptionsOnChange ? undefined : fetchOptions}
disabled={disabled} disabled={disabled}
Component={ Component={
Component || ( Component || (
@@ -73,6 +79,7 @@ export function SegmentAsync<T>({
value={value && !isObject(value) ? { value } : value} value={value && !isObject(value) ? { value } : value}
placeholder={inputPlaceholder} placeholder={inputPlaceholder}
options={state.value ?? []} options={state.value ?? []}
loadOptions={reloadOptionsOnChange ? fetchOptions : undefined}
width={width} width={width}
noOptionsMessage={noOptionMessageHandler(state)} noOptionsMessage={noOptionMessageHandler(state)}
allowCustomValue={allowCustomValue} allowCustomValue={allowCustomValue}

View File

@@ -1,6 +1,6 @@
import React, { HTMLProps, useRef } from 'react'; import React, { HTMLProps, useRef } from 'react';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { Select } from '../Select/Select'; import { AsyncSelect, Select } from '../Select/Select';
import { useTheme2 } from '../../themes/ThemeContext'; import { useTheme2 } from '../../themes/ThemeContext';
/** @internal /** @internal
@@ -11,6 +11,10 @@ export interface Props<T> extends Omit<HTMLProps<HTMLDivElement>, 'value' | 'onC
value?: SelectableValue<T>; value?: SelectableValue<T>;
options: Array<SelectableValue<T>>; options: Array<SelectableValue<T>>;
onChange: (item: SelectableValue<T>) => void; onChange: (item: SelectableValue<T>) => void;
/**
* If provided - AsyncSelect will be used allowing to reload options when the value in the input changes
*/
loadOptions?: (inputValue: string) => Promise<Array<SelectableValue<T>>>;
onClickOutside: () => void; onClickOutside: () => void;
width: number; width: number;
noOptionsMessage?: string; noOptionsMessage?: string;
@@ -30,6 +34,7 @@ export function SegmentSelect<T>({
options = [], options = [],
onChange, onChange,
onClickOutside, onClickOutside,
loadOptions = undefined,
width: widthPixels, width: widthPixels,
noOptionsMessage = '', noOptionsMessage = '',
allowCustomValue = false, allowCustomValue = false,
@@ -41,9 +46,18 @@ export function SegmentSelect<T>({
let width = widthPixels > 0 ? widthPixels / theme.spacing.gridSize : undefined; let width = widthPixels > 0 ? widthPixels / theme.spacing.gridSize : undefined;
let Component;
let asyncOptions = {};
if (loadOptions) {
Component = AsyncSelect;
asyncOptions = { loadOptions, defaultOptions: true };
} else {
Component = Select;
}
return ( return (
<div {...rest} ref={ref}> <div {...rest} ref={ref}>
<Select <Component
menuShouldPortal menuShouldPortal
width={width} width={width}
noOptionsMessage={noOptionsMessage} noOptionsMessage={noOptionsMessage}
@@ -70,6 +84,7 @@ export function SegmentSelect<T>({
} }
}} }}
allowCustomValue={allowCustomValue} allowCustomValue={allowCustomValue}
{...asyncOptions}
/> />
</div> </div>
); );

View File

@@ -29,6 +29,7 @@ import { GraphiteTextEditor } from '../plugins/datasource/graphite/components/Gr
import { PlayButton } from '../plugins/datasource/graphite/components/PlayButton'; import { PlayButton } from '../plugins/datasource/graphite/components/PlayButton';
import { AddGraphiteFunction } from '../plugins/datasource/graphite/components/AddGraphiteFunction'; import { AddGraphiteFunction } from '../plugins/datasource/graphite/components/AddGraphiteFunction';
import { GraphiteFunctionEditor } from '../plugins/datasource/graphite/components/GraphiteFunctionEditor'; import { GraphiteFunctionEditor } from '../plugins/datasource/graphite/components/GraphiteFunctionEditor';
import { SeriesSection } from '../plugins/datasource/graphite/components/SeriesSection';
const { SecretFormField } = LegacyForms; const { SecretFormField } = LegacyForms;
@@ -211,4 +212,5 @@ export function registerAngularDirectives() {
react2AngularDirective('playButton', PlayButton, ['dispatch']); react2AngularDirective('playButton', PlayButton, ['dispatch']);
react2AngularDirective('addGraphiteFunction', AddGraphiteFunction, ['funcDefs', 'dispatch']); react2AngularDirective('addGraphiteFunction', AddGraphiteFunction, ['funcDefs', 'dispatch']);
react2AngularDirective('graphiteFunctionEditor', GraphiteFunctionEditor, ['func', 'dispatch']); react2AngularDirective('graphiteFunctionEditor', GraphiteFunctionEditor, ['func', 'dispatch']);
react2AngularDirective('seriesSection', SeriesSection, ['state', 'dispatch']);
} }

View File

@@ -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}
/>
);
}

View File

@@ -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;
`,
};
}

View File

@@ -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} />
);
}

View File

@@ -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}
/>
</>
);
}

View File

@@ -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)};
`,
};
}

View File

@@ -2,6 +2,21 @@ import { FuncDefs, FuncInstance, ParamDef } from '../gfunc';
import { forEach, sortBy } from 'lodash'; import { forEach, sortBy } from 'lodash';
import { SelectableValue } from '@grafana/data'; import { SelectableValue } from '@grafana/data';
import { EditableParam } from './FunctionParamEditor'; 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>> { export function mapFuncDefsToSelectables(funcDefs: FuncDefs): Array<SelectableValue<string>> {
const categories: any = {}; const categories: any = {};

View File

@@ -303,11 +303,15 @@ export default class GraphiteQuery {
if (tag.key === this.removeTagValue) { if (tag.key === this.removeTagValue) {
this.removeTag(tagIndex); this.removeTag(tagIndex);
if (this.tags.length === 0) {
this.removeFunction(this.getSeriesByTagFunc());
this.checkOtherSegmentsIndex = 0;
this.seriesByTagUsed = false;
}
return; return;
} }
const newTagParam = renderTagString(tag); this.getSeriesByTagFunc()!.params[tagIndex] = renderTagString(tag);
this.getSeriesByTagFunc()!.params[tagIndex] = newTagParam;
this.tags[tagIndex] = tag; this.tags[tagIndex] = tag;
} }

View File

@@ -10,46 +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.state.queryModel.seriesByTagUsed" ng-repeat="tag in ctrl.state.queryModel.tags" class="gf-form"> <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>
<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>
<div ng-if="ctrl.state.paused" class="gf-form"> <div ng-if="ctrl.state.paused" class="gf-form">
<play-button dispatch="ctrl.dispatch" /> <play-button dispatch="ctrl.dispatch" />

View File

@@ -3,10 +3,8 @@ import { QueryCtrl } from 'app/plugins/sdk';
import { auto } from 'angular'; import { auto } from 'angular';
import { TemplateSrv } from '@grafana/runtime'; import { TemplateSrv } from '@grafana/runtime';
import { actions } from './state/actions'; import { actions } from './state/actions';
import { getAltSegments, getTagOperators, getTags, getTagsAsSegments, getTagValues } from './state/providers';
import { createStore, GraphiteQueryEditorState } from './state/store'; import { createStore, GraphiteQueryEditorState } from './state/store';
import { import {
AngularDropdownOptions,
GraphiteActionDispatcher, GraphiteActionDispatcher,
GraphiteQueryEditorAngularDependencies, GraphiteQueryEditorAngularDependencies,
GraphiteSegment, GraphiteSegment,
@@ -30,8 +28,8 @@ export class GraphiteQueryCtrl extends QueryCtrl {
supportsTags = false; supportsTags = false;
paused = false; paused = false;
private state: GraphiteQueryEditorState; state: GraphiteQueryEditorState;
private readonly dispatch: GraphiteActionDispatcher; readonly dispatch: GraphiteActionDispatcher;
/** @ngInject */ /** @ngInject */
constructor( constructor(
@@ -104,7 +102,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
} }
setSegmentFocus(segmentIndex: any) { 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. * This is used for new segments and segments with metrics selected.
*/ */
async getAltSegments(index: number, text: string): Promise<GraphiteSegment[]> { getAltSegments(index: number, text: string): void {
return await getAltSegments(this.state, index, text); // WIP: moved to state/providers (the same name)
} }
addAltTagSegments(prefix: string, altSegments: any[]) { addAltTagSegments(prefix: string, altSegments: any[]) {
@@ -128,7 +126,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
* Apply changes to a given metric segment * Apply changes to a given metric segment
*/ */
async segmentValueChanged(segment: GraphiteSegment, index: number) { async segmentValueChanged(segment: GraphiteSegment, index: number) {
await this.dispatch(actions.segmentValueChanged({ segment, index })); // WIP: moved to MetricsSegment
} }
spliceSegments(index: any) { spliceSegments(index: any) {
@@ -148,7 +146,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
} }
async addFunction(name: string) { async addFunction(name: string) {
await this.dispatch(actions.addFunction({ name })); // WIP: removed, called from AddGraphiteFunction
} }
removeFunction(func: any) { removeFunction(func: any) {
@@ -177,22 +175,22 @@ export class GraphiteQueryCtrl extends QueryCtrl {
/** /**
* Get list of tags for editing exiting tag with <gf-form-dropdown> * Get list of tags for editing exiting tag with <gf-form-dropdown>
*/ */
async getTags(index: number, query: string): Promise<AngularDropdownOptions[]> { getTags(index: number, query: string): void {
return await getTags(this.state, index, query); // WIP: removed, called from TagsSection
} }
/** /**
* Get tag list when adding a new tag with <metric-segment> * Get tag list when adding a new tag with <metric-segment>
*/ */
async getTagsAsSegments(query: string): Promise<GraphiteSegment[]> { getTagsAsSegments(query: string): void {
return await getTagsAsSegments(this.state, query); // WIP: removed, called from TagsSection
} }
/** /**
* Get list of available tag operators * Get list of available tag operators
*/ */
getTagOperators(): AngularDropdownOptions[] { getTagOperators(): void {
return getTagOperators(); // WIP: removed, called from TagsSection
} }
getAllTagValues(tag: { key: any }) { getAllTagValues(tag: { key: any }) {
@@ -202,19 +200,19 @@ export class GraphiteQueryCtrl extends QueryCtrl {
/** /**
* Get list of available tag values * Get list of available tag values
*/ */
async getTagValues(tag: GraphiteTag, index: number, query: string): Promise<AngularDropdownOptions[]> { getTagValues(tag: GraphiteTag, index: number, query: string): void {
return await getTagValues(this.state, tag, index, query); // WIP: removed, called from TagsSection
} }
/** /**
* Apply changes when a tag is changed * Apply changes when a tag is changed
*/ */
async tagChanged(tag: GraphiteTag, index: number) { async tagChanged(tag: GraphiteTag, index: number) {
await this.dispatch(actions.tagChanged({ tag, index })); // WIP: removed, called from TagsSection
} }
async addNewTag(segment: GraphiteSegment) { async addNewTag(segment: GraphiteSegment) {
await this.dispatch(actions.addNewTag({ segment })); // WIP: removed, called from TagsSection
} }
removeTag(index: any) { removeTag(index: any) {
@@ -235,7 +233,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
} }
async unpause() { async unpause() {
await this.dispatch(actions.unpause()); // WIP: removed, called from PlayButton
} }
getCollapsedText() { getCollapsedText() {

View File

@@ -5,6 +5,8 @@ import { GraphiteQueryCtrl } from '../query_ctrl';
import { TemplateSrvStub } from 'test/specs/helpers'; import { TemplateSrvStub } from 'test/specs/helpers';
import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput'; import { silenceConsoleOutput } from 'test/core/utils/silenceConsoleOutput';
import { actions } from '../state/actions'; import { actions } from '../state/actions';
import { getAltSegmentsSelectables, getTagsSelectables, getTagsAsSegmentsSelectables } from '../state/providers';
import { GraphiteSegment } from '../types';
jest.mock('app/core/utils/promiseToDigest', () => ({ jest.mock('app/core/utils/promiseToDigest', () => ({
promiseToDigest: (scope: any) => { promiseToDigest: (scope: any) => {
@@ -105,8 +107,8 @@ describe('GraphiteQueryCtrl', () => {
describe('when middle segment value of test.prod.* is changed', () => { describe('when middle segment value of test.prod.* is changed', () => {
beforeEach(async () => { beforeEach(async () => {
const segment = { type: 'segment', value: 'test', expandable: true }; const segment: GraphiteSegment = { type: 'metric', value: 'test', expandable: true };
await ctx.ctrl.segmentValueChanged(segment, 1); await ctx.ctrl.dispatch(actions.segmentValueChanged({ segment: segment, index: 1 }));
}); });
it('should validate metric key exists', () => { it('should validate metric key exists', () => {
@@ -129,7 +131,7 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, 'test.prod.*.count'); 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', () => { it('should add function with correct node number', () => {
@@ -149,7 +151,7 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: true }]);
await changeTarget(ctx, ''); 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', () => { it('should add function and remove select metric link', () => {
@@ -191,7 +193,7 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([]);
await changeTarget(ctx, 'test.count'); 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', () => { 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 expect(async () => {
await ctx.ctrl.getAltSegments(0, 'any'); await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
expect(mockDispatch).toBeCalledWith( expect(mockDispatch).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'appNotifications/notifyApp', type: 'appNotifications/notifyApp',
@@ -221,11 +223,11 @@ describe('GraphiteQueryCtrl', () => {
}).not.toThrow(); }).not.toThrow();
}); });
it('getAltSegments should display the error message only once', async () => { it('getAltSegmentsSelectables should display the error message only once', async () => {
await ctx.ctrl.getAltSegments(0, 'any'); await getAltSegmentsSelectables(ctx.ctrl.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1); 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); 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 expect(async () => {
await ctx.ctrl.getTags(0, 'any'); await getTagsSelectables(ctx.ctrl.state, 0, 'any');
expect(mockDispatch).toBeCalledWith( expect(mockDispatch).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'appNotifications/notifyApp', type: 'appNotifications/notifyApp',
@@ -252,17 +254,17 @@ describe('GraphiteQueryCtrl', () => {
}).not.toThrow(); }).not.toThrow();
}); });
it('getTags should display the error message only once', async () => { it('getTagsSelectables should display the error message only once', async () => {
await ctx.ctrl.getTags(0, 'any'); await getTagsSelectables(ctx.ctrl.state, 0, 'any');
expect(mockDispatch.mock.calls.length).toBe(1); 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); 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 expect(async () => {
await ctx.ctrl.getTagsAsSegments('any'); await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
expect(mockDispatch).toBeCalledWith( expect(mockDispatch).toBeCalledWith(
expect.objectContaining({ expect.objectContaining({
type: 'appNotifications/notifyApp', type: 'appNotifications/notifyApp',
@@ -271,11 +273,11 @@ describe('GraphiteQueryCtrl', () => {
}).not.toThrow(); }).not.toThrow();
}); });
it('getTagsAsSegments should display the error message only once', async () => { it('getTagsAsSegmentsSelectables should display the error message only once', async () => {
await ctx.ctrl.getTagsAsSegments('any'); await getTagsAsSegmentsSelectables(ctx.ctrl.state, 'any');
expect(mockDispatch.mock.calls.length).toBe(1); 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); expect(mockDispatch.mock.calls.length).toBe(1);
}); });
}); });
@@ -350,7 +352,7 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, ''); await changeTarget(ctx, '');
await ctx.ctrl.addFunction(gfunc.getFuncDef('seriesByTag')); await ctx.ctrl.dispatch(actions.addFunction({ name: 'seriesByTag' }));
}); });
it('should update functions', () => { it('should update functions', () => {
@@ -393,7 +395,7 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, 'seriesByTag()'); 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', () => { it('should update tags with default value', () => {
@@ -411,7 +413,9 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')"); 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', () => { it('should update tags', () => {
@@ -432,7 +436,9 @@ describe('GraphiteQueryCtrl', () => {
beforeEach(async () => { beforeEach(async () => {
ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]); ctx.ctrl.state.datasource.metricFindQuery = () => Promise.resolve([{ expandable: false }]);
await changeTarget(ctx, "seriesByTag('tag1=value1', 'tag2!=~value2')"); 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', () => { it('should update tags', () => {

View File

@@ -12,7 +12,7 @@ import { FuncInstance } from '../gfunc';
const init = createAction<GraphiteQueryEditorAngularDependencies>('init'); const init = createAction<GraphiteQueryEditorAngularDependencies>('init');
// Metrics & Tags // Metrics & Tags
const segmentValueChanged = createAction<{ segment: GraphiteSegment; index: number }>('segment-value-changed'); const segmentValueChanged = createAction<{ segment: GraphiteSegment | string; index: number }>('segment-value-changed');
// Tags // Tags
const addNewTag = createAction<{ segment: GraphiteSegment }>('add-new-tag'); const addNewTag = createAction<{ segment: GraphiteSegment }>('add-new-tag');

View File

@@ -1,15 +1,16 @@
import { GraphiteQueryEditorState } from './store'; import { GraphiteQueryEditorState } from './store';
import { each, map } from 'lodash'; import { map } from 'lodash';
import { dispatch } from '../../../../store/store'; import { dispatch } from '../../../../store/store';
import { notifyApp } from '../../../../core/reducers/appNotification'; import { notifyApp } from '../../../../core/reducers/appNotification';
import { createErrorNotification } from '../../../../core/copy/appNotification'; import { createErrorNotification } from '../../../../core/copy/appNotification';
import { FuncInstance } from '../gfunc'; 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. * 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 * 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 { export function spliceSegments(state: GraphiteQueryEditorState, index: number): void {
state.segments = state.segments.splice(0, index); state.segments = state.segments.splice(0, index);
state.queryModel.segments = state.queryModel.segments.splice(0, index); state.queryModel.segments = state.queryModel.segments.splice(0, index);

View File

@@ -6,7 +6,9 @@ import {
handleMetricsAutoCompleteError, handleMetricsAutoCompleteError,
handleTagsAutoCompleteError, handleTagsAutoCompleteError,
} from './helpers'; } 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. * 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) * - mixed list of metrics and tags (only when nothing was selected)
* - list of metric names (if a metric name was selected for this segment) * - list of metric names (if a metric name was selected for this segment)
*/ */
export async function getAltSegments( async function getAltSegments(
state: GraphiteQueryEditorState, state: GraphiteQueryEditorState,
index: number, index: number,
prefix: string prefix: string
@@ -90,25 +92,29 @@ export async function getAltSegments(
return []; return [];
} }
export function getTagOperators(): AngularDropdownOptions[] { export async function getAltSegmentsSelectables(
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS); 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 * Returns tags as dropdown options
*/ */
export async function getTags( async function getTags(state: GraphiteQueryEditorState, index: number, tagPrefix: string): Promise<string[]> {
state: GraphiteQueryEditorState,
index: number,
tagPrefix: string
): Promise<AngularDropdownOptions[]> {
try { try {
const tagExpressions = state.queryModel.renderTagExpressions(index); const tagExpressions = state.queryModel.renderTagExpressions(index);
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix); const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
const altTags = map(values, 'text'); const altTags = map(values, 'text');
altTags.splice(0, 0, state.removeTagValue); altTags.splice(0, 0, state.removeTagValue);
return mapToDropdownOptions(altTags); return altTags;
} catch (err) { } catch (err) {
handleTagsAutoCompleteError(state, err); handleTagsAutoCompleteError(state, err);
} }
@@ -116,15 +122,20 @@ export async function getTags(
return []; 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. * List of tags when a tag is added. getTags is used for editing.
* When adding - segment is used. When editing - dropdown is used. * When adding - segment is used. When editing - dropdown is used.
*/ */
export async function getTagsAsSegments( async function getTagsAsSegments(state: GraphiteQueryEditorState, tagPrefix: string): Promise<GraphiteSegment[]> {
state: GraphiteQueryEditorState, let tagsAsSegments: GraphiteSegment[];
tagPrefix: string
): Promise<GraphiteSegment[]> {
let tagsAsSegments: GraphiteSegment[] = [];
try { try {
const tagExpressions = state.queryModel.renderTagExpressions(); const tagExpressions = state.queryModel.renderTagExpressions();
const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix); const values = await state.datasource.getTagsAutoComplete(tagExpressions, tagPrefix);
@@ -143,12 +154,19 @@ export async function getTagsAsSegments(
return tagsAsSegments; 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, state: GraphiteQueryEditorState,
tag: GraphiteTag, tag: GraphiteTag,
index: number, index: number,
valuePrefix: string valuePrefix: string
): Promise<AngularDropdownOptions[]> { ): Promise<string[]> {
const tagExpressions = state.queryModel.renderTagExpressions(index); const tagExpressions = state.queryModel.renderTagExpressions(index);
const tagKey = tag.key; const tagKey = tag.key;
const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {}); const values = await state.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix, {});
@@ -158,7 +176,16 @@ export async function getTagValues(
altValues.push('${' + variable.name + ':regex}'); 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[]) { function removeTaggedEntry(altSegments: GraphiteSegment[]) {
remove(altSegments, (s) => s.value === '_tagged'); remove(altSegments, (s) => s.value === '_tagged');
} }
function mapToDropdownOptions(results: string[]) {
return map(results, (value) => {
return { text: value, value: value };
});
}

View File

@@ -14,7 +14,6 @@ import {
parseTarget, parseTarget,
pause, pause,
removeTagPrefix, removeTagPrefix,
setSegmentFocus,
smartlyHandleNewAliasByNode, smartlyHandleNewAliasByNode,
spliceSegments, spliceSegments,
} from './helpers'; } from './helpers';
@@ -72,9 +71,22 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
await buildSegments(state, false); await buildSegments(state, false);
} }
if (actions.segmentValueChanged.match(action)) { 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.error = null;
state.segments[segmentIndex] = segment;
state.queryModel.updateSegmentValue(segment, segmentIndex); state.queryModel.updateSegmentValue(segment, segmentIndex);
if (state.queryModel.functions.length > 0 && state.queryModel.functions[0].def.fake) { 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; return state;
} }
// if newly selected segment can be expanded -> check if the path is correct
if (segment.expandable) { if (segment.expandable) {
await checkOtherSegments(state, segmentIndex + 1); await checkOtherSegments(state, segmentIndex + 1);
setSegmentFocus(state, segmentIndex + 1);
handleTargetChanged(state);
} else { } else {
// if not expandable -> remove all other segments
spliceSegments(state, segmentIndex + 1); spliceSegments(state, segmentIndex + 1);
} }
setSegmentFocus(state, segmentIndex + 1);
handleTargetChanged(state); handleTargetChanged(state);
} }
if (actions.tagChanged.match(action)) { if (actions.tagChanged.match(action)) {
const { tag, index: tagIndex } = action.payload; const { tag, index: tagIndex } = action.payload;
state.queryModel.updateTag(tag, tagIndex); state.queryModel.updateTag(tag, tagIndex);
handleTargetChanged(state); handleTargetChanged(state);
if (state.queryModel.tags.length === 0) {
await checkOtherSegments(state, 0);
state.paused = false;
}
} }
if (actions.addNewTag.match(action)) { if (actions.addNewTag.match(action)) {
const segment = action.payload.segment; const segment = action.payload.segment;

View File

@@ -60,7 +60,6 @@ export type GraphiteSegment = {
value: string; value: string;
type?: 'tag' | 'metric' | 'series-ref'; type?: 'tag' | 'metric' | 'series-ref';
expandable?: boolean; expandable?: boolean;
focus?: boolean;
fake?: boolean; fake?: boolean;
}; };