diff --git a/public/app/plugins/datasource/graphite/FunctionEditor.tsx b/public/app/plugins/datasource/graphite/FunctionEditor.tsx index d178904640a..ffdbd5dddfe 100644 --- a/public/app/plugins/datasource/graphite/FunctionEditor.tsx +++ b/public/app/plugins/datasource/graphite/FunctionEditor.tsx @@ -1,4 +1,4 @@ -import React, { Suspense } from 'react'; +import React from 'react'; import { PopoverController, Popover, ClickOutsideWrapper } from '@grafana/ui'; import { FunctionDescriptor, FunctionEditorControls, FunctionEditorControlsProps } from './FunctionEditorControls'; @@ -9,15 +9,6 @@ interface FunctionEditorProps extends FunctionEditorControlsProps { interface FunctionEditorState { showingDescription: boolean; } -const FunctionDescription = React.lazy(async () => { - // @ts-ignore - const { default: rst2html } = await import(/* webpackChunkName: "rst2html" */ 'rst2html'); - return { - default(props: { description?: string }) { - return
; - }, - }; -}); class FunctionEditor extends React.PureComponent { private triggerRef = React.createRef(); @@ -31,25 +22,7 @@ class FunctionEditor extends React.PureComponent { - const { - onMoveLeft, - onMoveRight, - func: { - def: { name, description }, - }, - } = this.props; - const { showingDescription } = this.state; - - if (showingDescription) { - return ( -
-

{name}

- Loading description...}> - - -
- ); - } + const { onMoveLeft, onMoveRight } = this.props; return ( { - this.setState({ showingDescription: true }, () => { - updatePopperPosition(); - }); - }} /> ); }; @@ -83,9 +51,6 @@ class FunctionEditor extends React.PureComponent { - this.setState({ showingDescription: false }); - }} renderArrow={({ arrowProps, placement }) => (
)} @@ -101,9 +66,6 @@ class FunctionEditor extends React.PureComponent { - this.setState({ showingDescription: false }); - }} style={{ cursor: 'pointer' }} > {this.props.func.def.name} diff --git a/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx b/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx index 1a8211b7919..cd9a18b2c78 100644 --- a/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx +++ b/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx @@ -1,5 +1,5 @@ -import React from 'react'; -import { Icon } from '@grafana/ui'; +import React, { Suspense } from 'react'; +import { Icon, Tooltip } from '@grafana/ui'; export interface FunctionDescriptor { text: string; @@ -20,9 +20,28 @@ export interface FunctionEditorControlsProps { onRemove: (func: FunctionDescriptor) => void; } -const FunctionHelpButton = (props: { description?: string; name: string; onDescriptionShow: () => void }) => { +const FunctionDescription = React.lazy(async () => { + // @ts-ignore + const { default: rst2html } = await import(/* webpackChunkName: "rst2html" */ 'rst2html'); + return { + default(props: { description?: string }) { + return
; + }, + }; +}); + +const FunctionHelpButton = (props: { description?: string; name: string }) => { if (props.description) { - return ; + let tooltip = ( + Loading description...}> + + + ); + return ( + + + + ); } return ( @@ -42,10 +61,9 @@ const FunctionHelpButton = (props: { description?: string; name: string; onDescr export const FunctionEditorControls = ( props: FunctionEditorControlsProps & { func: FunctionDescriptor; - onDescriptionShow: () => void; } ) => { - const { func, onMoveLeft, onMoveRight, onRemove, onDescriptionShow } = props; + const { func, onMoveLeft, onMoveRight, onRemove } = props; return (
onMoveLeft(func)} /> - + onRemove(func)} /> onMoveRight(func)} />
diff --git a/public/app/plugins/datasource/graphite/datasource.test.ts b/public/app/plugins/datasource/graphite/datasource.test.ts index d3d98337e7b..170f85bea0b 100644 --- a/public/app/plugins/datasource/graphite/datasource.test.ts +++ b/public/app/plugins/datasource/graphite/datasource.test.ts @@ -253,6 +253,37 @@ describe('graphiteDatasource', () => { }); }); + describe('when fetching Graphite function descriptions', () => { + // `"default": Infinity` (invalid JSON) in params passed by Graphite API in 1.1.7 + const INVALID_JSON = + '{"testFunction":{"name":"function","description":"description","module":"graphite.render.functions","group":"Transform","params":[{"name":"param","type":"intOrInf","required":true,"default":Infinity}]}}'; + + it('should parse the response with an invalid JSON', async () => { + fetchMock.mockImplementation(() => { + return of(createFetchResponse(INVALID_JSON)); + }); + const funcDefs = await ctx.ds.getFuncDefs(); + expect(funcDefs).toEqual({ + testFunction: { + category: 'Transform', + defaultParams: ['inf'], + description: 'description', + fake: true, + name: 'function', + params: [ + { + multiple: false, + name: 'param', + optional: false, + options: undefined, + type: 'int_or_infinity', + }, + ], + }, + }); + }); + }); + describe('building graphite params', () => { it('should return empty array if no targets', () => { const results = ctx.ds.buildGraphiteParams({ diff --git a/public/app/plugins/datasource/graphite/datasource.ts b/public/app/plugins/datasource/graphite/datasource.ts index 8153fdd7280..24e71a37d97 100644 --- a/public/app/plugins/datasource/graphite/datasource.ts +++ b/public/app/plugins/datasource/graphite/datasource.ts @@ -581,7 +581,17 @@ export class GraphiteDatasource extends DataSourceApi { if (results.status !== 200 || typeof results.data !== 'object') { - this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + if (typeof results.data === 'string') { + // Fix for a Graphite bug: https://github.com/graphite-project/graphite-web/issues/2609 + // There is a fix for it https://github.com/graphite-project/graphite-web/pull/2612 but + // it was merged to master in July 2020 but it has never been released (the last Graphite + // release was 1.1.7 - March 2020). The bug was introduced in Graphite 1.1.7, in versions + // 1.1.0 - 1.1.6 /functions endpoint returns a valid JSON + const fixedData = JSON.parse(results.data.replace(/"default": ?Infinity/g, '"default": 1e9999')); + this.funcDefs = gfunc.parseFuncDefs(fixedData); + } else { + this.funcDefs = gfunc.getFuncDefs(this.graphiteVersion); + } } else { this.funcDefs = gfunc.parseFuncDefs(results.data); } diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index c050d8aa207..399b4c76816 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -999,7 +999,7 @@ export class FuncInstance { } // param types that should never be quoted - if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node'], paramType)) { + if (_.includes(['value_or_series', 'boolean', 'int', 'float', 'node', 'int_or_infinity'], paramType)) { return value; } @@ -1155,7 +1155,11 @@ function parseFuncDefs(rawDefs: any) { }; if (rawParam.default !== undefined) { - func.defaultParams.push(_.toString(rawParam.default)); + if (rawParam.default === Infinity) { + func.defaultParams.push('inf'); + } else { + func.defaultParams.push(_.toString(rawParam.default)); + } } else if (rawParam.suggestions) { func.defaultParams.push(_.toString(rawParam.suggestions[0])); } else { @@ -1179,6 +1183,8 @@ function parseFuncDefs(rawDefs: any) { param.type = 'int_or_interval'; } else if (rawParam.type === 'seriesList') { param.type = 'value_or_series'; + } else if (rawParam.type === 'intOrInf') { + param.type = 'int_or_infinity'; } if (rawParam.options) { diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index cf8d2e8ce88..d81f54cbd40 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -68,6 +68,7 @@ input[type='text'].tight-form-func-param { .rst-unknown.rst-directive { font-family: monospace; margin-bottom: $space-md; + word-wrap: break-word; } .rst-interpreted_text {