mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Display graphite function name editor in a tooltip
This commit is contained in:
parent
a6cae5b2b8
commit
1069d7f5b1
@ -1,6 +1,6 @@
|
|||||||
import React, { Component, createRef } from 'react';
|
import React, { Component, createRef } from 'react';
|
||||||
import PopperController from '../Tooltip/PopperController';
|
import { PopperController } from '../Tooltip/PopperController';
|
||||||
import Popper from '../Tooltip/Popper';
|
import { Popper } from '../Tooltip/Popper';
|
||||||
import { ColorPickerPopover } from './ColorPickerPopover';
|
import { ColorPickerPopover } from './ColorPickerPopover';
|
||||||
import { Themeable } from '../../types';
|
import { Themeable } from '../../types';
|
||||||
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette';
|
||||||
|
@ -58,7 +58,7 @@ class Popper extends PureComponent<Props> {
|
|||||||
// TODO: move modifiers config to popper controller
|
// TODO: move modifiers config to popper controller
|
||||||
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
|
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
|
||||||
>
|
>
|
||||||
{({ ref, style, placement, arrowProps }) => {
|
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onMouseEnter={onMouseEnter}
|
onMouseEnter={onMouseEnter}
|
||||||
@ -73,7 +73,12 @@ class Popper extends PureComponent<Props> {
|
|||||||
className={`${wrapperClassName}`}
|
className={`${wrapperClassName}`}
|
||||||
>
|
>
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
{typeof content === 'string' ? content : React.cloneElement(content)}
|
{typeof content === 'string' && content}
|
||||||
|
{React.isValidElement(content) && React.cloneElement(content)}
|
||||||
|
{typeof content === 'function' &&
|
||||||
|
content({
|
||||||
|
updatePopperPosition: scheduleUpdate,
|
||||||
|
})}
|
||||||
{renderArrow &&
|
{renderArrow &&
|
||||||
renderArrow({
|
renderArrow({
|
||||||
arrowProps,
|
arrowProps,
|
||||||
@ -93,4 +98,4 @@ class Popper extends PureComponent<Props> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Popper;
|
export { Popper };
|
||||||
|
@ -7,7 +7,7 @@ export interface PopperContentProps {
|
|||||||
updatePopperPosition?: () => void;
|
updatePopperPosition?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T>;
|
export type PopperContent<T extends PopperContentProps> = string | React.ReactElement<T> | ((props: T) => JSX.Element);
|
||||||
|
|
||||||
export interface UsingPopperProps {
|
export interface UsingPopperProps {
|
||||||
show?: boolean;
|
show?: boolean;
|
||||||
@ -101,4 +101,4 @@ class PopperController extends React.Component<Props, State> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PopperController;
|
export { PopperController };
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import React, { createRef } from 'react';
|
import React, { createRef } from 'react';
|
||||||
import * as PopperJS from 'popper.js';
|
import * as PopperJS from 'popper.js';
|
||||||
import Popper from './Popper';
|
import { Popper } from './Popper';
|
||||||
import PopperController, { UsingPopperProps } from './PopperController';
|
import { PopperController, UsingPopperProps } from './PopperController';
|
||||||
|
|
||||||
interface TooltipProps extends UsingPopperProps {
|
interface TooltipProps extends UsingPopperProps {
|
||||||
theme?: 'info' | 'error';
|
theme?: 'info' | 'error';
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
export { DeleteButton } from './DeleteButton/DeleteButton';
|
export { DeleteButton } from './DeleteButton/DeleteButton';
|
||||||
export { Tooltip } from './Tooltip/Tooltip';
|
export { Tooltip } from './Tooltip/Tooltip';
|
||||||
|
export { PopperController } from './Tooltip/PopperController';
|
||||||
|
export { Popper } from './Tooltip/Popper';
|
||||||
export { Portal } from './Portal/Portal';
|
export { Portal } from './Portal/Portal';
|
||||||
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar';
|
||||||
|
|
||||||
|
@ -10,10 +10,12 @@ import { SideMenu } from './components/sidemenu/SideMenu';
|
|||||||
import { MetricSelect } from './components/Select/MetricSelect';
|
import { MetricSelect } from './components/Select/MetricSelect';
|
||||||
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
import AppNotificationList from './components/AppNotifications/AppNotificationList';
|
||||||
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
|
import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui';
|
||||||
|
import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor';
|
||||||
|
|
||||||
export function registerAngularDirectives() {
|
export function registerAngularDirectives() {
|
||||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||||
react2AngularDirective('sidemenu', SideMenu, []);
|
react2AngularDirective('sidemenu', SideMenu, []);
|
||||||
|
react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']);
|
||||||
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
react2AngularDirective('appNotificationsList', AppNotificationList, []);
|
||||||
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']);
|
||||||
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
react2AngularDirective('emptyListCta', EmptyListCTA, ['model']);
|
||||||
|
104
public/app/plugins/datasource/graphite/FunctionEditor.tsx
Normal file
104
public/app/plugins/datasource/graphite/FunctionEditor.tsx
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { PopperController, Popper } from '@grafana/ui';
|
||||||
|
import rst2html from 'rst2html';
|
||||||
|
import { FunctionDescriptor, FunctionEditorControlsProps, FunctionEditorControls } from './FunctionEditorControls';
|
||||||
|
|
||||||
|
interface FunctionEditorProps extends FunctionEditorControlsProps {
|
||||||
|
func: FunctionDescriptor;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface FunctionEditorState {
|
||||||
|
showingDescription: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FunctionEditor extends React.PureComponent<FunctionEditorProps, FunctionEditorState> {
|
||||||
|
private triggerRef = React.createRef<HTMLSpanElement>();
|
||||||
|
|
||||||
|
constructor(props: FunctionEditorProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
|
showingDescription: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
renderContent = ({ updatePopperPosition }) => {
|
||||||
|
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>
|
||||||
|
<div
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: rst2html(description),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FunctionEditorControls
|
||||||
|
{...this.props}
|
||||||
|
onMoveLeft={() => {
|
||||||
|
onMoveLeft(this.props.func);
|
||||||
|
updatePopperPosition();
|
||||||
|
}}
|
||||||
|
onMoveRight={() => {
|
||||||
|
onMoveRight(this.props.func);
|
||||||
|
updatePopperPosition();
|
||||||
|
}}
|
||||||
|
onDescriptionShow={() => {
|
||||||
|
this.setState({ showingDescription: true }, () => {
|
||||||
|
updatePopperPosition();
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<PopperController content={this.renderContent} placement="top" hideAfter={300}>
|
||||||
|
{(showPopper, hidePopper, popperProps) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{this.triggerRef && (
|
||||||
|
<Popper
|
||||||
|
{...popperProps}
|
||||||
|
referenceElement={this.triggerRef.current}
|
||||||
|
wrapperClassName="popper"
|
||||||
|
className="popper__background"
|
||||||
|
onMouseLeave={() => {
|
||||||
|
this.setState({ showingDescription: false });
|
||||||
|
hidePopper();
|
||||||
|
}}
|
||||||
|
onMouseEnter={showPopper}
|
||||||
|
renderArrow={({ arrowProps, placement }) => (
|
||||||
|
<div className="popper__arrow" data-placement={placement} {...arrowProps} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<span
|
||||||
|
ref={this.triggerRef}
|
||||||
|
onClick={popperProps.show ? hidePopper : showPopper}
|
||||||
|
onMouseLeave={() => {
|
||||||
|
hidePopper();
|
||||||
|
this.setState({ showingDescription: false });
|
||||||
|
}}
|
||||||
|
style={{ cursor: 'pointer' }}
|
||||||
|
>
|
||||||
|
{this.props.func.def.name}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
</PopperController>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export { FunctionEditor };
|
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
export interface FunctionDescriptor {
|
||||||
|
text: string;
|
||||||
|
params: string[];
|
||||||
|
def: {
|
||||||
|
category: string;
|
||||||
|
defaultParams: string[];
|
||||||
|
description?: string;
|
||||||
|
fake: boolean;
|
||||||
|
name: string;
|
||||||
|
params: string[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FunctionEditorControlsProps {
|
||||||
|
onMoveLeft: (func: FunctionDescriptor) => void;
|
||||||
|
onMoveRight: (func: FunctionDescriptor) => void;
|
||||||
|
onRemove: (func: FunctionDescriptor) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FunctionHelpButton = (props: { description: string; name: string; onDescriptionShow: () => void }) => {
|
||||||
|
if (props.description) {
|
||||||
|
return <span className="pointer fa fa-question-circle" onClick={props.onDescriptionShow} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="pointer fa fa-question-circle"
|
||||||
|
onClick={() => {
|
||||||
|
window.open(
|
||||||
|
'http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions.' + props.name,
|
||||||
|
'_blank'
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FunctionEditorControls = (
|
||||||
|
props: FunctionEditorControlsProps & {
|
||||||
|
func: FunctionDescriptor;
|
||||||
|
onDescriptionShow: () => void;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
const { func, onMoveLeft, onMoveRight, onRemove, onDescriptionShow } = props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
width: '60px',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="pointer fa fa-arrow-left" onClick={() => onMoveLeft(func)} />
|
||||||
|
<FunctionHelpButton
|
||||||
|
name={func.def.name}
|
||||||
|
description={func.def.description}
|
||||||
|
onDescriptionShow={onDescriptionShow}
|
||||||
|
/>
|
||||||
|
<span className="pointer fa fa-remove" onClick={() => onRemove(func)} />
|
||||||
|
<span className="pointer fa fa-arrow-right" onClick={() => onMoveRight(func)} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -1,33 +1,42 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
import rst2html from 'rst2html';
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
||||||
const funcSpanTemplate = '<a ng-click="">{{func.def.name}}</a><span>(</span>';
|
const funcSpanTemplate = `
|
||||||
|
<function-editor
|
||||||
|
func="func"
|
||||||
|
onRemove="ctrl.handleRemoveFunction"
|
||||||
|
onMoveLeft="ctrl.handleMoveLeft"
|
||||||
|
onMoveRight="ctrl.handleMoveRight"
|
||||||
|
/><span>(</span>
|
||||||
|
`;
|
||||||
const paramTemplate =
|
const paramTemplate =
|
||||||
'<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
|
'<input type="text" style="display:none"' + ' class="input-small tight-form-func-param"></input>';
|
||||||
|
|
||||||
const funcControlsTemplate = `
|
|
||||||
<div class="tight-form-func-controls">
|
|
||||||
<span class="pointer fa fa-arrow-left"></span>
|
|
||||||
<span class="pointer fa fa-question-circle"></span>
|
|
||||||
<span class="pointer fa fa-remove" ></span>
|
|
||||||
<span class="pointer fa fa-arrow-right"></span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
restrict: 'A',
|
restrict: 'A',
|
||||||
link: function postLink($scope, elem) {
|
link: function postLink($scope, elem) {
|
||||||
const $funcLink = $(funcSpanTemplate);
|
const $funcLink = $(funcSpanTemplate);
|
||||||
const $funcControls = $(funcControlsTemplate);
|
|
||||||
const ctrl = $scope.ctrl;
|
const ctrl = $scope.ctrl;
|
||||||
const func = $scope.func;
|
const func = $scope.func;
|
||||||
let scheduledRelink = false;
|
let scheduledRelink = false;
|
||||||
let paramCountAtLink = 0;
|
let paramCountAtLink = 0;
|
||||||
let cancelBlur = null;
|
let cancelBlur = null;
|
||||||
|
|
||||||
|
ctrl.handleRemoveFunction = func => {
|
||||||
|
ctrl.removeFunction(func);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.handleMoveLeft = func => {
|
||||||
|
ctrl.moveFunction(func, -1);
|
||||||
|
};
|
||||||
|
|
||||||
|
ctrl.handleMoveRight = func => {
|
||||||
|
ctrl.moveFunction(func, 1);
|
||||||
|
};
|
||||||
|
|
||||||
function clickFuncParam(this: any, paramIndex) {
|
function clickFuncParam(this: any, paramIndex) {
|
||||||
/*jshint validthis:true */
|
/*jshint validthis:true */
|
||||||
|
|
||||||
@ -158,24 +167,7 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function toggleFuncControls() {
|
|
||||||
const targetDiv = elem.closest('.tight-form');
|
|
||||||
|
|
||||||
if (elem.hasClass('show-function-controls')) {
|
|
||||||
elem.removeClass('show-function-controls');
|
|
||||||
targetDiv.removeClass('has-open-function');
|
|
||||||
$funcControls.hide();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
elem.addClass('show-function-controls');
|
|
||||||
targetDiv.addClass('has-open-function');
|
|
||||||
|
|
||||||
$funcControls.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function addElementsAndCompile() {
|
function addElementsAndCompile() {
|
||||||
$funcControls.appendTo(elem);
|
|
||||||
$funcLink.appendTo(elem);
|
$funcLink.appendTo(elem);
|
||||||
|
|
||||||
const defParams = _.clone(func.def.params);
|
const defParams = _.clone(func.def.params);
|
||||||
@ -245,69 +237,10 @@ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerFuncControlsToggle() {
|
|
||||||
$funcLink.click(toggleFuncControls);
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerFuncControlsActions() {
|
|
||||||
$funcControls.click(e => {
|
|
||||||
const $target = $(e.target);
|
|
||||||
if ($target.hasClass('fa-remove')) {
|
|
||||||
toggleFuncControls();
|
|
||||||
$scope.$apply(() => {
|
|
||||||
ctrl.removeFunction($scope.func);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($target.hasClass('fa-arrow-left')) {
|
|
||||||
$scope.$apply(() => {
|
|
||||||
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index - 1);
|
|
||||||
ctrl.targetChanged();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($target.hasClass('fa-arrow-right')) {
|
|
||||||
$scope.$apply(() => {
|
|
||||||
_.move(ctrl.queryModel.functions, $scope.$index, $scope.$index + 1);
|
|
||||||
ctrl.targetChanged();
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($target.hasClass('fa-question-circle')) {
|
|
||||||
const funcDef = ctrl.datasource.getFuncDef(func.def.name);
|
|
||||||
if (funcDef && funcDef.description) {
|
|
||||||
popoverSrv.show({
|
|
||||||
element: e.target,
|
|
||||||
position: 'bottom left',
|
|
||||||
classNames: 'drop-popover drop-function-def',
|
|
||||||
template: `
|
|
||||||
<div style="overflow:auto;max-height:30rem;">
|
|
||||||
<h4> ${funcDef.name} </h4>
|
|
||||||
${rst2html(funcDef.description)}
|
|
||||||
</div>`,
|
|
||||||
openOn: 'click',
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
window.open(
|
|
||||||
'http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions.' + func.def.name,
|
|
||||||
'_blank'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function relink() {
|
function relink() {
|
||||||
elem.children().remove();
|
elem.children().remove();
|
||||||
|
|
||||||
addElementsAndCompile();
|
addElementsAndCompile();
|
||||||
ifJustAddedFocusFirstParam();
|
ifJustAddedFocusFirstParam();
|
||||||
registerFuncControlsToggle();
|
|
||||||
registerFuncControlsActions();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
relink();
|
relink();
|
||||||
|
@ -154,6 +154,11 @@ export default class GraphiteQuery {
|
|||||||
this.functions = _.without(this.functions, func);
|
this.functions = _.without(this.functions, func);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveFunction(func, offset) {
|
||||||
|
const index = this.functions.indexOf(func);
|
||||||
|
_.move(this.functions, index, index + offset);
|
||||||
|
}
|
||||||
|
|
||||||
updateModelTarget(targets) {
|
updateModelTarget(targets) {
|
||||||
// render query
|
// render query
|
||||||
if (!this.target.textEditor) {
|
if (!this.target.textEditor) {
|
||||||
|
@ -272,6 +272,11 @@ export class GraphiteQueryCtrl extends QueryCtrl {
|
|||||||
this.targetChanged();
|
this.targetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
moveFunction(func, offset) {
|
||||||
|
this.queryModel.moveFunction(func, offset);
|
||||||
|
this.targetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
addSeriesByTagFunc(tag) {
|
addSeriesByTagFunc(tag) {
|
||||||
const newFunc = this.datasource.createFuncInstance('seriesByTag', {
|
const newFunc = this.datasource.createFuncInstance('seriesByTag', {
|
||||||
withDefaultParams: false,
|
withDefaultParams: false,
|
||||||
|
@ -50,7 +50,6 @@ input[type='text'].tight-form-func-param {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tight-form-func-controls {
|
.tight-form-func-controls {
|
||||||
display: none;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
.fa-arrow-left {
|
.fa-arrow-left {
|
||||||
|
Loading…
Reference in New Issue
Block a user