mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user