mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 10:20:29 -06:00
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:
parent
1d37d675d7
commit
0bf1a97262
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -6,4 +6,5 @@ export interface SegmentProps<T> {
|
||||
allowCustomValue?: boolean;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
inputMinWidth?: number;
|
||||
}
|
||||
|
@ -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']);
|
||||
}
|
||||
|
@ -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()}
|
||||
|
@ -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 }) => {
|
||||
|
@ -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;
|
||||
|
@ -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'
|
||||
);
|
||||
}
|
@ -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)};
|
||||
`,
|
||||
};
|
||||
}
|
@ -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"
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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" />;
|
||||
}
|
@ -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' },
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
@ -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');
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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">
|
||||
|
@ -1,4 +1,3 @@
|
||||
import './add_graphite_func';
|
||||
import './func_editor';
|
||||
|
||||
import GraphiteQuery from './graphite_query';
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user