Graphite: Migrate to React (part 2: migrate smaller AngularJS directives) (#36797)

* 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

* Simplify setting Segment Select min width

* Extract view logic to a helper and update types definitions

* Clean up types

* Update FuncDef types and add tests

Co-authored-by: Giordano Ricci <me@giordanoricci.com>
This commit is contained in:
Piotr Jamróz 2021-07-21 20:09:00 +02:00 committed by GitHub
parent 1d37d675d7
commit 0bf1a97262
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 234 additions and 239 deletions

View File

@ -22,9 +22,11 @@ export function Segment<T>({
allowCustomValue,
placeholder,
disabled,
inputMinWidth,
...rest
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false);
const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth;
const styles = useStyles(getSegmentStyles);
if (!expanded) {

View File

@ -26,11 +26,13 @@ export function SegmentAsync<T>({
allowCustomValue,
disabled,
placeholder,
inputMinWidth,
noOptionMessageHandler = mapStateToNoOptionsMessage,
...rest
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
const [state, fetchOptions] = useAsyncFn(loadOptions, [loadOptions]);
const [Label, width, expanded, setExpanded] = useExpandableLabel(false);
const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false);
const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth;
const styles = useStyles(getSegmentStyles);
if (!expanded) {

View File

@ -6,4 +6,5 @@ export interface SegmentProps<T> {
allowCustomValue?: boolean;
placeholder?: string;
disabled?: boolean;
inputMinWidth?: number;
}

View File

@ -25,6 +25,9 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker';
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
import { GraphiteTextEditor } from '../plugins/datasource/graphite/components/GraphiteTextEditor';
import { PlayButton } from '../plugins/datasource/graphite/components/PlayButton';
import { AddGraphiteFunction } from '../plugins/datasource/graphite/components/AddGraphiteFunction';
const { SecretFormField } = LegacyForms;
@ -38,7 +41,6 @@ export function registerAngularDirectives() {
]);
react2AngularDirective('spinner', Spinner, ['inline']);
react2AngularDirective('helpModal', HelpModal, []);
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
react2AngularDirective('emptyListCta', EmptyListCTA, [
'title',
@ -201,4 +203,10 @@ export function registerAngularDirectives() {
['datasource', { watchDepth: 'reference' }],
'onChange',
]);
// Temporal wrappers for Graphite migration
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
react2AngularDirective('graphiteTextEditor', GraphiteTextEditor, ['rawQuery', 'dispatch']);
react2AngularDirective('playButton', PlayButton, ['dispatch']);
react2AngularDirective('addGraphiteFunction', AddGraphiteFunction, ['funcDefs', 'dispatch']);
}

View File

@ -1,28 +1,25 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { FunctionEditor } from './FunctionEditor';
import { FunctionDescriptor } from './FunctionEditorControls';
import { FuncInstance } from './gfunc';
function mockFunctionDescriptor(name: string, unknown?: boolean): FunctionDescriptor {
return {
text: '',
function mockFunctionInstance(name: string, unknown?: boolean): FuncInstance {
const def = {
category: 'category',
defaultParams: [],
fake: false,
name: name,
params: [],
def: {
category: 'category',
defaultParams: [],
fake: false,
name: name,
params: [],
unknown: unknown,
},
unknown: unknown,
};
return new FuncInstance(def);
}
describe('FunctionEditor', () => {
it('should display a defined function with name and no icon', () => {
render(
<FunctionEditor
func={mockFunctionDescriptor('foo')}
func={mockFunctionInstance('foo')}
onMoveLeft={() => {}}
onMoveRight={() => {}}
onRemove={() => {}}
@ -36,7 +33,7 @@ describe('FunctionEditor', () => {
it('should display an unknown function with name and warning icon', () => {
render(
<FunctionEditor
func={mockFunctionDescriptor('bar', true)}
func={mockFunctionInstance('bar', true)}
onMoveLeft={jest.fn()}
onMoveRight={jest.fn()}
onRemove={jest.fn()}

View File

@ -1,10 +1,11 @@
import React, { useRef } from 'react';
import { PopoverController, Popover, ClickOutsideWrapper, Icon, Tooltip, useTheme } from '@grafana/ui';
import { FunctionDescriptor, FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls';
import { FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls';
import { FuncInstance } from './gfunc';
import { css } from '@emotion/css';
interface FunctionEditorProps extends FunctionEditorControlsProps {
func: FunctionDescriptor;
func: FuncInstance;
}
const FunctionEditor: React.FC<FunctionEditorProps> = ({ onMoveLeft, onMoveRight, func, ...props }) => {

View File

@ -1,27 +1,11 @@
import React, { Suspense } from 'react';
import { Icon, Tooltip } from '@grafana/ui';
export interface FunctionDescriptor {
text: string;
params: string[];
def: {
category: string;
defaultParams: string[];
description?: string;
fake: boolean;
name: string;
params: string[];
/**
* True if the function was not found on the list of available function descriptions.
*/
unknown?: boolean;
};
}
import { FuncInstance } from './gfunc';
export interface FunctionEditorControlsProps {
onMoveLeft: (func: FunctionDescriptor) => void;
onMoveRight: (func: FunctionDescriptor) => void;
onRemove: (func: FunctionDescriptor) => void;
onMoveLeft: (func: FuncInstance) => void;
onMoveRight: (func: FuncInstance) => void;
onRemove: (func: FuncInstance) => void;
}
const FunctionDescription = React.lazy(async () => {
@ -64,7 +48,7 @@ const FunctionHelpButton = (props: { description?: string; name: string }) => {
export const FunctionEditorControls = (
props: FunctionEditorControlsProps & {
func: FunctionDescriptor;
func: FuncInstance;
}
) => {
const { func, onMoveLeft, onMoveRight, onRemove } = props;

View File

@ -1,164 +0,0 @@
import { map, find, forEach, sortBy } from 'lodash';
import $ from 'jquery';
// @ts-ignore
import Drop from 'tether-drop';
import coreModule from 'app/core/core_module';
import { FuncDef } from './gfunc';
/** @ngInject */
export function graphiteAddFunc($compile: any) {
const inputTemplate =
'<input type="text"' + ' class="gf-form-input"' + ' spellcheck="false" style="display:none"></input>';
const buttonTemplate =
'<a class="gf-form-label dropdown-toggle"' +
' tabindex="1" gf-dropdown="functionMenu" data-toggle="dropdown">' +
'<icon name="\'plus\'" size="\'sm\'"></name></a>';
return {
link: function ($scope: any, elem: JQuery) {
const ctrl = $scope.ctrl;
const $input = $(inputTemplate);
const $button = $(buttonTemplate);
$input.appendTo(elem);
$button.appendTo(elem);
// TODO: ctrl.state is not ready yet when link() is called. This will be moved to a separate provider.
ctrl.datasource.getFuncDefs().then((funcDefs: FuncDef[]) => {
const allFunctions = map(funcDefs, 'name').sort();
$scope.functionMenu = createFunctionDropDownMenu(funcDefs);
$input.attr('data-provide', 'typeahead');
$input.typeahead({
source: allFunctions,
minLength: 1,
items: 10,
updater: (value: any) => {
let funcDef: any = ctrl.state.datasource.getFuncDef(value);
if (!funcDef) {
// try find close match
value = value.toLowerCase();
funcDef = find(allFunctions, (funcName) => {
return funcName.toLowerCase().indexOf(value) === 0;
});
if (!funcDef) {
return '';
}
}
$scope.$apply(() => {
ctrl.addFunction(funcDef);
});
$input.trigger('blur');
return '';
},
});
$button.click(() => {
$button.hide();
$input.show();
$input.focus();
});
$input.keyup(() => {
elem.toggleClass('open', $input.val() === '');
});
$input.blur(() => {
// clicking the function dropdown menu won't
// work if you remove class at once
setTimeout(() => {
$input.val('');
$input.hide();
$button.show();
elem.removeClass('open');
}, 200);
});
$compile(elem.contents())($scope);
});
let drop: any;
const cleanUpDrop = () => {
if (drop) {
drop.destroy();
drop = null;
}
};
$(elem)
.on('mouseenter', 'ul.dropdown-menu li', async () => {
cleanUpDrop();
let funcDef;
try {
funcDef = ctrl.state.datasource.getFuncDef($('a', this).text());
} catch (e) {
// ignore
}
if (funcDef && funcDef.description) {
let shortDesc = funcDef.description;
if (shortDesc.length > 500) {
shortDesc = shortDesc.substring(0, 497) + '...';
}
const contentElement = document.createElement('div');
// @ts-ignore
const { default: rst2html } = await import(/* webpackChunkName: "rst2html" */ 'rst2html');
contentElement.innerHTML = '<h4>' + funcDef.name + '</h4>' + rst2html(shortDesc);
drop = new Drop({
target: this,
content: contentElement,
classes: 'drop-popover',
openOn: 'always',
tetherOptions: {
attachment: 'bottom left',
targetAttachment: 'bottom right',
},
});
}
})
.on('mouseout', 'ul.dropdown-menu li', () => {
cleanUpDrop();
});
$scope.$on('$destroy', cleanUpDrop);
},
};
}
coreModule.directive('graphiteAddFunc', graphiteAddFunc);
function createFunctionDropDownMenu(funcDefs: FuncDef[]) {
const categories: any = {};
forEach(funcDefs, (funcDef) => {
if (!funcDef.category) {
return;
}
if (!categories[funcDef.category]) {
categories[funcDef.category] = [];
}
categories[funcDef.category].push({
text: funcDef.name,
click: "ctrl.addFunction('" + funcDef.name + "')",
});
});
return sortBy(
map(categories, (submenu, category) => {
return {
text: category,
submenu: sortBy(submenu, 'text'),
};
}),
'text'
);
}

View File

@ -0,0 +1,42 @@
import React, { useCallback, useMemo } from 'react';
import { Button, Segment, useStyles2 } from '@grafana/ui';
import { FuncDefs } from '../gfunc';
import { actions } from '../state/actions';
import { GrafanaTheme2 } from '@grafana/data';
import { css, cx } from '@emotion/css';
import { mapFuncDefsToSelectables } from './helpers';
import { Dispatch } from 'redux';
type Props = {
dispatch: Dispatch;
funcDefs: FuncDefs;
};
export function AddGraphiteFunction({ dispatch, funcDefs }: Props) {
const onChange = useCallback(
({ value }) => {
dispatch(actions.addFunction({ name: value }));
},
[dispatch]
);
const styles = useStyles2(getStyles);
const options = useMemo(() => mapFuncDefsToSelectables(funcDefs), [funcDefs]);
return (
<Segment
Component={<Button icon="plus" variant="secondary" className={cx(styles.button)} />}
options={options}
onChange={onChange}
inputMinWidth={150}
></Segment>
);
}
function getStyles(theme: GrafanaTheme2) {
return {
button: css`
margin-right: ${theme.spacing(0.5)};
`,
};
}

View File

@ -0,0 +1,30 @@
import React, { useCallback, useState } from 'react';
import { QueryField } from '@grafana/ui';
import { actions } from '../state/actions';
import { Dispatch } from 'redux';
type Props = {
rawQuery: string;
dispatch: Dispatch;
};
export function GraphiteTextEditor({ rawQuery, dispatch }: Props) {
const [currentQuery, updateCurrentQuery] = useState<string>(rawQuery);
const applyChanges = useCallback(() => {
dispatch(actions.updateQuery({ query: currentQuery }));
}, [dispatch, currentQuery]);
return (
<>
<QueryField
query={rawQuery}
onChange={updateCurrentQuery}
onBlur={applyChanges}
onRunQuery={applyChanges}
placeholder={'Enter a Graphite query (run with Shift+Enter)'}
portalOrigin="graphite"
/>
</>
);
}

View File

@ -0,0 +1,16 @@
import React, { useCallback } from 'react';
import { Button } from '@grafana/ui';
import { actions } from '../state/actions';
import { Dispatch } from 'redux';
type Props = {
rawQuery: string;
dispatch: Dispatch;
};
export function PlayButton({ dispatch }: Props) {
const onClick = useCallback(() => {
dispatch(actions.unpause());
}, [dispatch]);
return <Button icon="play" onClick={onClick} type="button" variant="secondary" />;
}

View File

@ -0,0 +1,31 @@
import { mapFuncDefsToSelectables } from './helpers';
import { FuncDefs } from '../gfunc';
describe('Graphite components helpers', () => {
it('converts function definitions to selectable options', function () {
const functionDefs: FuncDefs = {
functionA1: { name: 'functionA1', category: 'A', params: [], defaultParams: [] },
functionB1: { name: 'functionB1', category: 'B', params: [], defaultParams: [] },
functionA2: { name: 'functionA2', category: 'A', params: [], defaultParams: [] },
functionB2: { name: 'functionB2', category: 'B', params: [], defaultParams: [] },
};
const options = mapFuncDefsToSelectables(functionDefs);
expect(options).toMatchObject([
{
label: 'A',
options: [
{ label: 'functionA1', value: 'functionA1' },
{ label: 'functionA2', value: 'functionA2' },
],
},
{
label: 'B',
options: [
{ label: 'functionB1', value: 'functionB1' },
{ label: 'functionB2', value: 'functionB2' },
],
},
]);
});
});

View File

@ -0,0 +1,22 @@
import { FuncDefs } from '../gfunc';
import { forEach, sortBy } from 'lodash';
import { SelectableValue } from '@grafana/data';
export function mapFuncDefsToSelectables(funcDefs: FuncDefs): Array<SelectableValue<string>> {
const categories: any = {};
forEach(funcDefs, (funcDef) => {
if (!funcDef.category) {
return;
}
if (!categories[funcDef.category]) {
categories[funcDef.category] = { label: funcDef.category, value: funcDef.category, options: [] };
}
categories[funcDef.category].options.push({
label: funcDef.name,
value: funcDef.name,
});
});
return sortBy(categories, 'label');
}

View File

@ -5,23 +5,23 @@ import {
DataQueryResponse,
DataSourceApi,
dateMath,
MetricFindValue,
QueryResultMetaStat,
ScopedVars,
toDataFrame,
TimeRange,
MetricFindValue,
toDataFrame,
} from '@grafana/data';
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
import gfunc from './gfunc';
import gfunc, { FuncDefs, FuncInstance } from './gfunc';
import { getBackendSrv } from '@grafana/runtime';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
// Types
import {
GraphiteLokiMapping,
GraphiteOptions,
GraphiteQuery,
GraphiteQueryImportConfiguration,
GraphiteType,
GraphiteLokiMapping,
MetricTankRequestMeta,
} from './types';
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
@ -45,7 +45,7 @@ export class GraphiteDatasource extends DataSourceApi<
rollupIndicatorEnabled: boolean;
cacheTimeout: any;
withCredentials: boolean;
funcDefs: any = null;
funcDefs: FuncDefs | null = null;
funcDefsPromise: Promise<any> | null = null;
_seriesRefLetters: string;
private readonly metricMappings: GraphiteLokiMapping[];
@ -636,7 +636,7 @@ export class GraphiteDatasource extends DataSourceApi<
.toPromise();
}
createFuncInstance(funcDef: any, options?: any) {
createFuncInstance(funcDef: any, options?: any): FuncInstance {
return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
}

View File

@ -13,6 +13,11 @@ describe('gfunc', () => {
});
it('marks function as unknown when it is not available in the index', () => {
expect(gfunc.getFuncDef('bar', INDEX)).toEqual({ name: 'bar', params: [{ multiple: true }], unknown: true });
expect(gfunc.getFuncDef('bar', INDEX)).toEqual({
name: 'bar',
params: [{ name: '', type: '', multiple: true }],
defaultParams: [''],
unknown: true,
});
});
});

View File

@ -2,26 +2,43 @@ import { assign, each, filter, forEach, get, includes, isString, last, map, toSt
import { isVersionGtOrEq } from 'app/core/utils/version';
import { InterpolateFunction } from '@grafana/data';
const index: any = {};
type ParamDef = {
name: string;
type: string;
options?: Array<string | number>;
multiple?: boolean;
optional?: boolean;
version?: string;
};
export interface FuncDef {
name: any;
name: string;
params: ParamDef[];
defaultParams: Array<string | number>;
category?: string;
params?: any;
defaultParams?: any;
shortName?: any;
fake?: boolean;
version?: string;
description?: string;
/**
* True if the function was not found on the list of available function descriptions.
*/
unknown?: boolean;
}
function addFuncDef(funcDef: FuncDef) {
export type FuncDefs = {
[functionName in string]: FuncDef;
};
const index: FuncDefs = {};
function addFuncDef(funcDef: Partial<FuncDef> & { name: string; category: string }) {
funcDef.params = funcDef.params || [];
funcDef.defaultParams = funcDef.defaultParams || [];
index[funcDef.name] = funcDef;
index[funcDef.name] = funcDef as FuncDef;
if (funcDef.shortName) {
index[funcDef.shortName] = funcDef;
index[funcDef.shortName] = funcDef as FuncDef;
}
}
@ -965,13 +982,13 @@ addFuncDef({
version: '1.1',
});
function isVersionRelatedFunction(obj: { version: string }, graphiteVersion: string) {
function isVersionRelatedFunction(obj: { version?: string }, graphiteVersion: string) {
return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version);
}
export class FuncInstance {
def: any;
params: any;
def: FuncDef;
params: Array<string | number>;
text: any;
added: boolean;
/**
@ -982,11 +999,11 @@ export class FuncInstance {
*/
hidden?: boolean;
constructor(funcDef: any, options?: { withDefaultParams: any }) {
constructor(funcDef: FuncDef, options?: { withDefaultParams: any }) {
this.def = funcDef;
this.params = [];
if (options && options.withDefaultParams) {
if (options && options.withDefaultParams && funcDef.defaultParams) {
this.params = funcDef.defaultParams.slice(0);
}
@ -1081,23 +1098,23 @@ export class FuncInstance {
}
}
function createFuncInstance(funcDef: any, options?: { withDefaultParams: any }, idx?: any) {
function createFuncInstance(funcDef: any, options?: { withDefaultParams: any }, idx?: any): FuncInstance {
if (isString(funcDef)) {
funcDef = getFuncDef(funcDef, idx);
}
return new FuncInstance(funcDef, options);
}
function getFuncDef(name: string, idx?: any) {
function getFuncDef(name: string, idx?: any): FuncDef {
if (!(idx || index)[name]) {
return { name: name, params: [{ multiple: true }], unknown: true };
return { name: name, params: [{ name: '', type: '', multiple: true }], defaultParams: [''], unknown: true };
}
return (idx || index)[name];
}
function getFuncDefs(graphiteVersion: string, idx?: any) {
const funcs: any = {};
forEach(idx || index, (funcDef) => {
function getFuncDefs(graphiteVersion: string, idx?: any): FuncDefs {
const funcs: FuncDefs = {};
forEach(idx || index, (funcDef: FuncDef) => {
if (isVersionRelatedFunction(funcDef, graphiteVersion)) {
funcs[funcDef.name] = assign({}, funcDef, {
params: filter(funcDef.params, (param) => {
@ -1110,8 +1127,8 @@ function getFuncDefs(graphiteVersion: string, idx?: any) {
}
// parse response from graphite /functions endpoint into internal format
function parseFuncDefs(rawDefs: any) {
const funcDefs: any = {};
function parseFuncDefs(rawDefs: any): FuncDefs {
const funcDefs: FuncDefs = {};
forEach(rawDefs || {}, (funcDef, funcName) => {
// skip graphite graph functions

View File

@ -1,7 +1,7 @@
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
<div class="gf-form" ng-show="ctrl.state.target.textEditor">
<input type="text" class="gf-form-input" style="font-family: monospace;" ng-value="ctrl.state.target.target" spellcheck="false" ng-blur="ctrl.targetTextChanged($event)"></input>
<graphite-text-editor style="width: 100%" rawQuery="ctrl.state.target.target" dispatch="ctrl.dispatch"></graphite-text-editor>
</div>
<div ng-hide="ctrl.target.textEditor">
@ -52,7 +52,7 @@
</div>
<div ng-if="ctrl.state.paused" class="gf-form">
<a ng-click="ctrl.unpause()" class="gf-form-label query-part"><icon name="'play'"></icon></a>
<play-button dispatch="ctrl.dispatch" />
</div>
<div class="gf-form gf-form--grow">
@ -70,7 +70,7 @@
</div>
<div class="gf-form dropdown">
<span graphite-add-func></span>
<add-graphite-function funcDefs="ctrl.state.funcDefs" dispatch="ctrl.dispatch" />
</div>
<div class="gf-form gf-form--grow">

View File

@ -1,4 +1,3 @@
import './add_graphite_func';
import './func_editor';
import GraphiteQuery from './graphite_query';

View File

@ -3,6 +3,7 @@ import { each, map } from 'lodash';
import { dispatch } from '../../../../store/store';
import { notifyApp } from '../../../../core/reducers/appNotification';
import { createErrorNotification } from '../../../../core/copy/appNotification';
import { FuncInstance } from '../gfunc';
/**
* Helpers used by reducers and providers. They modify state object directly so should operate on a copy of the state.
@ -134,10 +135,7 @@ export async function addSeriesByTagFunc(state: GraphiteQueryEditorState, tag: s
await parseTarget(state);
}
export function smartlyHandleNewAliasByNode(
state: GraphiteQueryEditorState,
func: { def: { name: string }; params: number[]; added: boolean }
): void {
export function smartlyHandleNewAliasByNode(state: GraphiteQueryEditorState, func: FuncInstance): void {
if (func.def.name !== 'aliasByNode') {
return;
}

View File

@ -19,6 +19,7 @@ import {
spliceSegments,
} from './helpers';
import { Action } from 'redux';
import { FuncDefs } from '../gfunc';
export type GraphiteQueryEditorState = {
/**
@ -38,6 +39,8 @@ export type GraphiteQueryEditorState = {
target: { target: string; textEditor: boolean };
funcDefs: FuncDefs | null;
segments: GraphiteSegment[];
queryModel: GraphiteQuery;
@ -63,6 +66,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
supportsTags: deps.datasource.supportsTags,
paused: false,
removeTagValue: '-- remove tag --',
funcDefs: deps.datasource.funcDefs,
};
await buildSegments(state, false);