From dbda60b3504f27238397fa0323232e80c2d00df8 Mon Sep 17 00:00:00 2001 From: Christopher Henn Date: Mon, 18 Mar 2019 08:41:57 -0700 Subject: [PATCH] Enable selecting variable values in time machine --- CHANGELOG.md | 2 + ui/package-lock.json | 41 ++---- .../components/dropdowns/Dropdown.tsx | 133 ++++++------------ .../components/dropdowns/DropdownButton.scss | 4 + .../dropdowns/test/Dropdown.test.tsx | 20 --- ui/src/shared/components/BoxTooltip.scss | 25 ++++ ui/src/shared/components/BoxTooltip.tsx | 72 ++++++++++ ui/src/timeMachine/actions/queries.ts | 44 ++++-- .../FluxFunctionsToolbar.scss | 66 --------- .../fluxFunctionsToolbar/FunctionTooltip.tsx | 103 -------------- .../FunctionTooltipContents.tsx | 35 +++++ .../fluxFunctionsToolbar/ToolbarFunction.tsx | 33 ++--- .../variableToolbar/VariableItem.tsx | 41 ++++-- .../variableToolbar/VariableToolbar.scss | 4 + .../VariableTooltipContents.tsx | 96 +++++++++++++ ui/src/variables/actions/index.ts | 25 +++- ui/src/variables/reducers/index.ts | 22 +++ ui/src/variables/selectors/index.tsx | 44 ++++++ 18 files changed, 464 insertions(+), 346 deletions(-) create mode 100644 ui/src/shared/components/BoxTooltip.scss create mode 100644 ui/src/shared/components/BoxTooltip.tsx delete mode 100644 ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltip.tsx create mode 100644 ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltipContents.tsx create mode 100644 ui/src/timeMachine/components/variableToolbar/VariableTooltipContents.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index f57f0a89b24..8b5f8ef6176 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,9 @@ ## v2.0.0-alpha.7 [unreleased] ### Features + 1. [12663](https://github.com/influxdata/influxdb/pull/12663): Insert flux function near cursor in flux editor +1. [12678](https://github.com/influxdata/influxdb/pull/12678): Enable the use of variables in the Data Explorer and Cell Editor Overlay ### Bug Fixes diff --git a/ui/package-lock.json b/ui/package-lock.json index 6ca2b7ad1d1..deb8ef36a21 100644 --- a/ui/package-lock.json +++ b/ui/package-lock.json @@ -6106,8 +6106,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -6131,15 +6130,13 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -6156,22 +6153,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -6302,8 +6296,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -6317,7 +6310,6 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -6334,7 +6326,6 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6343,15 +6334,13 @@ "version": "0.0.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/minipass/-/minipass-2.2.4.tgz", "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -6372,7 +6361,6 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -6461,8 +6449,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -6476,7 +6463,6 @@ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -6572,8 +6558,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -6615,7 +6600,6 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -6637,7 +6621,6 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -6686,15 +6669,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.0.2.tgz", "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, diff --git a/ui/src/clockface/components/dropdowns/Dropdown.tsx b/ui/src/clockface/components/dropdowns/Dropdown.tsx index 3720fd65d81..8ce5bad3fdd 100644 --- a/ui/src/clockface/components/dropdowns/Dropdown.tsx +++ b/ui/src/clockface/components/dropdowns/Dropdown.tsx @@ -1,7 +1,6 @@ // Libraries import React, {Component, CSSProperties, MouseEvent} from 'react' import classnames from 'classnames' -import {isUndefined, isNull} from 'lodash' // Components import {ClickOutside} from 'src/shared/components/ClickOutside' @@ -80,9 +79,6 @@ class Dropdown extends Component { const {widthPixels} = this.props const width = widthPixels ? `${widthPixels}px` : '100%' - this.validateChildCount() - this.validateMode() - return (
@@ -133,12 +129,14 @@ class Dropdown extends Component { titleText, buttonTestID, } = this.props + const {expanded} = this.state const children: JSX.Element[] = this.props.children const selectedChild = children.find(child => child.props.id === selectedID) const isLoading = status === ComponentStatus.Loading + let resolvedStatus = status let dropdownLabel if (isLoading) { @@ -147,6 +145,7 @@ class Dropdown extends Component { dropdownLabel = selectedChild.props.children } else { dropdownLabel = titleText + resolvedStatus = ComponentStatus.Disabled } return ( @@ -156,7 +155,7 @@ class Dropdown extends Component { size={buttonSize} icon={icon} onClick={this.toggleMenu} - status={status} + status={resolvedStatus} title={titleText} testID={buttonTestID} > @@ -174,55 +173,52 @@ class Dropdown extends Component { children, testID, } = this.props + const {expanded} = this.state - if (expanded) { - return ( -
- -
- {menuHeader && menuHeader} - {React.Children.map(children, (child: JSX.Element) => { - if (this.childTypeIsValid(child)) { - if (child.type === DropdownItem) { - return ( - - {child.props.children} - - ) - } - - return ( - - ) - } else { - throw new Error( - 'Expected children of type or ' - ) - } - })} -
-
-
- ) + if (!expanded) { + return null } - return null + return ( +
+ +
+ {menuHeader && menuHeader} + {React.Children.map(children, (child: JSX.Element) => { + if (child.type === DropdownItem) { + return ( + + {child.props.children} + + ) + } else if (child.type === DropdownDivider) { + return + } else { + throw new Error( + 'Expected children of type or ' + ) + } + })} +
+
+
+ ) } private get menuStyle(): CSSProperties { @@ -245,48 +241,11 @@ class Dropdown extends Component { } } - private get shouldHaveChildren(): boolean { - const {status} = this.props - - return ( - status === ComponentStatus.Default || status === ComponentStatus.Valid - ) - } - private handleItemClick = (value: any): void => { const {onChange} = this.props onChange(value) this.collapseMenu() } - - private validateChildCount = (): void => { - const {children} = this.props - - if (this.shouldHaveChildren && React.Children.count(children) === 0) { - throw new Error( - 'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.' - ) - } - } - - private validateMode = (): void => { - const {mode, selectedID, titleText} = this.props - - if (mode === DropdownMode.ActionList && titleText === '') { - throw new Error('Dropdowns in ActionList mode require a titleText prop.') - } - - if ( - mode === DropdownMode.Radio && - this.shouldHaveChildren && - (isUndefined(selectedID) || isNull(selectedID)) - ) { - throw new Error('Dropdowns in Radio mode require a selectedID prop.') - } - } - - private childTypeIsValid = (child: JSX.Element): boolean => - child.type === DropdownItem || child.type === DropdownDivider } export default Dropdown diff --git a/ui/src/clockface/components/dropdowns/DropdownButton.scss b/ui/src/clockface/components/dropdowns/DropdownButton.scss index 576f4dd74e3..053d2e01322 100644 --- a/ui/src/clockface/components/dropdowns/DropdownButton.scss +++ b/ui/src/clockface/components/dropdowns/DropdownButton.scss @@ -55,6 +55,10 @@ } } +.dropdown--button.button-disabled { + font-style: italic; +} + .dropdown--button.button-xs { @include buttonSizing($form-xs-padding, $form-xs-font); } diff --git a/ui/src/clockface/components/dropdowns/test/Dropdown.test.tsx b/ui/src/clockface/components/dropdowns/test/Dropdown.test.tsx index 1bf67519469..edff48ec071 100644 --- a/ui/src/clockface/components/dropdowns/test/Dropdown.test.tsx +++ b/ui/src/clockface/components/dropdowns/test/Dropdown.test.tsx @@ -79,24 +79,4 @@ describe('Dropdown', () => { expect(actualProps).toEqual(expectedProps) }) }) - - describe('when no children are present', () => { - const errorLog = console.error - - beforeEach(() => { - console.error = jest.fn(() => {}) - }) - - afterEach(() => { - console.error = errorLog - }) - - it('throws error', () => { - expect(() => { - wrapperSetup({children: null}) - }).toThrow( - 'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.' - ) - }) - }) }) diff --git a/ui/src/shared/components/BoxTooltip.scss b/ui/src/shared/components/BoxTooltip.scss new file mode 100644 index 00000000000..e21e56076ed --- /dev/null +++ b/ui/src/shared/components/BoxTooltip.scss @@ -0,0 +1,25 @@ +@import 'src/style/modules'; + +$box-tooltip--caret-size: 6px; + +.box-tooltip { + position: fixed; + visibility: hidden; + z-index: $z--dygraph-legend; + border: $ix-border solid $c-pool; + background-color: $g1-raven; + border-radius: $radius; + padding: 10px; + font-size: 13px; +} + +.box-tooltip.left .box-tooltip--caret { + position: absolute; + right: -$box-tooltip--caret-size; + transform: translate(0,-50%); + width: 0; + height: 0; + border-top: $box-tooltip--caret-size solid transparent; + border-bottom: $box-tooltip--caret-size solid transparent; + border-left: $box-tooltip--caret-size solid $c-pool; +} diff --git a/ui/src/shared/components/BoxTooltip.tsx b/ui/src/shared/components/BoxTooltip.tsx new file mode 100644 index 00000000000..a065a3db50a --- /dev/null +++ b/ui/src/shared/components/BoxTooltip.tsx @@ -0,0 +1,72 @@ +// Libraries +import React, {useRef, useLayoutEffect, FunctionComponent} from 'react' +import {createPortal} from 'react-dom' + +// Constants +import {TOOLTIP_PORTAL_ID} from 'src/shared/components/TooltipPortal' + +// Styles +import 'src/shared/components/BoxTooltip.scss' + +interface Props { + triggerRect: DOMRect + children: JSX.Element +} + +const BoxTooltip: FunctionComponent = ({triggerRect, children}) => { + const ref = useRef(null) + + // Position the tooltip after it has rendered, taking into account its size + useLayoutEffect(() => { + const el = ref.current + + if (!el || !triggerRect) { + return + } + + const rect = el.getBoundingClientRect() + + // Always position the tooltip to the left of the trigger position + const left = triggerRect.left - rect.width + + // Attempt to position the vertical midpoint of tooltip next to the + // vertical midpoint of the trigger rectangle + let top = triggerRect.top + triggerRect.height / 2 - rect.height / 2 + + // If the tooltip overflows the top of the screen, align the top of + // the tooltip with the top of the screen + if (top < 0) { + top = 0 + } + + // If the tooltip overflows the bottom of the screen, align the bottom of + // the tooltip with the bottom of the screen + if (top + rect.height > window.innerHeight) { + top = window.innerHeight - rect.height + } + + el.setAttribute( + 'style', + `visibility: visible; top: ${top}px; left: ${left}px;` + ) + + // Position the caret (little arrow on the side of the tooltip) so that it + // points to the vertical midpoint of the trigger rectangle + const caretTop = triggerRect.top + triggerRect.height / 2 - top + + el.querySelector('.box-tooltip--caret').setAttribute( + 'style', + `top: ${caretTop}px;` + ) + }) + + return createPortal( +
+ {children} +
+
, + document.querySelector(`#${TOOLTIP_PORTAL_ID}`) + ) +} + +export default BoxTooltip diff --git a/ui/src/timeMachine/actions/queries.ts b/ui/src/timeMachine/actions/queries.ts index e7c096468a9..557a74cecd6 100644 --- a/ui/src/timeMachine/actions/queries.ts +++ b/ui/src/timeMachine/actions/queries.ts @@ -1,6 +1,3 @@ -// Libraries -import {get} from 'lodash' - // API import { executeQueryWithVars, @@ -8,7 +5,7 @@ import { } from 'src/shared/apis/query' // Actions -import {refreshVariableValues} from 'src/variables/actions' +import {refreshVariableValues, selectValue} from 'src/variables/actions' // Utils import {getActiveTimeMachine} from 'src/timeMachine/selectors' @@ -16,7 +13,11 @@ import {getActiveOrg} from 'src/organizations/selectors' import {getVariableAssignments} from 'src/variables/selectors' import {getTimeRangeVars} from 'src/variables/utils/getTimeRangeVars' import {filterUnusedVars} from 'src/shared/utils/filterUnusedVars' -import {getVariablesForOrg} from 'src/variables/selectors' +import { + getVariablesForOrg, + getVariable, + getHydratedVariables, +} from 'src/variables/selectors' // Types import {WrappedCancelablePromise, CancellationError} from 'src/types/promises' @@ -68,13 +69,11 @@ export const refreshTimeMachineVariableValues = () => async ( // Find variables whose values have already been loaded by the TimeMachine // (regardless of whether these variables are currently being used) - const existingVariableIDs = Object.keys( - get(getState(), `variables.values.${contextID}.values`, {}) - ) + const hydratedVariables = getHydratedVariables(getState(), contextID) // Refresh values for all variables with existing values and in use variables const variablesToRefresh = variables.filter( - v => variablesInUse.includes(v) || existingVariableIDs.includes(v.id) + v => variablesInUse.includes(v) || hydratedVariables.includes(v) ) await dispatch(refreshVariableValues(contextID, orgID, variablesToRefresh)) @@ -140,3 +139,30 @@ export const saveAndExecuteQueries = () => async dispatch => { dispatch(saveDraftQueries()) dispatch(executeQueries()) } + +export const addVariableToTimeMachine = (variableID: string) => async ( + dispatch, + getState: GetState +) => { + const contextID = getState().timeMachines.activeTimeMachineID + const orgID = getActiveOrg(getState()).id + + const variable = getVariable(getState(), variableID) + const variables = getHydratedVariables(getState(), contextID) + + if (!variables.includes(variable)) { + variables.push(variable) + } + + await dispatch(refreshVariableValues(contextID, orgID, variables)) +} + +export const selectVariableValue = ( + variableID: string, + selectedValue: string +) => async (dispatch, getState: GetState) => { + const contextID = getState().timeMachines.activeTimeMachineID + + dispatch(selectValue(contextID, variableID, selectedValue)) + dispatch(executeQueries()) +} diff --git a/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss b/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss index e825580f08a..6f6618e7c01 100644 --- a/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss +++ b/ui/src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss @@ -46,36 +46,11 @@ padding: 15px; } -.flux-functions-toolbar--tooltip { - padding-right: 8px; - max-width: 600px; - position: fixed; - transform: translateY(50%); - z-index: 10; -} - -.flux-functions-toolbar--tooltip-caret { - content: ""; - width: 0; - height: 0; - position: fixed; - border-top: 6px solid transparent; - border-bottom: 6px solid transparent; - border-left: 6px solid $c-pool; - margin-top: 9px; - margin-left: -8px; - z-index: 9999; -} - .flux-functions-toolbar--tooltip-contents { - border: $ix-border solid $c-pool; - border-radius: $radius; width: 100%; max-width: 600px; display: inline-flex; align-items: center; - background-color: $g1-raven; - padding: 10px; article { margin-bottom: $ix-marg-c; @@ -112,44 +87,3 @@ margin: $ix-marg-a 0 $ix-marg-c 0; } } - - -.flux-functions-toolbar--tooltip-dismiss { - position: absolute; - z-index: 5000; - top: 0; - right: 0; - transform: translate(0,-50%); - width: 24px; - height: 24px; - outline: none; - border-radius: 50%; - background-color: $c-pool; - transition: background-color 0.25s ease; - border: 0; - - &:before, - &:after { - content: ''; - position: absolute; - width: 13px; - height: 3px; - top: 50%; - left: 50%; - border-radius: 1px; - background-color: $g20-white; - } - - &:before { - transform: translate(-50%, -50%) rotate(45deg); - } - - &:after { - transform: translate(-50%, -50%) rotate(-45deg); - } - - &:hover { - background-color: $c-laser; - cursor: pointer; - } -} diff --git a/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltip.tsx b/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltip.tsx deleted file mode 100644 index 9a916512044..00000000000 --- a/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltip.tsx +++ /dev/null @@ -1,103 +0,0 @@ -// Libraries -import React, {PureComponent, MouseEvent, CSSProperties, createRef} from 'react' - -// Components -import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' -import TooltipDescription from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipDescription' -import TooltipArguments from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipArguments' -import TooltipExample from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipExample' -import TooltipLink from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipLink' -import {ErrorHandling} from 'src/shared/decorators/errors' - -// Types -import {FluxToolbarFunction} from 'src/types/shared' - -interface Props { - func: FluxToolbarFunction - onDismiss: () => void - tipPosition?: {top: number; right: number} -} - -interface State { - bottomPosition: number | null -} - -const MAX_HEIGHT = 400 - -class FunctionTooltip extends PureComponent { - public state: State = {bottomPosition: null} - - private tooltipRef = createRef() - - public componentDidMount() { - const {bottom, height} = this.tooltipRef.current.getBoundingClientRect() - - if (bottom > window.innerHeight) { - this.setState({bottomPosition: height / 2}) - } - } - - public render() { - const {desc, args, example, link} = this.props.func - - return ( - <> -
-
- - - ) - } - - private get styleCaretPosition(): CSSProperties { - const {top, right} = this.props.tipPosition - - return { - top: `${Math.min(top, window.innerHeight)}px`, - right: `${right + 4}px`, - } - } - - private get stylePosition(): CSSProperties { - const {top, right} = this.props.tipPosition - const {bottomPosition} = this.state - - return { - bottom: `${bottomPosition || window.innerHeight - top - 15}px`, - right: `${right + 2}px`, - } - } - - private handleDismiss = (e: MouseEvent) => { - const {onDismiss} = this.props - - e.preventDefault() - e.stopPropagation() - onDismiss() - } -} - -export default ErrorHandling(FunctionTooltip) diff --git a/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltipContents.tsx b/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltipContents.tsx new file mode 100644 index 00000000000..cd312fb9c72 --- /dev/null +++ b/ui/src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltipContents.tsx @@ -0,0 +1,35 @@ +// Libraries +import React, {FunctionComponent} from 'react' + +// Components +import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar' +import TooltipDescription from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipDescription' +import TooltipArguments from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipArguments' +import TooltipExample from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipExample' +import TooltipLink from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipLink' + +// Types +import {FluxToolbarFunction} from 'src/types/shared' + +const MAX_HEIGHT = 400 + +interface Props { + func: FluxToolbarFunction +} + +const FunctionTooltipContents: FunctionComponent = ({ + func: {desc, args, example, link}, +}) => { + return ( +
+ + + + + + +
+ ) +} + +export default FunctionTooltipContents diff --git a/ui/src/timeMachine/components/fluxFunctionsToolbar/ToolbarFunction.tsx b/ui/src/timeMachine/components/fluxFunctionsToolbar/ToolbarFunction.tsx index 30974eeab51..3ca98fc2723 100644 --- a/ui/src/timeMachine/components/fluxFunctionsToolbar/ToolbarFunction.tsx +++ b/ui/src/timeMachine/components/fluxFunctionsToolbar/ToolbarFunction.tsx @@ -2,7 +2,8 @@ import React, {PureComponent, createRef} from 'react' // Component -import FunctionTooltip from 'src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltip' +import FunctionTooltipContents from 'src/timeMachine/components/fluxFunctionsToolbar/FunctionTooltipContents' +import BoxTooltip from 'src/shared/components/BoxTooltip' // Types import {FluxToolbarFunction} from 'src/types/shared' @@ -15,7 +16,6 @@ interface Props { interface State { isActive: boolean - hoverPosition: {top: number; right: number} } class ToolbarFunction extends PureComponent { @@ -23,11 +23,13 @@ class ToolbarFunction extends PureComponent { testID: 'toolbar-function', } - public state: State = {isActive: false, hoverPosition: undefined} + public state: State = {isActive: false} + private functionRef = createRef() public render() { const {func, testID} = this.props + const {isActive} = this.state return (
{ onMouseLeave={this.handleStopHover} data-testid={testID} > - {this.tooltip} + {isActive && ( + + + + )}
{ ) } - private get tooltip(): JSX.Element | null { - if (this.state.isActive) { - return ( - - ) + private get domRect(): DOMRect { + if (!this.functionRef.current) { + return null } - return null + return this.functionRef.current.getBoundingClientRect() as DOMRect } private get helperText(): JSX.Element | null { @@ -73,10 +73,7 @@ class ToolbarFunction extends PureComponent { } private handleHover = () => { - const {top, left} = this.functionRef.current.getBoundingClientRect() - const right = window.innerWidth - left - - this.setState({isActive: true, hoverPosition: {top, right}}) + this.setState({isActive: true}) } private handleStopHover = () => { diff --git a/ui/src/timeMachine/components/variableToolbar/VariableItem.tsx b/ui/src/timeMachine/components/variableToolbar/VariableItem.tsx index cd97cb49a4c..6eaec83df51 100644 --- a/ui/src/timeMachine/components/variableToolbar/VariableItem.tsx +++ b/ui/src/timeMachine/components/variableToolbar/VariableItem.tsx @@ -1,25 +1,42 @@ // Libraries -import React, {PureComponent} from 'react' +import React, {FunctionComponent, useRef, useState} from 'react' + +// Components +import VariableTooltipContents from 'src/timeMachine/components/variableToolbar/VariableTooltipContents' +import BoxTooltip from 'src/shared/components/BoxTooltip' // Types import {Variable} from '@influxdata/influx' -// Styles -import 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss' - interface Props { variable: Variable } -class VariableItem extends PureComponent { - public render() { - const {variable} = this.props - return ( -
-
{variable.name}
-
- ) +const VariableItem: FunctionComponent = ({variable}) => { + const trigger = useRef(null) + const [tooltipVisible, setTooltipVisible] = useState(false) + + let triggerRect: DOMRect = null + + if (trigger.current) { + triggerRect = trigger.current.getBoundingClientRect() as DOMRect } + + return ( +
setTooltipVisible(true)} + onMouseLeave={() => setTooltipVisible(false)} + ref={trigger} + > +
{variable.name}
+ {tooltipVisible && ( + + + + )} +
+ ) } export default VariableItem diff --git a/ui/src/timeMachine/components/variableToolbar/VariableToolbar.scss b/ui/src/timeMachine/components/variableToolbar/VariableToolbar.scss index 6cf3b60d844..4cd558e6e54 100644 --- a/ui/src/timeMachine/components/variableToolbar/VariableToolbar.scss +++ b/ui/src/timeMachine/components/variableToolbar/VariableToolbar.scss @@ -40,3 +40,7 @@ font-weight: 600; color: $g18-cloud; } + +.variable-tooltip--contents .form--element { + margin-bottom: 0; +} diff --git a/ui/src/timeMachine/components/variableToolbar/VariableTooltipContents.tsx b/ui/src/timeMachine/components/variableToolbar/VariableTooltipContents.tsx new file mode 100644 index 00000000000..556517d3820 --- /dev/null +++ b/ui/src/timeMachine/components/variableToolbar/VariableTooltipContents.tsx @@ -0,0 +1,96 @@ +// Libraries +import React, {FunctionComponent} from 'react' +import {connect} from 'react-redux' +import {get} from 'lodash' + +// Components +import {Form, Dropdown} from 'src/clockface' + +// Actions +import { + addVariableToTimeMachine, + selectVariableValue, +} from 'src/timeMachine/actions/queries' + +// Utils +import { + getTimeMachineValues, + getTimeMachineValuesStatus, +} from 'src/variables/selectors' +import {toComponentStatus} from 'src/shared/utils/toComponentStatus' + +// Types +import {RemoteDataState} from 'src/types' +import {AppState} from 'src/types/v2' +import {VariableValues} from 'src/variables/types' + +interface StateProps { + values?: VariableValues + valuesStatus: RemoteDataState +} + +interface DispatchProps { + onAddVariableToTimeMachine: typeof addVariableToTimeMachine + onSelectVariableValue: typeof selectVariableValue +} + +interface OwnProps { + variableID: string +} + +type Props = StateProps & DispatchProps & OwnProps + +const VariableTooltipContents: FunctionComponent = ({ + variableID, + values, + valuesStatus, + onAddVariableToTimeMachine, + onSelectVariableValue, +}) => { + const dropdownItems: string[] = get(values, 'values') || [] + + const handleMouseEnter = () => { + if (values || valuesStatus === RemoteDataState.Loading) { + return + } + + onAddVariableToTimeMachine(variableID) + } + + return ( +
+ + onSelectVariableValue(variableID, value)} + > + {dropdownItems.map(value => ( + + {value} + + ))} + + +
+ ) +} + +const mstp = (state: AppState, ownProps: OwnProps) => { + const valuesStatus = getTimeMachineValuesStatus(state) + const values = getTimeMachineValues(state, ownProps.variableID) + + return {values, valuesStatus} +} + +const mdtp = { + onAddVariableToTimeMachine: addVariableToTimeMachine, + onSelectVariableValue: selectVariableValue, +} + +export default connect( + mstp, + mdtp +)(VariableTooltipContents) diff --git a/ui/src/variables/actions/index.ts b/ui/src/variables/actions/index.ts index bac9604bcdc..c71bccaad1b 100644 --- a/ui/src/variables/actions/index.ts +++ b/ui/src/variables/actions/index.ts @@ -23,7 +23,12 @@ import {GetState} from 'src/types/v2' import {Variable} from '@influxdata/influx' import {VariableValuesByID} from 'src/variables/types' -export type Action = SetVariables | SetVariable | RemoveVariable | SetValues +export type Action = + | SetVariables + | SetVariable + | RemoveVariable + | SetValues + | SelectValue interface SetVariables { type: 'SET_VARIABLES' @@ -87,6 +92,24 @@ const setValues = ( payload: {contextID, status, values}, }) +interface SelectValue { + type: 'SELECT_VARIABLE_VALUE' + payload: { + contextID: string + variableID: string + selectedValue: string + } +} + +export const selectValue = ( + contextID: string, + variableID: string, + selectedValue: string +): SelectValue => ({ + type: 'SELECT_VARIABLE_VALUE', + payload: {contextID, variableID, selectedValue}, +}) + export const getVariables = () => async (dispatch: Dispatch) => { try { dispatch(setVariables(RemoteDataState.Loading)) diff --git a/ui/src/variables/reducers/index.ts b/ui/src/variables/reducers/index.ts index aabb9cb2d68..0a1a3a2be43 100644 --- a/ui/src/variables/reducers/index.ts +++ b/ui/src/variables/reducers/index.ts @@ -1,5 +1,6 @@ // Libraries import {produce} from 'immer' +import {get} from 'lodash' // Types import {RemoteDataState} from 'src/types' @@ -88,6 +89,27 @@ export const variablesReducer = ( } else { draftState.values[contextID] = {status, values: null} } + + return + } + + case 'SELECT_VARIABLE_VALUE': { + const {contextID, variableID, selectedValue} = action.payload + + const valuesExist = !!get( + draftState, + `values.${contextID}.values.${variableID}` + ) + + if (!valuesExist) { + return + } + + draftState.values[contextID].values[ + variableID + ].selectedValue = selectedValue + + return } } }) diff --git a/ui/src/variables/selectors/index.tsx b/ui/src/variables/selectors/index.tsx index 3afdb7b9a0b..95fdc27ab1a 100644 --- a/ui/src/variables/selectors/index.tsx +++ b/ui/src/variables/selectors/index.tsx @@ -86,3 +86,47 @@ export const getVariableAssignments = ( state.variables.values[contextID], state.variables.variables ) + +export const getTimeMachineValues = ( + state: AppState, + variableID: string +): VariableValues => { + const activeTimeMachineID = state.timeMachines.activeTimeMachineID + const values = get( + state, + `variables.values.${activeTimeMachineID}.values.${variableID}` + ) + + return values +} + +export const getTimeMachineValuesStatus = ( + state: AppState +): RemoteDataState => { + const activeTimeMachineID = state.timeMachines.activeTimeMachineID + const valuesStatus = get( + state, + `variables.values.${activeTimeMachineID}.status` + ) + + return valuesStatus +} + +export const getVariable = (state: AppState, variableID: string): Variable => { + return get(state, `variables.variables.${variableID}.variable`) +} + +export const getHydratedVariables = ( + state: AppState, + contextID: string +): Variable[] => { + const hydratedVariableIDs: string[] = Object.keys( + get(state, `variables.values.${contextID}.values`, {}) + ) + + const hydratedVariables = Object.values(state.variables.variables) + .map(d => d.variable) + .filter(v => hydratedVariableIDs.includes(v.id)) + + return hydratedVariables +}