diff --git a/ui/src/notebooks/pipes/Query/Resizer.tsx b/ui/src/notebooks/pipes/Query/Resizer.tsx new file mode 100644 index 00000000000..43268edb801 --- /dev/null +++ b/ui/src/notebooks/pipes/Query/Resizer.tsx @@ -0,0 +1,149 @@ +// Libraries +import React, {FC, useRef, useEffect, ReactNode, useState} from 'react' +import classnames from 'classnames' + +// Components +import ResizerHeader from 'src/notebooks/pipes/Query/ResizerHeader' + +// Types +import {Visibility} from 'src/notebooks/pipes/Query' +import {PipeData} from 'src/notebooks/index' + +interface Props { + data: PipeData + onUpdate: (data: any) => void + children: ReactNode + resizingEnabled: boolean +} + +const MINIMUM_RESULTS_PANEL_HEIGHT = 100 + +const Resizer: FC = ({data, onUpdate, children, resizingEnabled}) => { + const height = data.resultsPanelHeight + const visibility = data.resultsVisibility + + const [size, updateSize] = useState(height) + const [isDragging, updateDragging] = useState(false) + const resultsBodyRef = useRef(null) + const dragHandleRef = useRef(null) + + const resultsBodyClassName = classnames('notebook-raw-data--body', { + [`notebook-raw-data--body__${visibility}`]: resizingEnabled && visibility, + }) + + const updateResultsStyle = (): void => { + if (resultsBodyRef.current && resizingEnabled && visibility === 'visible') { + resultsBodyRef.current.setAttribute('style', `height: ${size}px`) + } else { + resultsBodyRef.current.setAttribute('style', '') + } + } + + const handleUpdateVisibility = (resultsVisibility: Visibility): void => { + onUpdate({resultsVisibility}) + } + + const handleUpdateHeight = (resultsPanelHeight: number): void => { + onUpdate({resultsPanelHeight}) + } + + // Ensure results renders with proper height on initial render + useEffect(() => { + updateResultsStyle() + }, []) + + // Update results height when associated props change + useEffect(() => { + updateResultsStyle() + }, [size, visibility, resizingEnabled]) + + // Update local height when context height changes + // so long as it is a different value + useEffect(() => { + if (height !== size) { + updateSize(height) + } + }, [height]) + + // Handle changes in drag state + useEffect(() => { + if (isDragging === true) { + dragHandleRef.current && + dragHandleRef.current.classList.add( + 'notebook-raw-data--drag-handle__dragging' + ) + } + + if (isDragging === false) { + dragHandleRef.current && + dragHandleRef.current.classList.remove( + 'notebook-raw-data--drag-handle__dragging' + ) + handleUpdateHeight(size) + } + }, [isDragging]) + + const handleMouseMove = (e: MouseEvent): void => { + if (!resultsBodyRef.current) { + return + } + + const {pageY} = e + const {top} = resultsBodyRef.current.getBoundingClientRect() + + const updatedHeight = Math.round( + Math.max(pageY - top, MINIMUM_RESULTS_PANEL_HEIGHT) + ) + + updateSize(updatedHeight) + } + + const handleMouseDown = (): void => { + updateDragging(true) + const body = document.getElementsByTagName('body')[0] + body && body.classList.add('notebook-results--dragging') + + window.addEventListener('mousemove', handleMouseMove) + window.addEventListener('mouseup', handleMouseUp) + } + + const handleMouseUp = (): void => { + updateDragging(false) + const body = document.getElementsByTagName('body')[0] + body && body.classList.remove('notebook-results--dragging') + + window.removeEventListener('mousemove', handleMouseMove) + window.removeEventListener('mouseup', handleMouseUp) + } + + let resultsBody = children + + if (!resizingEnabled) { + resultsBody = ( +
+ Run the Flow to see results +
+ ) + } + + if (resizingEnabled && visibility === 'hidden') { + resultsBody =
Results hidden
+ } + + return ( + <> + +
+ {resultsBody} +
+ + ) +} + +export default Resizer diff --git a/ui/src/notebooks/pipes/Query/ResizerHeader.tsx b/ui/src/notebooks/pipes/Query/ResizerHeader.tsx new file mode 100644 index 00000000000..9843809bc1b --- /dev/null +++ b/ui/src/notebooks/pipes/Query/ResizerHeader.tsx @@ -0,0 +1,66 @@ +// Libraries +import React, {FC, RefObject} from 'react' +import classnames from 'classnames' + +// Components +import {Icon, IconFont} from '@influxdata/clockface' + +// Types +import {Visibility} from 'src/notebooks/pipes/Query' + +interface Props { + visibility: Visibility + onUpdateVisibility: (visibility: Visibility) => void + onStartDrag: () => void + resizingEnabled: boolean + dragHandleRef: RefObject +} + +const ResizerHeader: FC = ({ + visibility, + onUpdateVisibility, + onStartDrag, + resizingEnabled, + dragHandleRef, +}) => { + const glyph = visibility === 'visible' ? IconFont.EyeOpen : IconFont.EyeClosed + const className = classnames('notebook-raw-data--header', { + [`notebook-raw-data--header__${visibility}`]: resizingEnabled && visibility, + }) + + if (!resizingEnabled) { + return ( +
+ +
+ ) + } + + const handleToggleVisibility = (): void => { + if (visibility === 'visible') { + onUpdateVisibility('hidden') + } else { + onUpdateVisibility('visible') + } + } + + return ( +
+
+ +
+
+
+
+
+
+
+ ) +} + +export default ResizerHeader diff --git a/ui/src/notebooks/pipes/Query/Results.tsx b/ui/src/notebooks/pipes/Query/Results.tsx index 4cfd5718874..a8787668d8c 100644 --- a/ui/src/notebooks/pipes/Query/Results.tsx +++ b/ui/src/notebooks/pipes/Query/Results.tsx @@ -2,34 +2,26 @@ import React, {FC} from 'react' import {BothResults} from 'src/notebooks/context/query' import {AutoSizer} from 'react-virtualized' -import classnames from 'classnames' // Components import RawFluxDataTable from 'src/timeMachine/components/RawFluxDataTable' -import ResultsHeader from 'src/notebooks/pipes/Query/ResultsHeader' +import Resizer from 'src/notebooks/pipes/Query/Resizer' // Types -import {RawDataSize} from 'src/notebooks/pipes/Query' +import {PipeData} from 'src/notebooks/index' interface Props { + data: PipeData results: BothResults - size: RawDataSize - onUpdateSize: (size: RawDataSize) => void + onUpdate: (data: any) => void } -const Results: FC = ({results, size, onUpdateSize}) => { +const Results: FC = ({results, onUpdate, data}) => { const resultsExist = !!results.raw - const className = classnames('notebook-raw-data', { - [`notebook-raw-data__${size}`]: resultsExist && size, - }) - let resultsBody = ( -
Run the Flow to see results
- ) - - if (resultsExist) { - resultsBody = ( -
+ return ( +
+ {({width, height}) => width && @@ -42,18 +34,7 @@ const Results: FC = ({results, size, onUpdateSize}) => { ) } -
- ) - } - - return ( -
- - {resultsBody} +
) } diff --git a/ui/src/notebooks/pipes/Query/ResultsHeader.tsx b/ui/src/notebooks/pipes/Query/ResultsHeader.tsx deleted file mode 100644 index eb5d73458dd..00000000000 --- a/ui/src/notebooks/pipes/Query/ResultsHeader.tsx +++ /dev/null @@ -1,55 +0,0 @@ -// Libraries -import React, {FC} from 'react' -import classnames from 'classnames' - -// Components -import {Icon, IconFont} from '@influxdata/clockface' - -// Types -import {RawDataSize} from 'src/notebooks/pipes/Query' - -interface Props { - resultsExist: boolean - size: RawDataSize - onUpdateSize: (size: RawDataSize) => void -} - -const ResultsHeader: FC = ({resultsExist, size, onUpdateSize}) => { - if (!resultsExist) { - return ( -
- -
- ) - } - - const handleClick = (newSize: RawDataSize) => (): void => { - onUpdateSize(newSize) - } - - const generateClassName = (buttonSize: RawDataSize): string => { - return classnames('notebook-raw-data--size-toggle', { - [`notebook-raw-data--size-toggle__${buttonSize}`]: buttonSize, - 'notebook-raw-data--size-toggle__active': buttonSize === size, - }) - } - - return ( -
-
-
-
-
- ) -} - -export default ResultsHeader diff --git a/ui/src/notebooks/pipes/Query/index.ts b/ui/src/notebooks/pipes/Query/index.ts index 14653251574..23a1c36b16e 100644 --- a/ui/src/notebooks/pipes/Query/index.ts +++ b/ui/src/notebooks/pipes/Query/index.ts @@ -2,14 +2,15 @@ import {register} from 'src/notebooks' import View from './view' import './style.scss' -export type RawDataSize = 'small' | 'medium' | 'large' +export type Visibility = 'visible' | 'hidden' register({ type: 'query', component: View, button: 'Custom Script', initial: { - rawDataSize: 'small', + resultsVisibility: 'visible', + resultsPanelHeight: 200, activeQuery: 0, queries: [ { diff --git a/ui/src/notebooks/pipes/Query/style.scss b/ui/src/notebooks/pipes/Query/style.scss index 4f876e3390b..e2e39591c23 100644 --- a/ui/src/notebooks/pipes/Query/style.scss +++ b/ui/src/notebooks/pipes/Query/style.scss @@ -5,6 +5,8 @@ $notebook-results-small: 200px; $notebook-results-medium: 400px; $notebook-results-large: 600px; +$notebook-results--drag-handle: 30px; + .notebook-panel--body .flux-editor--monaco { position: relative; @@ -43,72 +45,112 @@ $notebook-results-large: 600px; display: flex; flex-direction: column; align-items: center; + justify-content: space-between; padding: $cf-marg-b 0; } +.notebook-raw-data--vis-toggle { + align-items: center; + justify-content: center; + display: flex; + font-size: 1.5em; + transition: color 0.25s ease; + width: $notebook-results--drag-handle; + height: $notebook-results--drag-handle; + position: relative; + + &:before { + display: inline-block; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +.notebook-raw-data--header__visible .notebook-raw-data--vis-toggle, +.notebook-raw-data--header__hidden .notebook-raw-data--vis-toggle { + &:hover { + cursor: pointer; + } +} + +.notebook-raw-data--header__visible .notebook-raw-data--vis-toggle { + color: $c-pool; + + &:hover { + color: $c-laser; + } +} + +.notebook-raw-data--header__hidden .notebook-raw-data--vis-toggle { + color: $g8-storm; + + &:hover { + color: $g13-mist; + } +} + .notebook-raw-data--body, .notebook-raw-data--empty { position: relative; flex: 1 0 0; } +.notebook-raw-data--body { + display: flex; + align-items: center; +} + +.notebook-raw-data--body__visible { + display: block; +} + .notebook-raw-data--empty { color: $g8-storm; user-select: none; padding: $cf-marg-b 0; font-weight: $cf-font-weight--medium; + height: 30px; } -.notebook-raw-data__success { - height: 200px; -} - -.notebook-raw-data--size-toggle { - border-radius: 50%; - position: relative; - width: 24px; - height: 24px; - margin-bottom: $cf-marg-a; - border: $cf-border solid $g3-castle; - background-color: $g1-raven; - transition: background-color 0.25s ease; - - &:after { - content: ''; - position: absolute; - top: 50%; - left: 50%; - border-radius: 50%; - transition: width 0.25s ease, height 0.25s ease, background-color 0.25s ease; - transform: translate(-50%, -50%); - background-color: $g5-pepper; - } +.notebook-raw-data--drag-handle { + width: $notebook-results--drag-handle; + height: $notebook-results--drag-handle; + display: flex; + align-items: center; + flex-direction: column; + justify-content: center; &:hover { - cursor: pointer; + cursor: ns-resize; + } - &:after { - background-color: $g7-graphite; - } + .notebook-raw-data--header__hidden & { + display: none; } } -.notebook-raw-data--size-toggle__active:after, -.notebook-raw-data--size-toggle__active:hover:after { - background-color: $c-pool; +// Hacky way to ensure the cursor is consistent during drag no matter +// where the cursor is +body.notebook-results--dragging, +body.notebook-results--dragging:hover { + cursor: ns-resize !important; + user-select: none !important; } -.notebook-raw-data--size-toggle__small:after { - width: 8px; - height: 8px; +.notebook-raw-data--drag-icon { + width: $notebook-results--drag-handle / 2; + height: $cf-border; + background-color: $g5-pepper; + transition: background-color 0.25s ease; + margin: $cf-border 0; } -.notebook-raw-data--size-toggle__medium:after { - width: 12px; - height: 12px; +.notebook-raw-data--drag-handle:hover .notebook-raw-data--drag-icon { + background-color: $g13-mist; } -.notebook-raw-data--size-toggle__large:after { - width: 16px; - height: 16px; +.notebook-raw-data--drag-handle__dragging .notebook-raw-data--drag-icon { + background-color: $c-pool; } diff --git a/ui/src/notebooks/pipes/Query/view.tsx b/ui/src/notebooks/pipes/Query/view.tsx index 43ee604642f..67f8a75703f 100644 --- a/ui/src/notebooks/pipes/Query/view.tsx +++ b/ui/src/notebooks/pipes/Query/view.tsx @@ -3,7 +3,6 @@ import React, {FC, useMemo} from 'react' // Types import {PipeProp} from 'src/notebooks' -import {RawDataSize} from 'src/notebooks/pipes/Query' // Components import FluxMonacoEditor from 'src/shared/components/FluxMonacoEditor' @@ -15,7 +14,6 @@ import 'src/notebooks/pipes/Query/style.scss' const Query: FC = ({data, onUpdate, Context, results}) => { const {queries, activeQuery} = data const query = queries[activeQuery] - const size = data.rawDataSize || 'small' function updateText(text) { const _queries = queries.slice() @@ -27,10 +25,6 @@ const Query: FC = ({data, onUpdate, Context, results}) => { onUpdate({queries: _queries}) } - const onUpdateSize = (rawDataSize: RawDataSize): void => { - onUpdate({rawDataSize}) - } - return useMemo( () => ( @@ -40,10 +34,10 @@ const Query: FC = ({data, onUpdate, Context, results}) => { onSubmitScript={() => {}} autogrow /> - + ), - [query.text, results, data.rawDataSize] + [query.text, results, data.resultsVisibility, data.resultsPanelHeight] ) }