diff --git a/packages/grafana-ui/src/components/Segment/Segment.tsx b/packages/grafana-ui/src/components/Segment/Segment.tsx index dcb10922951..23fa86e7f5a 100644 --- a/packages/grafana-ui/src/components/Segment/Segment.tsx +++ b/packages/grafana-ui/src/components/Segment/Segment.tsx @@ -11,6 +11,7 @@ export interface SegmentSyncProps extends SegmentProps, Omit; onChange: (item: SelectableValue) => void; options: Array>; + inputMinWidth?: number; } export function Segment({ @@ -20,12 +21,16 @@ export function Segment({ Component, className, allowCustomValue, + allowEmptyValue, placeholder, disabled, inputMinWidth, + inputPlaceholder, + onExpandedChange, + autofocus = false, ...rest }: React.PropsWithChildren>) { - const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false); + const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange); const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth; const styles = useStyles(getSegmentStyles); @@ -59,10 +64,12 @@ export function Segment({ setExpanded(false)} allowCustomValue={allowCustomValue} + allowEmptyValue={allowEmptyValue} onChange={(item) => { setExpanded(false); onChange(item); diff --git a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx index 84cbcca4cf5..ffe4a60d2d8 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentAsync.tsx @@ -15,6 +15,7 @@ export interface SegmentAsyncProps extends SegmentProps, Omit Promise>>; onChange: (item: SelectableValue) => void; noOptionMessageHandler?: (state: AsyncState>>) => string; + inputMinWidth?: number; } export function SegmentAsync({ @@ -24,14 +25,18 @@ export function SegmentAsync({ Component, className, allowCustomValue, + allowEmptyValue, disabled, placeholder, inputMinWidth, + inputPlaceholder, + autofocus = false, + onExpandedChange, noOptionMessageHandler = mapStateToNoOptionsMessage, ...rest }: React.PropsWithChildren>) { const [state, fetchOptions] = useAsyncFn(loadOptions, [loadOptions]); - const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(false); + const [Label, labelWidth, expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange); const width = inputMinWidth ? Math.max(inputMinWidth, labelWidth) : labelWidth; const styles = useStyles(getSegmentStyles); @@ -66,10 +71,12 @@ export function SegmentAsync({ { setExpanded(false); }} diff --git a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx index a3f8e3c23c5..286c4b25288 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentInput.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentInput.tsx @@ -10,7 +10,6 @@ import { useStyles } from '../../themes'; export interface SegmentInputProps extends SegmentProps, Omit, 'value' | 'onChange'> { value: string | number; onChange: (text: string | number) => void; - autofocus?: boolean; } const FONT_SIZE = 14; @@ -21,14 +20,16 @@ export function SegmentInput({ Component, className, placeholder, + inputPlaceholder, disabled, autofocus = false, + onExpandedChange, ...rest }: React.PropsWithChildren>) { const ref = useRef(null); const [value, setValue] = useState(initialValue); const [inputWidth, setInputWidth] = useState(measureText((initialValue || '').toString(), FONT_SIZE).width); - const [Label, , expanded, setExpanded] = useExpandableLabel(autofocus); + const [Label, , expanded, setExpanded] = useExpandableLabel(autofocus, onExpandedChange); const styles = useStyles(getSegmentStyles); useClickAway(ref, () => { @@ -71,6 +72,7 @@ export function SegmentInput({ autoFocus className={cx(`gf-form gf-form-input`, inputWidthStyle)} value={value} + placeholder={inputPlaceholder} onChange={(item) => { const { width } = measureText(item.target.value, FONT_SIZE); setInputWidth(width); diff --git a/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx b/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx index fd514eefb30..5614b0beab1 100644 --- a/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx +++ b/packages/grafana-ui/src/components/Segment/SegmentSelect.tsx @@ -15,17 +15,25 @@ export interface Props extends Omit, 'value' | 'onC width: number; noOptionsMessage?: string; allowCustomValue?: boolean; + /** + * If true, empty value will be passed to onChange callback otherwise using empty value + * will work as canceling and using the previous value + */ + allowEmptyValue?: boolean; + placeholder?: string; } /** @internal */ export function SegmentSelect({ value, + placeholder = '', options = [], onChange, onClickOutside, width: widthPixels, noOptionsMessage = '', allowCustomValue = false, + allowEmptyValue = false, ...rest }: React.PropsWithChildren>) { const ref = useRef(null); @@ -38,7 +46,7 @@ export function SegmentSelect({ '; - - return { - restrict: 'A', - link: function postLink($scope: any, elem: JQuery) { - const $funcLink = $(funcSpanTemplate); - const ctrl = $scope.ctrl; - const func = $scope.func; - let scheduledRelink = false; - let paramCountAtLink = 0; - let cancelBlur: any = null; - - ctrl.handleRemoveFunction = (func: any) => { - ctrl.dispatch(actions.removeFunction({ func })); - }; - - ctrl.handleMoveLeft = (func: any) => { - ctrl.dispatch(actions.moveFunction({ func, offset: -1 })); - }; - - ctrl.handleMoveRight = (func: any) => { - ctrl.dispatch(actions.moveFunction({ func, offset: 1 })); - }; - - function clickFuncParam(this: any, paramIndex: any) { - const $link = $(this); - const $comma = $link.prev('.comma'); - const $input = $link.next(); - - $input.val(func.params[paramIndex]); - - $comma.removeClass('query-part__last'); - $link.hide(); - $input.show(); - $input.focus(); - $input.select(); - - const typeahead = $input.data('typeahead'); - if (typeahead) { - $input.val(''); - typeahead.lookup(); - } - } - - function scheduledRelinkIfNeeded() { - if (paramCountAtLink === func.params.length) { - return; - } - - if (!scheduledRelink) { - scheduledRelink = true; - setTimeout(() => { - relink(); - scheduledRelink = false; - }, 200); - } - } - - function paramDef(index: number) { - if (index < func.def.params.length) { - return func.def.params[index]; - } - if ((last(func.def.params) as any).multiple) { - return assign({}, last(func.def.params), { optional: true }); - } - return {}; - } - - function switchToLink(inputElem: HTMLElement, paramIndex: any) { - const $input = $(inputElem); - - clearTimeout(cancelBlur); - cancelBlur = null; - - const $link = $input.prev(); - const $comma = $link.prev('.comma'); - const newValue = $input.val() as any; - - // remove optional empty params - if (newValue !== '' || paramDef(paramIndex).optional) { - func.updateParam(newValue, paramIndex); - $link.html(newValue ? templateSrv.highlightVariablesAsHtml(newValue) : ' '); - } - - scheduledRelinkIfNeeded(); - - $scope.$apply(() => { - // WIP: at the moment function params are mutated directly by func_editor - // after migrating to react it will be done by passing param value to - // updateFunctionParam action - ctrl.dispatch(actions.updateFunctionParam({ func })); - }); - - if ($link.hasClass('query-part__last') && newValue === '') { - $comma.addClass('query-part__last'); - } else { - $link.removeClass('query-part__last'); - } - - $input.hide(); - $link.show(); - } - - // this = input element - function inputBlur(this: any, paramIndex: any) { - const inputElem = this; - // happens long before the click event on the typeahead options - // need to have long delay because the blur - cancelBlur = setTimeout(() => { - switchToLink(inputElem, paramIndex); - }, 200); - } - - function inputKeyPress(this: any, paramIndex: any, e: any) { - if (e.which === 13) { - $(this).blur(); - } - } - - function inputKeyDown(this: any) { - this.style.width = (3 + this.value.length) * 8 + 'px'; - } - - function addTypeahead($input: any, paramIndex: any) { - $input.attr('data-provide', 'typeahead'); - - let options = paramDef(paramIndex).options; - if (paramDef(paramIndex).type === 'int') { - options = map(options, (val) => { - return val.toString(); - }); - } - - $input.typeahead({ - source: options, - minLength: 0, - items: 20, - updater: (value: any) => { - $input.val(value); - switchToLink($input[0], paramIndex); - return value; - }, - }); - - const typeahead = $input.data('typeahead'); - typeahead.lookup = function () { - this.query = this.$element.val() || ''; - return this.process(this.source); - }; - } - - function addElementsAndCompile() { - $funcLink.appendTo(elem); - - if (func.def.unknown) { - elem.addClass('unknown-function'); - } - - const defParams: any = clone(func.def.params); - const lastParam: any = last(func.def.params); - - while (func.params.length >= defParams.length && lastParam && lastParam.multiple) { - defParams.push(assign({}, lastParam, { optional: true })); - } - - each(defParams, (param: any, index: number) => { - if (param.optional && func.params.length < index) { - return false; - } - - let paramValue = templateSrv.highlightVariablesAsHtml(func.params[index]); - const hasValue = paramValue !== null && paramValue !== undefined && paramValue !== ''; - const last = index >= func.params.length - 1 && param.optional && !hasValue; - let linkClass = 'query-part__link'; - - if (last) { - linkClass += ' query-part__last'; - } - - if (last && param.multiple) { - paramValue = '+'; - } else if (!hasValue) { - // for params with no value default to param name - paramValue = param.name; - linkClass += ' query-part__link--no-value'; - } - - if (index > 0) { - $(', ').appendTo(elem); - } - - const $paramLink = $(`${paramValue}`); - const $input = $(paramTemplate); - $input.attr('placeholder', param.name); - - paramCountAtLink++; - - $paramLink.appendTo(elem); - $input.appendTo(elem); - - $input.blur(partial(inputBlur, index)); - $input.keyup(inputKeyDown); - $input.keypress(partial(inputKeyPress, index)); - $paramLink.click(partial(clickFuncParam, index)); - - if (param.options) { - addTypeahead($input, index); - } - - return true; - }); - - $(')').appendTo(elem); - - $compile(elem.contents())($scope); - } - - function ifJustAddedFocusFirstParam() { - if ($scope.func.added) { - $scope.func.added = false; - setTimeout(() => { - elem.find('.query-part__link').first().click(); - }, 10); - } - } - - function relink() { - elem.children().remove(); - addElementsAndCompile(); - ifJustAddedFocusFirstParam(); - } - - relink(); - }, - }; -} - -coreModule.directive('graphiteFuncEditor', graphiteFuncEditor); diff --git a/public/app/plugins/datasource/graphite/gfunc.ts b/public/app/plugins/datasource/graphite/gfunc.ts index 7e3ec63ba24..aee41af7dd3 100644 --- a/public/app/plugins/datasource/graphite/gfunc.ts +++ b/public/app/plugins/datasource/graphite/gfunc.ts @@ -2,7 +2,7 @@ import { assign, each, filter, forEach, get, includes, isString, last, map, toSt import { isVersionGtOrEq } from 'app/core/utils/version'; import { InterpolateFunction } from '@grafana/data'; -type ParamDef = { +export type ParamDef = { name: string; type: string; options?: Array; @@ -990,6 +990,10 @@ export class FuncInstance { def: FuncDef; params: Array; text: any; + /** + * True if this function was just added and not edited yet. It's used to focus on first + * function param to edit it straight away after adding a function. + */ declare added: boolean; /** * Hidden functions are not displayed in UI but available in text editor diff --git a/public/app/plugins/datasource/graphite/graphite_query.ts b/public/app/plugins/datasource/graphite/graphite_query.ts index 7bedf438973..b6099c8ce50 100644 --- a/public/app/plugins/datasource/graphite/graphite_query.ts +++ b/public/app/plugins/datasource/graphite/graphite_query.ts @@ -192,6 +192,9 @@ export default class GraphiteQuery { this.updateRenderedTarget(target, targets); } } + + // clean-up added param + this.functions.forEach((func) => (func.added = false)); } updateRenderedTarget(target: { refId: string | number; target: any; targetFull: any }, targets: any) { diff --git a/public/app/plugins/datasource/graphite/partials/query.editor.html b/public/app/plugins/datasource/graphite/partials/query.editor.html index 3b3d5b88390..45011ea7be2 100644 --- a/public/app/plugins/datasource/graphite/partials/query.editor.html +++ b/public/app/plugins/datasource/graphite/partials/query.editor.html @@ -66,7 +66,7 @@
- +