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:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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']);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 { 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 = {};
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user