mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -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,
|
allowCustomValue,
|
||||||
placeholder,
|
placeholder,
|
||||||
disabled,
|
disabled,
|
||||||
|
inputMinWidth,
|
||||||
...rest
|
...rest
|
||||||
}: React.PropsWithChildren<SegmentSyncProps<T>>) {
|
}: 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);
|
const styles = useStyles(getSegmentStyles);
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
|
@ -26,11 +26,13 @@ export function SegmentAsync<T>({
|
|||||||
allowCustomValue,
|
allowCustomValue,
|
||||||
disabled,
|
disabled,
|
||||||
placeholder,
|
placeholder,
|
||||||
|
inputMinWidth,
|
||||||
noOptionMessageHandler = mapStateToNoOptionsMessage,
|
noOptionMessageHandler = mapStateToNoOptionsMessage,
|
||||||
...rest
|
...rest
|
||||||
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
|
}: React.PropsWithChildren<SegmentAsyncProps<T>>) {
|
||||||
const [state, fetchOptions] = useAsyncFn(loadOptions, [loadOptions]);
|
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);
|
const styles = useStyles(getSegmentStyles);
|
||||||
|
|
||||||
if (!expanded) {
|
if (!expanded) {
|
||||||
|
@ -6,4 +6,5 @@ export interface SegmentProps<T> {
|
|||||||
allowCustomValue?: boolean;
|
allowCustomValue?: boolean;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
inputMinWidth?: number;
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,9 @@ import { FolderPicker } from 'app/core/components/Select/FolderPicker';
|
|||||||
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
import { SearchField, SearchResults, SearchResultsFilter } from '../features/search';
|
||||||
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
import { TimePickerSettings } from 'app/features/dashboard/components/DashboardSettings/TimePickerSettings';
|
||||||
import QueryEditor from 'app/plugins/datasource/grafana-azure-monitor-datasource/components/QueryEditor/QueryEditor';
|
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;
|
const { SecretFormField } = LegacyForms;
|
||||||
|
|
||||||
@ -38,7 +41,6 @@ export function registerAngularDirectives() {
|
|||||||
]);
|
]);
|
||||||
react2AngularDirective('spinner', Spinner, ['inline']);
|
react2AngularDirective('spinner', Spinner, ['inline']);
|
||||||
react2AngularDirective('helpModal', HelpModal, []);
|
react2AngularDirective('helpModal', HelpModal, []);
|
||||||
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, [
|
react2AngularDirective('emptyListCta', EmptyListCTA, [
|
||||||
'title',
|
'title',
|
||||||
@ -201,4 +203,10 @@ export function registerAngularDirectives() {
|
|||||||
['datasource', { watchDepth: 'reference' }],
|
['datasource', { watchDepth: 'reference' }],
|
||||||
'onChange',
|
'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 React from 'react';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import { FunctionEditor } from './FunctionEditor';
|
import { FunctionEditor } from './FunctionEditor';
|
||||||
import { FunctionDescriptor } from './FunctionEditorControls';
|
import { FuncInstance } from './gfunc';
|
||||||
|
|
||||||
function mockFunctionDescriptor(name: string, unknown?: boolean): FunctionDescriptor {
|
function mockFunctionInstance(name: string, unknown?: boolean): FuncInstance {
|
||||||
return {
|
const def = {
|
||||||
text: '',
|
category: 'category',
|
||||||
|
defaultParams: [],
|
||||||
|
fake: false,
|
||||||
|
name: name,
|
||||||
params: [],
|
params: [],
|
||||||
def: {
|
unknown: unknown,
|
||||||
category: 'category',
|
|
||||||
defaultParams: [],
|
|
||||||
fake: false,
|
|
||||||
name: name,
|
|
||||||
params: [],
|
|
||||||
unknown: unknown,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
return new FuncInstance(def);
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('FunctionEditor', () => {
|
describe('FunctionEditor', () => {
|
||||||
it('should display a defined function with name and no icon', () => {
|
it('should display a defined function with name and no icon', () => {
|
||||||
render(
|
render(
|
||||||
<FunctionEditor
|
<FunctionEditor
|
||||||
func={mockFunctionDescriptor('foo')}
|
func={mockFunctionInstance('foo')}
|
||||||
onMoveLeft={() => {}}
|
onMoveLeft={() => {}}
|
||||||
onMoveRight={() => {}}
|
onMoveRight={() => {}}
|
||||||
onRemove={() => {}}
|
onRemove={() => {}}
|
||||||
@ -36,7 +33,7 @@ describe('FunctionEditor', () => {
|
|||||||
it('should display an unknown function with name and warning icon', () => {
|
it('should display an unknown function with name and warning icon', () => {
|
||||||
render(
|
render(
|
||||||
<FunctionEditor
|
<FunctionEditor
|
||||||
func={mockFunctionDescriptor('bar', true)}
|
func={mockFunctionInstance('bar', true)}
|
||||||
onMoveLeft={jest.fn()}
|
onMoveLeft={jest.fn()}
|
||||||
onMoveRight={jest.fn()}
|
onMoveRight={jest.fn()}
|
||||||
onRemove={jest.fn()}
|
onRemove={jest.fn()}
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import React, { useRef } from 'react';
|
import React, { useRef } from 'react';
|
||||||
import { PopoverController, Popover, ClickOutsideWrapper, Icon, Tooltip, useTheme } from '@grafana/ui';
|
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';
|
import { css } from '@emotion/css';
|
||||||
|
|
||||||
interface FunctionEditorProps extends FunctionEditorControlsProps {
|
interface FunctionEditorProps extends FunctionEditorControlsProps {
|
||||||
func: FunctionDescriptor;
|
func: FuncInstance;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FunctionEditor: React.FC<FunctionEditorProps> = ({ onMoveLeft, onMoveRight, func, ...props }) => {
|
const FunctionEditor: React.FC<FunctionEditorProps> = ({ onMoveLeft, onMoveRight, func, ...props }) => {
|
||||||
|
@ -1,27 +1,11 @@
|
|||||||
import React, { Suspense } from 'react';
|
import React, { Suspense } from 'react';
|
||||||
import { Icon, Tooltip } from '@grafana/ui';
|
import { Icon, Tooltip } from '@grafana/ui';
|
||||||
|
import { FuncInstance } from './gfunc';
|
||||||
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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FunctionEditorControlsProps {
|
export interface FunctionEditorControlsProps {
|
||||||
onMoveLeft: (func: FunctionDescriptor) => void;
|
onMoveLeft: (func: FuncInstance) => void;
|
||||||
onMoveRight: (func: FunctionDescriptor) => void;
|
onMoveRight: (func: FuncInstance) => void;
|
||||||
onRemove: (func: FunctionDescriptor) => void;
|
onRemove: (func: FuncInstance) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const FunctionDescription = React.lazy(async () => {
|
const FunctionDescription = React.lazy(async () => {
|
||||||
@ -64,7 +48,7 @@ const FunctionHelpButton = (props: { description?: string; name: string }) => {
|
|||||||
|
|
||||||
export const FunctionEditorControls = (
|
export const FunctionEditorControls = (
|
||||||
props: FunctionEditorControlsProps & {
|
props: FunctionEditorControlsProps & {
|
||||||
func: FunctionDescriptor;
|
func: FuncInstance;
|
||||||
}
|
}
|
||||||
) => {
|
) => {
|
||||||
const { func, onMoveLeft, onMoveRight, onRemove } = props;
|
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,
|
DataQueryResponse,
|
||||||
DataSourceApi,
|
DataSourceApi,
|
||||||
dateMath,
|
dateMath,
|
||||||
|
MetricFindValue,
|
||||||
QueryResultMetaStat,
|
QueryResultMetaStat,
|
||||||
ScopedVars,
|
ScopedVars,
|
||||||
toDataFrame,
|
|
||||||
TimeRange,
|
TimeRange,
|
||||||
MetricFindValue,
|
toDataFrame,
|
||||||
} from '@grafana/data';
|
} from '@grafana/data';
|
||||||
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
import { isVersionGtOrEq, SemVersion } from 'app/core/utils/version';
|
||||||
import gfunc from './gfunc';
|
import gfunc, { FuncDefs, FuncInstance } from './gfunc';
|
||||||
import { getBackendSrv } from '@grafana/runtime';
|
import { getBackendSrv } from '@grafana/runtime';
|
||||||
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
// Types
|
// Types
|
||||||
import {
|
import {
|
||||||
|
GraphiteLokiMapping,
|
||||||
GraphiteOptions,
|
GraphiteOptions,
|
||||||
GraphiteQuery,
|
GraphiteQuery,
|
||||||
GraphiteQueryImportConfiguration,
|
GraphiteQueryImportConfiguration,
|
||||||
GraphiteType,
|
GraphiteType,
|
||||||
GraphiteLokiMapping,
|
|
||||||
MetricTankRequestMeta,
|
MetricTankRequestMeta,
|
||||||
} from './types';
|
} from './types';
|
||||||
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
import { getRollupNotice, getRuntimeConsolidationNotice } from 'app/plugins/datasource/graphite/meta';
|
||||||
@ -45,7 +45,7 @@ export class GraphiteDatasource extends DataSourceApi<
|
|||||||
rollupIndicatorEnabled: boolean;
|
rollupIndicatorEnabled: boolean;
|
||||||
cacheTimeout: any;
|
cacheTimeout: any;
|
||||||
withCredentials: boolean;
|
withCredentials: boolean;
|
||||||
funcDefs: any = null;
|
funcDefs: FuncDefs | null = null;
|
||||||
funcDefsPromise: Promise<any> | null = null;
|
funcDefsPromise: Promise<any> | null = null;
|
||||||
_seriesRefLetters: string;
|
_seriesRefLetters: string;
|
||||||
private readonly metricMappings: GraphiteLokiMapping[];
|
private readonly metricMappings: GraphiteLokiMapping[];
|
||||||
@ -636,7 +636,7 @@ export class GraphiteDatasource extends DataSourceApi<
|
|||||||
.toPromise();
|
.toPromise();
|
||||||
}
|
}
|
||||||
|
|
||||||
createFuncInstance(funcDef: any, options?: any) {
|
createFuncInstance(funcDef: any, options?: any): FuncInstance {
|
||||||
return gfunc.createFuncInstance(funcDef, options, this.funcDefs);
|
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', () => {
|
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 { isVersionGtOrEq } from 'app/core/utils/version';
|
||||||
import { InterpolateFunction } from '@grafana/data';
|
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 {
|
export interface FuncDef {
|
||||||
name: any;
|
name: string;
|
||||||
|
params: ParamDef[];
|
||||||
|
defaultParams: Array<string | number>;
|
||||||
category?: string;
|
category?: string;
|
||||||
params?: any;
|
|
||||||
defaultParams?: any;
|
|
||||||
shortName?: any;
|
shortName?: any;
|
||||||
fake?: boolean;
|
fake?: boolean;
|
||||||
version?: string;
|
version?: string;
|
||||||
description?: 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.params = funcDef.params || [];
|
||||||
funcDef.defaultParams = funcDef.defaultParams || [];
|
funcDef.defaultParams = funcDef.defaultParams || [];
|
||||||
|
|
||||||
index[funcDef.name] = funcDef;
|
index[funcDef.name] = funcDef as FuncDef;
|
||||||
if (funcDef.shortName) {
|
if (funcDef.shortName) {
|
||||||
index[funcDef.shortName] = funcDef;
|
index[funcDef.shortName] = funcDef as FuncDef;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -965,13 +982,13 @@ addFuncDef({
|
|||||||
version: '1.1',
|
version: '1.1',
|
||||||
});
|
});
|
||||||
|
|
||||||
function isVersionRelatedFunction(obj: { version: string }, graphiteVersion: string) {
|
function isVersionRelatedFunction(obj: { version?: string }, graphiteVersion: string) {
|
||||||
return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version);
|
return !obj.version || isVersionGtOrEq(graphiteVersion, obj.version);
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FuncInstance {
|
export class FuncInstance {
|
||||||
def: any;
|
def: FuncDef;
|
||||||
params: any;
|
params: Array<string | number>;
|
||||||
text: any;
|
text: any;
|
||||||
added: boolean;
|
added: boolean;
|
||||||
/**
|
/**
|
||||||
@ -982,11 +999,11 @@ export class FuncInstance {
|
|||||||
*/
|
*/
|
||||||
hidden?: boolean;
|
hidden?: boolean;
|
||||||
|
|
||||||
constructor(funcDef: any, options?: { withDefaultParams: any }) {
|
constructor(funcDef: FuncDef, options?: { withDefaultParams: any }) {
|
||||||
this.def = funcDef;
|
this.def = funcDef;
|
||||||
this.params = [];
|
this.params = [];
|
||||||
|
|
||||||
if (options && options.withDefaultParams) {
|
if (options && options.withDefaultParams && funcDef.defaultParams) {
|
||||||
this.params = funcDef.defaultParams.slice(0);
|
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)) {
|
if (isString(funcDef)) {
|
||||||
funcDef = getFuncDef(funcDef, idx);
|
funcDef = getFuncDef(funcDef, idx);
|
||||||
}
|
}
|
||||||
return new FuncInstance(funcDef, options);
|
return new FuncInstance(funcDef, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFuncDef(name: string, idx?: any) {
|
function getFuncDef(name: string, idx?: any): FuncDef {
|
||||||
if (!(idx || index)[name]) {
|
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];
|
return (idx || index)[name];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getFuncDefs(graphiteVersion: string, idx?: any) {
|
function getFuncDefs(graphiteVersion: string, idx?: any): FuncDefs {
|
||||||
const funcs: any = {};
|
const funcs: FuncDefs = {};
|
||||||
forEach(idx || index, (funcDef) => {
|
forEach(idx || index, (funcDef: FuncDef) => {
|
||||||
if (isVersionRelatedFunction(funcDef, graphiteVersion)) {
|
if (isVersionRelatedFunction(funcDef, graphiteVersion)) {
|
||||||
funcs[funcDef.name] = assign({}, funcDef, {
|
funcs[funcDef.name] = assign({}, funcDef, {
|
||||||
params: filter(funcDef.params, (param) => {
|
params: filter(funcDef.params, (param) => {
|
||||||
@ -1110,8 +1127,8 @@ function getFuncDefs(graphiteVersion: string, idx?: any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// parse response from graphite /functions endpoint into internal format
|
// parse response from graphite /functions endpoint into internal format
|
||||||
function parseFuncDefs(rawDefs: any) {
|
function parseFuncDefs(rawDefs: any): FuncDefs {
|
||||||
const funcDefs: any = {};
|
const funcDefs: FuncDefs = {};
|
||||||
|
|
||||||
forEach(rawDefs || {}, (funcDef, funcName) => {
|
forEach(rawDefs || {}, (funcDef, funcName) => {
|
||||||
// skip graphite graph functions
|
// skip graphite graph functions
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
<query-editor-row query-ctrl="ctrl" has-text-edit-mode="true">
|
||||||
|
|
||||||
<div class="gf-form" ng-show="ctrl.state.target.textEditor">
|
<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>
|
||||||
|
|
||||||
<div ng-hide="ctrl.target.textEditor">
|
<div ng-hide="ctrl.target.textEditor">
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div ng-if="ctrl.state.paused" class="gf-form">
|
<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>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
@ -70,7 +70,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form dropdown">
|
<div class="gf-form dropdown">
|
||||||
<span graphite-add-func></span>
|
<add-graphite-function funcDefs="ctrl.state.funcDefs" dispatch="ctrl.dispatch" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="gf-form gf-form--grow">
|
<div class="gf-form gf-form--grow">
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
import './add_graphite_func';
|
|
||||||
import './func_editor';
|
import './func_editor';
|
||||||
|
|
||||||
import GraphiteQuery from './graphite_query';
|
import GraphiteQuery from './graphite_query';
|
||||||
|
@ -3,6 +3,7 @@ import { each, 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
@ -134,10 +135,7 @@ export async function addSeriesByTagFunc(state: GraphiteQueryEditorState, tag: s
|
|||||||
await parseTarget(state);
|
await parseTarget(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function smartlyHandleNewAliasByNode(
|
export function smartlyHandleNewAliasByNode(state: GraphiteQueryEditorState, func: FuncInstance): void {
|
||||||
state: GraphiteQueryEditorState,
|
|
||||||
func: { def: { name: string }; params: number[]; added: boolean }
|
|
||||||
): void {
|
|
||||||
if (func.def.name !== 'aliasByNode') {
|
if (func.def.name !== 'aliasByNode') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,7 @@ import {
|
|||||||
spliceSegments,
|
spliceSegments,
|
||||||
} from './helpers';
|
} from './helpers';
|
||||||
import { Action } from 'redux';
|
import { Action } from 'redux';
|
||||||
|
import { FuncDefs } from '../gfunc';
|
||||||
|
|
||||||
export type GraphiteQueryEditorState = {
|
export type GraphiteQueryEditorState = {
|
||||||
/**
|
/**
|
||||||
@ -38,6 +39,8 @@ export type GraphiteQueryEditorState = {
|
|||||||
|
|
||||||
target: { target: string; textEditor: boolean };
|
target: { target: string; textEditor: boolean };
|
||||||
|
|
||||||
|
funcDefs: FuncDefs | null;
|
||||||
|
|
||||||
segments: GraphiteSegment[];
|
segments: GraphiteSegment[];
|
||||||
queryModel: GraphiteQuery;
|
queryModel: GraphiteQuery;
|
||||||
|
|
||||||
@ -63,6 +66,7 @@ const reducer = async (action: Action, state: GraphiteQueryEditorState): Promise
|
|||||||
supportsTags: deps.datasource.supportsTags,
|
supportsTags: deps.datasource.supportsTags,
|
||||||
paused: false,
|
paused: false,
|
||||||
removeTagValue: '-- remove tag --',
|
removeTagValue: '-- remove tag --',
|
||||||
|
funcDefs: deps.datasource.funcDefs,
|
||||||
};
|
};
|
||||||
|
|
||||||
await buildSegments(state, false);
|
await buildSegments(state, false);
|
||||||
|
Loading…
Reference in New Issue
Block a user