From 1069d7f5b160d1d45ae998611402220f924f4cb1 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 18 Feb 2019 17:55:38 +0100 Subject: [PATCH] Display graphite function name editor in a tooltip --- .../components/ColorPicker/ColorPicker.tsx | 4 +- .../src/components/Tooltip/Popper.tsx | 11 +- .../components/Tooltip/PopperController.tsx | 4 +- .../src/components/Tooltip/Tooltip.tsx | 4 +- packages/grafana-ui/src/components/index.ts | 2 + public/app/core/angular_wrappers.ts | 2 + .../datasource/graphite/FunctionEditor.tsx | 104 +++++++++++++++++ .../graphite/FunctionEditorControls.tsx | 65 +++++++++++ .../datasource/graphite/func_editor.ts | 107 ++++-------------- .../datasource/graphite/graphite_query.ts | 5 + .../plugins/datasource/graphite/query_ctrl.ts | 5 + public/sass/components/_query_editor.scss | 1 - 12 files changed, 217 insertions(+), 97 deletions(-) create mode 100644 public/app/plugins/datasource/graphite/FunctionEditor.tsx create mode 100644 public/app/plugins/datasource/graphite/FunctionEditorControls.tsx diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx index 27371d5515f..321323ac58b 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPicker.tsx @@ -1,6 +1,6 @@ import React, { Component, createRef } from 'react'; -import PopperController from '../Tooltip/PopperController'; -import Popper from '../Tooltip/Popper'; +import { PopperController } from '../Tooltip/PopperController'; +import { Popper } from '../Tooltip/Popper'; import { ColorPickerPopover } from './ColorPickerPopover'; import { Themeable } from '../../types'; import { getColorFromHexRgbOrName } from '../../utils/namedColorsPalette'; diff --git a/packages/grafana-ui/src/components/Tooltip/Popper.tsx b/packages/grafana-ui/src/components/Tooltip/Popper.tsx index cf4b9cdd653..ab25ead597c 100644 --- a/packages/grafana-ui/src/components/Tooltip/Popper.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Popper.tsx @@ -58,7 +58,7 @@ class Popper extends PureComponent { // TODO: move modifiers config to popper controller modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }} > - {({ ref, style, placement, arrowProps }) => { + {({ ref, style, placement, arrowProps, scheduleUpdate }) => { return (
{ className={`${wrapperClassName}`} >
- {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({ arrowProps, @@ -93,4 +98,4 @@ class Popper extends PureComponent { } } -export default Popper; +export { Popper }; diff --git a/packages/grafana-ui/src/components/Tooltip/PopperController.tsx b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx index 770d1ce9f37..ed422104701 100644 --- a/packages/grafana-ui/src/components/Tooltip/PopperController.tsx +++ b/packages/grafana-ui/src/components/Tooltip/PopperController.tsx @@ -7,7 +7,7 @@ export interface PopperContentProps { updatePopperPosition?: () => void; } -export type PopperContent = string | React.ReactElement; +export type PopperContent = string | React.ReactElement | ((props: T) => JSX.Element); export interface UsingPopperProps { show?: boolean; @@ -101,4 +101,4 @@ class PopperController extends React.Component { } } -export default PopperController; +export { PopperController }; diff --git a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx index 28987f272cc..81b33e44156 100644 --- a/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx +++ b/packages/grafana-ui/src/components/Tooltip/Tooltip.tsx @@ -1,7 +1,7 @@ import React, { createRef } from 'react'; import * as PopperJS from 'popper.js'; -import Popper from './Popper'; -import PopperController, { UsingPopperProps } from './PopperController'; +import { Popper } from './Popper'; +import { PopperController, UsingPopperProps } from './PopperController'; interface TooltipProps extends UsingPopperProps { theme?: 'info' | 'error'; diff --git a/packages/grafana-ui/src/components/index.ts b/packages/grafana-ui/src/components/index.ts index 86ce9347dad..ca8899bd928 100644 --- a/packages/grafana-ui/src/components/index.ts +++ b/packages/grafana-ui/src/components/index.ts @@ -1,5 +1,7 @@ export { DeleteButton } from './DeleteButton/DeleteButton'; export { Tooltip } from './Tooltip/Tooltip'; +export { PopperController } from './Tooltip/PopperController'; +export { Popper } from './Tooltip/Popper'; export { Portal } from './Portal/Portal'; export { CustomScrollbar } from './CustomScrollbar/CustomScrollbar'; diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 6db442e7470..9105de82d53 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -10,10 +10,12 @@ import { SideMenu } from './components/sidemenu/SideMenu'; import { MetricSelect } from './components/Select/MetricSelect'; import AppNotificationList from './components/AppNotifications/AppNotificationList'; import { ColorPicker, SeriesColorPickerPopoverWithTheme } from '@grafana/ui'; +import { FunctionEditor } from 'app/plugins/datasource/graphite/FunctionEditor'; export function registerAngularDirectives() { react2AngularDirective('passwordStrength', PasswordStrength, ['password']); react2AngularDirective('sidemenu', SideMenu, []); + react2AngularDirective('functionEditor', FunctionEditor, ['func', 'onRemove', 'onMoveLeft', 'onMoveRight']); react2AngularDirective('appNotificationsList', AppNotificationList, []); react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); diff --git a/public/app/plugins/datasource/graphite/FunctionEditor.tsx b/public/app/plugins/datasource/graphite/FunctionEditor.tsx new file mode 100644 index 00000000000..aa8aecb1ddf --- /dev/null +++ b/public/app/plugins/datasource/graphite/FunctionEditor.tsx @@ -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 { + private triggerRef = React.createRef(); + + 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 ( +
+

{name}

+
+
+ ); + } + + return ( + { + onMoveLeft(this.props.func); + updatePopperPosition(); + }} + onMoveRight={() => { + onMoveRight(this.props.func); + updatePopperPosition(); + }} + onDescriptionShow={() => { + this.setState({ showingDescription: true }, () => { + updatePopperPosition(); + }); + }} + /> + ); + }; + + render() { + return ( + + {(showPopper, hidePopper, popperProps) => { + return ( + <> + {this.triggerRef && ( + { + this.setState({ showingDescription: false }); + hidePopper(); + }} + onMouseEnter={showPopper} + renderArrow={({ arrowProps, placement }) => ( +
+ )} + /> + )} + + { + hidePopper(); + this.setState({ showingDescription: false }); + }} + style={{ cursor: 'pointer' }} + > + {this.props.func.def.name} + + + ); + }} + + ); + } +} + +export { FunctionEditor }; diff --git a/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx b/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx new file mode 100644 index 00000000000..9862cc4f038 --- /dev/null +++ b/public/app/plugins/datasource/graphite/FunctionEditorControls.tsx @@ -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 ; + } + + return ( + { + 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 ( +
+ onMoveLeft(func)} /> + + onRemove(func)} /> + onMoveRight(func)} /> +
+ ); +}; diff --git a/public/app/plugins/datasource/graphite/func_editor.ts b/public/app/plugins/datasource/graphite/func_editor.ts index 9e19083a9c3..f2522f2ab18 100644 --- a/public/app/plugins/datasource/graphite/func_editor.ts +++ b/public/app/plugins/datasource/graphite/func_editor.ts @@ -1,33 +1,42 @@ import _ from 'lodash'; import $ from 'jquery'; -import rst2html from 'rst2html'; import coreModule from 'app/core/core_module'; /** @ngInject */ export function graphiteFuncEditor($compile, templateSrv, popoverSrv) { - const funcSpanTemplate = '{{func.def.name}}('; + const funcSpanTemplate = ` + ( + `; const paramTemplate = ''; - const funcControlsTemplate = ` -
- - - - -
`; - return { restrict: 'A', link: function postLink($scope, elem) { const $funcLink = $(funcSpanTemplate); - const $funcControls = $(funcControlsTemplate); const ctrl = $scope.ctrl; const func = $scope.func; let scheduledRelink = false; let paramCountAtLink = 0; 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) { /*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() { - $funcControls.appendTo(elem); $funcLink.appendTo(elem); 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: ` -
-

${funcDef.name}

- ${rst2html(funcDef.description)} -
`, - openOn: 'click', - }); - } else { - window.open( - 'http://graphite.readthedocs.org/en/latest/functions.html#graphite.render.functions.' + func.def.name, - '_blank' - ); - } - return; - } - }); - } - function relink() { elem.children().remove(); - addElementsAndCompile(); ifJustAddedFocusFirstParam(); - registerFuncControlsToggle(); - registerFuncControlsActions(); } relink(); diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index ab137a6a299..40a6b1d8543 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -154,6 +154,11 @@ export default class GraphiteQuery { this.functions = _.without(this.functions, func); } + moveFunction(func, offset) { + const index = this.functions.indexOf(func); + _.move(this.functions, index, index + offset); + } + updateModelTarget(targets) { // render query if (!this.target.textEditor) { diff --git a/public/app/plugins/datasource/graphite/query_ctrl.ts b/public/app/plugins/datasource/graphite/query_ctrl.ts index b89e84d23a7..80ddc0f5e07 100644 --- a/public/app/plugins/datasource/graphite/query_ctrl.ts +++ b/public/app/plugins/datasource/graphite/query_ctrl.ts @@ -272,6 +272,11 @@ export class GraphiteQueryCtrl extends QueryCtrl { this.targetChanged(); } + moveFunction(func, offset) { + this.queryModel.moveFunction(func, offset); + this.targetChanged(); + } + addSeriesByTagFunc(tag) { const newFunc = this.datasource.createFuncInstance('seriesByTag', { withDefaultParams: false, diff --git a/public/sass/components/_query_editor.scss b/public/sass/components/_query_editor.scss index 71332541b2e..676b3cd5407 100644 --- a/public/sass/components/_query_editor.scss +++ b/public/sass/components/_query_editor.scss @@ -50,7 +50,6 @@ input[type='text'].tight-form-func-param { } .tight-form-func-controls { - display: none; text-align: center; .fa-arrow-left {