Show graphite functions descriptions (#32305)

* Fix parsing and displaying Graphite function descriptions

* Update docs

* Add support for inf value

* Remove redundant console.log

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>

* Remove empty line

Co-authored-by: Zoltán Bedi <zoltan.bedi@gmail.com>
pull/32368/head
Piotr Jamróz 4 years ago committed by GitHub
parent 70576873f7
commit 9f2fa7c20c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 42
      public/app/plugins/datasource/graphite/FunctionEditor.tsx
  2. 36
      public/app/plugins/datasource/graphite/FunctionEditorControls.tsx
  3. 31
      public/app/plugins/datasource/graphite/datasource.test.ts
  4. 12
      public/app/plugins/datasource/graphite/datasource.ts
  5. 10
      public/app/plugins/datasource/graphite/gfunc.ts
  6. 1
      public/sass/components/_query_editor.scss

@ -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 <div dangerouslySetInnerHTML={{ __html: rst2html(props.description ?? '') }} />;
},
};
});
class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEditorState> {
private triggerRef = React.createRef<HTMLSpanElement>();
@ -31,25 +22,7 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
}
renderContent = ({ updatePopperPosition }: any) => {
const {
onMoveLeft,
onMoveRight,
func: {
def: { name, description },
},
} = this.props;
const { showingDescription } = this.state;
if (showingDescription) {
return (
<div style={{ overflow: 'auto', maxHeight: '30rem', textAlign: 'left', fontWeight: 'normal' }}>
<h4 style={{ color: 'white' }}> {name} </h4>
<Suspense fallback={<span>Loading description...</span>}>
<FunctionDescription description={description} />
</Suspense>
</div>
);
}
const { onMoveLeft, onMoveRight } = this.props;
return (
<FunctionEditorControls
@ -62,11 +35,6 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
onMoveRight(this.props.func);
updatePopperPosition();
}}
onDescriptionShow={() => {
this.setState({ showingDescription: true }, () => {
updatePopperPosition();
});
}}
/>
);
};
@ -83,9 +51,6 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
referenceElement={this.triggerRef.current}
wrapperClassName="popper"
className="popper__background"
onMouseLeave={() => {
this.setState({ showingDescription: false });
}}
renderArrow={({ arrowProps, placement }) => (
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
)}
@ -101,9 +66,6 @@ class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEd
<span
ref={this.triggerRef}
onClick={popperProps.show ? hidePopper : showPopper}
onMouseLeave={() => {
this.setState({ showingDescription: false });
}}
style={{ cursor: 'pointer' }}
>
{this.props.func.def.name}

@ -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 <div dangerouslySetInnerHTML={{ __html: rst2html(props.description ?? '') }} />;
},
};
});
const FunctionHelpButton = (props: { description?: string; name: string }) => {
if (props.description) {
return <Icon className="pointer" name="question-circle" onClick={props.onDescriptionShow} />;
let tooltip = (
<Suspense fallback={<span>Loading description...</span>}>
<FunctionDescription description={props.description} />
</Suspense>
);
return (
<Tooltip content={tooltip} placement={'bottom-end'}>
<Icon className={props.description ? undefined : 'pointer'} name="question-circle" />
</Tooltip>
);
}
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 (
<div
style={{
@ -55,11 +73,7 @@ export const FunctionEditorControls = (
}}
>
<Icon name="arrow-left" onClick={() => onMoveLeft(func)} />
<FunctionHelpButton
name={func.def.name}
description={func.def.description}
onDescriptionShow={onDescriptionShow}
/>
<FunctionHelpButton name={func.def.name} description={func.def.description} />
<Icon name="times" onClick={() => onRemove(func)} />
<Icon name="arrow-right" onClick={() => onMoveRight(func)} />
</div>

@ -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({

@ -581,7 +581,17 @@ export class GraphiteDatasource extends DataSourceApi<GraphiteQuery, GraphiteOpt
.pipe(
map((results: any) => {
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);
}

@ -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) {

@ -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 {

Loading…
Cancel
Save