mirror of
https://github.com/grafana/grafana.git
synced 2025-02-09 23:16:16 -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 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';
|
||||
|
@ -58,7 +58,7 @@ class Popper extends PureComponent<Props> {
|
||||
// TODO: move modifiers config to popper controller
|
||||
modifiers={{ preventOverflow: { enabled: true, boundariesElement: 'window' } }}
|
||||
>
|
||||
{({ ref, style, placement, arrowProps }) => {
|
||||
{({ ref, style, placement, arrowProps, scheduleUpdate }) => {
|
||||
return (
|
||||
<div
|
||||
onMouseEnter={onMouseEnter}
|
||||
@ -73,7 +73,12 @@ class Popper extends PureComponent<Props> {
|
||||
className={`${wrapperClassName}`}
|
||||
>
|
||||
<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({
|
||||
arrowProps,
|
||||
@ -93,4 +98,4 @@ class Popper extends PureComponent<Props> {
|
||||
}
|
||||
}
|
||||
|
||||
export default Popper;
|
||||
export { Popper };
|
||||
|
@ -7,7 +7,7 @@ export interface PopperContentProps {
|
||||
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 {
|
||||
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 * 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';
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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']);
|
||||
|
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 'jquery';
|
||||
import rst2html from 'rst2html';
|
||||
import coreModule from 'app/core/core_module';
|
||||
|
||||
/** @ngInject */
|
||||
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 =
|
||||
'<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 {
|
||||
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: `
|
||||
<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() {
|
||||
elem.children().remove();
|
||||
|
||||
addElementsAndCompile();
|
||||
ifJustAddedFocusFirstParam();
|
||||
registerFuncControlsToggle();
|
||||
registerFuncControlsActions();
|
||||
}
|
||||
|
||||
relink();
|
||||
|
@ -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) {
|
||||
|
@ -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,
|
||||
|
@ -50,7 +50,6 @@ input[type='text'].tight-form-func-param {
|
||||
}
|
||||
|
||||
.tight-form-func-controls {
|
||||
display: none;
|
||||
text-align: center;
|
||||
|
||||
.fa-arrow-left {
|
||||
|
Loading…
Reference in New Issue
Block a user