From 2397212ffc4d4a5ac0ece4e6a16dae54d479f149 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Apr 2018 12:20:03 -0700 Subject: [PATCH 001/104] Convert ResizeContainer to TypeScript --- ...ResizeContainer.js => ResizeContainer.tsx} | 191 +++++++++--------- 1 file changed, 101 insertions(+), 90 deletions(-) rename ui/src/shared/components/{ResizeContainer.js => ResizeContainer.tsx} (66%) diff --git a/ui/src/shared/components/ResizeContainer.js b/ui/src/shared/components/ResizeContainer.tsx similarity index 66% rename from ui/src/shared/components/ResizeContainer.js rename to ui/src/shared/components/ResizeContainer.tsx index d9aaf53c9b..955af107cf 100644 --- a/ui/src/shared/components/ResizeContainer.js +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -1,8 +1,7 @@ -import React, {Component} from 'react' -import PropTypes from 'prop-types' +import React, {Component, ReactNode} from 'react' import classnames from 'classnames' -import ResizeHandle from 'shared/components/ResizeHandle' +import ResizeHandle from 'src/shared/components/ResizeHandle' import {ErrorHandling} from 'src/shared/decorators/errors' const maximumNumChildren = 2 @@ -11,91 +10,57 @@ const defaultMinBottomHeight = 200 const defaultInitialTopHeight = '50%' const defaultInitialBottomHeight = '50%' +interface State { + isDragging: boolean + topHeight: number + topHeightPixels: number + bottomHeight: number + bottomHeightPixels: number +} + +interface Props { + children: ReactNode + containerClass: string + minTopHeight: number + minBottomHeight: number + initialTopHeight: string + initialBottomHeight: string + theme?: string +} + @ErrorHandling -class ResizeContainer extends Component { +class ResizeContainer extends Component { + public static defaultProps: Partial = { + minTopHeight: defaultMinTopHeight, + minBottomHeight: defaultMinBottomHeight, + initialTopHeight: defaultInitialTopHeight, + initialBottomHeight: defaultInitialBottomHeight, + theme: '', + } + + private topRef: HTMLElement + private bottomRef: HTMLElement + private containerRef: HTMLElement + constructor(props) { super(props) this.state = { isDragging: false, topHeight: props.initialTopHeight, + topHeightPixels: 0, bottomHeight: props.initialBottomHeight, + bottomHeightPixels: 0, } } - static defaultProps = { - minTopHeight: defaultMinTopHeight, - minBottomHeight: defaultMinBottomHeight, - initialTopHeight: defaultInitialTopHeight, - initialBottomHeight: defaultInitialBottomHeight, - } - - componentDidMount() { + public componentDidMount() { this.setState({ - bottomHeightPixels: this.bottom.getBoundingClientRect().height, - topHeightPixels: this.top.getBoundingClientRect().height, + bottomHeightPixels: this.bottomRef.getBoundingClientRect().height, + topHeightPixels: this.topRef.getBoundingClientRect().height, }) } - handleStartDrag = () => { - this.setState({isDragging: true}) - } - - handleStopDrag = () => { - this.setState({isDragging: false}) - } - - handleMouseLeave = () => { - this.setState({isDragging: false}) - } - - handleDrag = e => { - if (!this.state.isDragging) { - return - } - - const {minTopHeight, minBottomHeight} = this.props - const oneHundred = 100 - const containerHeight = parseInt( - getComputedStyle(this.resizeContainer).height, - 10 - ) - // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. - const verticalOffset = window.innerHeight - containerHeight - const newTopPanelPercent = Math.ceil( - (e.pageY - verticalOffset) / containerHeight * oneHundred - ) - const newBottomPanelPercent = oneHundred - newTopPanelPercent - - // Don't trigger a resize unless the change in size is greater than minResizePercentage - const minResizePercentage = 0.5 - if ( - Math.abs(newTopPanelPercent - parseFloat(this.state.topHeight)) < - minResizePercentage - ) { - return - } - - const topHeightPixels = newTopPanelPercent / oneHundred * containerHeight - const bottomHeightPixels = - newBottomPanelPercent / oneHundred * containerHeight - - // Don't trigger a resize if the new sizes are too small - if ( - topHeightPixels < minTopHeight || - bottomHeightPixels < minBottomHeight - ) { - return - } - - this.setState({ - topHeight: `${newTopPanelPercent}%`, - bottomHeight: `${newBottomPanelPercent}%`, - bottomHeightPixels, - topHeightPixels, - }) - } - - render() { + public render() { const { topHeightPixels, bottomHeightPixels, @@ -120,12 +85,14 @@ class ResizeContainer extends Component { onMouseLeave={this.handleMouseLeave} onMouseUp={this.handleStopDrag} onMouseMove={this.handleDrag} - ref={r => (this.resizeContainer = r)} + ref={r => (this.containerRef = r)} >
(this.top = r)} + style={{ + height: `${topHeight}%`, + }} + ref={r => (this.topRef = r)} > {React.cloneElement(children[0], { resizerBottomHeight: bottomHeightPixels, @@ -136,12 +103,12 @@ class ResizeContainer extends Component { theme={theme} isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} - top={topHeight} + top={`${topHeight}%`} />
(this.bottom = r)} + style={{height: `${bottomHeight}%`, top: `${topHeight}%`}} + ref={r => (this.bottomRef = r)} > {React.cloneElement(children[1], { resizerBottomHeight: bottomHeightPixels, @@ -151,18 +118,62 @@ class ResizeContainer extends Component {
) } -} -const {node, number, string} = PropTypes + private handleStartDrag = () => { + this.setState({isDragging: true}) + } -ResizeContainer.propTypes = { - children: node.isRequired, - containerClass: string.isRequired, - minTopHeight: number, - minBottomHeight: number, - initialTopHeight: string, - initialBottomHeight: string, - theme: string, + private handleStopDrag = () => { + this.setState({isDragging: false}) + } + + private handleMouseLeave = () => { + this.setState({isDragging: false}) + } + + private handleDrag = e => { + if (!this.state.isDragging) { + return + } + + const {minTopHeight, minBottomHeight} = this.props + const oneHundred = 100 + const {height} = getComputedStyle(this.containerRef) + const containerHeight = Number(height) + // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. + const verticalOffset = window.innerHeight - containerHeight + const newTopPanelPercent = Math.ceil( + (e.pageY - verticalOffset) / containerHeight * oneHundred + ) + const newBottomPanelPercent = oneHundred - newTopPanelPercent + + // Don't trigger a resize unless the change in size is greater than minResizePercentage + const minResizePercentage = 0.5 + if ( + Math.abs(newTopPanelPercent - this.state.topHeight) < minResizePercentage + ) { + return + } + + const topHeightPixels = newTopPanelPercent / oneHundred * containerHeight + const bottomHeightPixels = + newBottomPanelPercent / oneHundred * containerHeight + + // Don't trigger a resize if the new sizes are too small + if ( + topHeightPixels < minTopHeight || + bottomHeightPixels < minBottomHeight + ) { + return + } + + this.setState({ + topHeight: newTopPanelPercent, + bottomHeight: newBottomPanelPercent, + bottomHeightPixels, + topHeightPixels, + }) + } } export default ResizeContainer From 26bf8dd4cc5a6f8af010e92044155c893b090bde Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Apr 2018 12:23:43 -0700 Subject: [PATCH 002/104] Use getter for percentage heights --- ui/src/shared/components/ResizeContainer.tsx | 23 +++++++++++--------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 955af107cf..dab4c02e1e 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -61,13 +61,7 @@ class ResizeContainer extends Component { } public render() { - const { - topHeightPixels, - bottomHeightPixels, - topHeight, - bottomHeight, - isDragging, - } = this.state + const {topHeightPixels, bottomHeightPixels, isDragging} = this.state const {containerClass, children, theme} = this.props if (React.Children.count(children) > maximumNumChildren) { @@ -90,7 +84,7 @@ class ResizeContainer extends Component {
(this.topRef = r)} > @@ -103,11 +97,14 @@ class ResizeContainer extends Component { theme={theme} isDragging={isDragging} onHandleStartDrag={this.handleStartDrag} - top={`${topHeight}%`} + top={this.percentageHeights.top} />
(this.bottomRef = r)} > {React.cloneElement(children[1], { @@ -119,6 +116,12 @@ class ResizeContainer extends Component { ) } + private get percentageHeights() { + const {topHeight, bottomHeight} = this.state + + return {top: `${topHeight}%`, bottom: `${bottomHeight}%`} + } + private handleStartDrag = () => { this.setState({isDragging: true}) } From faed250f3040d515fbc09872ab90771c1a0e94ef Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Apr 2018 14:13:31 -0700 Subject: [PATCH 003/104] Use render props in Resizer and implement in Data Explorer and Time Machine --- .../data_explorer/containers/DataExplorer.tsx | 64 ++++++++++++------- ui/src/ifql/components/TimeMachine.tsx | 29 +++++++-- ui/src/ifql/components/TimeMachineVis.tsx | 11 ++++ ui/src/shared/components/ResizeContainer.tsx | 61 ++++++++++-------- 4 files changed, 109 insertions(+), 56 deletions(-) create mode 100644 ui/src/ifql/components/TimeMachineVis.tsx diff --git a/ui/src/data_explorer/containers/DataExplorer.tsx b/ui/src/data_explorer/containers/DataExplorer.tsx index e3581e86a8..2d7696036e 100644 --- a/ui/src/data_explorer/containers/DataExplorer.tsx +++ b/ui/src/data_explorer/containers/DataExplorer.tsx @@ -91,12 +91,9 @@ export class DataExplorer extends PureComponent { source, timeRange, autoRefresh, - queryConfigs, - manualRefresh, onManualRefresh, errorThrownAction, writeLineProtocol, - queryConfigActions, handleChooseAutoRefresh, } = this.props @@ -129,30 +126,51 @@ export class DataExplorer extends PureComponent { minBottomHeight={MINIMUM_HEIGHTS.visualization} initialTopHeight={INITIAL_HEIGHTS.queryMaker} initialBottomHeight={INITIAL_HEIGHTS.visualization} - > - - - + renderTop={this.renderTop} + renderBottom={this.renderBottom} + />
) } + private renderTop = () => { + const {source, queryConfigActions} = this.props + return ( + + ) + } + + private renderBottom = () => { + const { + timeRange, + autoRefresh, + queryConfigs, + manualRefresh, + errorThrownAction, + queryConfigActions, + } = this.props + + return ( + + ) + } + private handleCloseWriteData = (): void => { this.setState({showWriteForm: false}) } diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 716c09994b..8d7944239a 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -1,7 +1,9 @@ import React, {PureComponent} from 'react' import BodyBuilder from 'src/ifql/components/BodyBuilder' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' - +import TimeMachineVis from 'src/ifql/components/TimeMachineVis' +import ResizeContainer from 'src/shared/components/ResizeContainer' +import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import { Suggestion, OnChangeScript, @@ -25,6 +27,20 @@ interface Body extends FlatBody { @ErrorHandling class TimeMachine extends PureComponent { public render() { + return ( + + ) + } + + private renderTop = height => { const { body, script, @@ -32,20 +48,21 @@ class TimeMachine extends PureComponent { onSubmitScript, suggestions, } = this.props - return ( -
+
-
- -
+
) } + + private renderBottom = (top, height) => ( + + ) } export default TimeMachine diff --git a/ui/src/ifql/components/TimeMachineVis.tsx b/ui/src/ifql/components/TimeMachineVis.tsx new file mode 100644 index 0000000000..d66234f37e --- /dev/null +++ b/ui/src/ifql/components/TimeMachineVis.tsx @@ -0,0 +1,11 @@ +import React, {SFC} from 'react' + +interface Props { + bottomHeight: number + topHeight: number +} +const TimeMachineVis: SFC = ({bottomHeight, topHeight}) => ( +
+) + +export default TimeMachineVis diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index dab4c02e1e..08773fead4 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -4,6 +4,7 @@ import classnames from 'classnames' import ResizeHandle from 'src/shared/components/ResizeHandle' import {ErrorHandling} from 'src/shared/decorators/errors' +const hundred = 100 const maximumNumChildren = 2 const defaultMinTopHeight = 200 const defaultMinBottomHeight = 200 @@ -19,7 +20,8 @@ interface State { } interface Props { - children: ReactNode + renderTop: (height: number) => ReactNode + renderBottom: (height: number, top: number) => ReactNode containerClass: string minTopHeight: number minBottomHeight: number @@ -62,7 +64,13 @@ class ResizeContainer extends Component { public render() { const {topHeightPixels, bottomHeightPixels, isDragging} = this.state - const {containerClass, children, theme} = this.props + const { + containerClass, + children, + theme, + renderTop, + renderBottom, + } = this.props if (React.Children.count(children) > maximumNumChildren) { console.error( @@ -83,45 +91,46 @@ class ResizeContainer extends Component { >
(this.topRef = r)} > - {React.cloneElement(children[0], { - resizerBottomHeight: bottomHeightPixels, - resizerTopHeight: topHeightPixels, - })} + {renderTop(topHeightPixels)}
(this.bottomRef = r)} > - {React.cloneElement(children[1], { - resizerBottomHeight: bottomHeightPixels, - resizerTopHeight: topHeightPixels, - })} + {renderBottom(topHeightPixels, bottomHeightPixels)}
) } - private get percentageHeights() { + private get topStyle() { + const {topHeight} = this.state + + return {height: `${topHeight}%`} + } + + private get bottomStyle() { const {topHeight, bottomHeight} = this.state return {top: `${topHeight}%`, bottom: `${bottomHeight}%`} } + private get topHandle() { + const {topHeight} = this.state + + return `${topHeight}%` + } + private handleStartDrag = () => { this.setState({isDragging: true}) } @@ -140,15 +149,14 @@ class ResizeContainer extends Component { } const {minTopHeight, minBottomHeight} = this.props - const oneHundred = 100 const {height} = getComputedStyle(this.containerRef) - const containerHeight = Number(height) + const containerHeight = parseInt(height, 10) // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. const verticalOffset = window.innerHeight - containerHeight const newTopPanelPercent = Math.ceil( - (e.pageY - verticalOffset) / containerHeight * oneHundred + (e.pageY - verticalOffset) / containerHeight * hundred ) - const newBottomPanelPercent = oneHundred - newTopPanelPercent + const newBottomPanelPercent = hundred - newTopPanelPercent // Don't trigger a resize unless the change in size is greater than minResizePercentage const minResizePercentage = 0.5 @@ -158,9 +166,8 @@ class ResizeContainer extends Component { return } - const topHeightPixels = newTopPanelPercent / oneHundred * containerHeight - const bottomHeightPixels = - newBottomPanelPercent / oneHundred * containerHeight + const topHeightPixels = newTopPanelPercent / hundred * containerHeight + const bottomHeightPixels = newBottomPanelPercent / hundred * containerHeight // Don't trigger a resize if the new sizes are too small if ( @@ -172,9 +179,9 @@ class ResizeContainer extends Component { this.setState({ topHeight: newTopPanelPercent, + topHeightPixels, bottomHeight: newBottomPanelPercent, bottomHeightPixels, - topHeightPixels, }) } } From 8354a37353cf8de29246504bf5b827901a83ddd1 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Apr 2018 14:13:58 -0700 Subject: [PATCH 004/104] WIP Style Time Machine --- ui/src/ifql/components/BodyBuilder.tsx | 8 +-- ui/src/ifql/components/ExpressionNode.tsx | 2 +- ui/src/ifql/components/FuncNode.tsx | 4 +- ui/src/ifql/containers/IFQLPage.tsx | 20 +++---- ui/src/style/components/func-node.scss | 69 ++++++++++++++++++----- 5 files changed, 68 insertions(+), 35 deletions(-) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index e81855fa64..256d80038b 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -21,8 +21,8 @@ class BodyBuilder extends PureComponent { return b.declarations.map(d => { if (d.funcs) { return ( -
-
{d.name} =
+
+
{d.name}
{ } return ( -
- {b.source} +
+
{b.source}
) }) diff --git a/ui/src/ifql/components/ExpressionNode.tsx b/ui/src/ifql/components/ExpressionNode.tsx index 8ef81749bb..92b84251af 100644 --- a/ui/src/ifql/components/ExpressionNode.tsx +++ b/ui/src/ifql/components/ExpressionNode.tsx @@ -21,7 +21,7 @@ class ExpressionNode extends PureComponent { {({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => { return ( -
+
{funcs.map(func => ( { onGenerateScript={onGenerateScript} /> )} -
- -
+
) } diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 2ad7d99b42..65156bb498 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -69,24 +69,20 @@ export class IFQLPage extends PureComponent {
-
+

Time Machine

-
-
- -
-
+
diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss index 82dcfed978..52937c2200 100644 --- a/ui/src/style/components/func-node.scss +++ b/ui/src/style/components/func-node.scss @@ -1,31 +1,49 @@ -.func-nodes-container { - display: inline-flex; +.declaration { + width: 500px; + margin-bottom: 30px; +} + +.variable-name { + font-size: 13px; + font-family: $code-font; + color: $c-honeydew; + padding: 5px 10px; + background-color: $g3-castle; + border-radius: $radius; + margin-bottom: 2px; + width: 100%; +} + + +.expression-node { + width: 100%; + display: flex; flex-direction: column; } .func-node { + width: 100%; display: flex; + align-items: stretch; + position: relative; + margin-bottom: 2px; } .func-node--name { - background: #252b35; - border-radius: $radius-small; - padding: 10px; - width: auto; - display: flex; + background-color: $g4-onyx; + padding: 10px; color: $ix-text-default; - margin-bottom: $ix-marg-a; - font-family: $ix-text-font; - font-weight: 500; - cursor: pointer; + font-size: 13px; + border-radius: $radius 0 0 $radius; + font-weight: 600; + width: 130px; } .func-args { - background: #252b35; - border-radius: $radius-small; + background-color: $g3-castle; + border-radius: 0 $radius $radius 0; padding: 10px; - margin-bottom: $ix-marg-a; - width: auto; + flex: 1 0 0; display: flex; align-items: stretch; flex-direction: column; @@ -37,3 +55,24 @@ .func-arg { display: flex; } + +.func-node--delete { + position: absolute; + width: 22px; + height: 22px; + top: 0; + right: -26px; + border-radius: 50%; + background-color: $c-curacao; + opacity: 0; + transition: background-color 0.25s ease, opacity 0.25s ease; + + &:hover { + background-color: $c-dreamsicle; + cursor: pointer; + } + + .func-node:hover & { + opacity: 1; + } +} From 900f74e66977930eec1f792865a2b4eb315eca68 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 27 Apr 2018 14:14:19 -0700 Subject: [PATCH 005/104] Use cheap eval instead of regular source map --- ui/webpack/dev.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/webpack/dev.config.js b/ui/webpack/dev.config.js index 7d575003c9..3ebbccb851 100644 --- a/ui/webpack/dev.config.js +++ b/ui/webpack/dev.config.js @@ -36,7 +36,7 @@ module.exports = { }, watch: true, cache: true, - devtool: 'source-map', + devtool: 'cheap-eval-source-map', entry: { app: path.resolve(__dirname, '..', 'src', 'index.tsx'), }, From b73abca28d9a7fa8197ea88a21320a95e0ee8112 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 09:11:11 -0700 Subject: [PATCH 006/104] WIP Redesign & Refactor Resizer --- ui/src/ifql/components/TimeMachine.tsx | 79 +++---- ui/src/ifql/components/TimeMachineEditor.tsx | 29 +-- ui/src/ifql/components/TimeMachineVis.tsx | 7 +- ui/src/ifql/containers/IFQLPage.tsx | 9 +- ui/src/shared/components/ResizeContainer.tsx | 209 +++++++++---------- ui/src/shared/components/ResizeDivision.tsx | 62 ++++++ ui/src/shared/components/ResizeHandle.tsx | 54 +++-- ui/src/shared/constants/index.tsx | 5 + ui/src/style/components/resizer.scss | 83 +++++--- 9 files changed, 317 insertions(+), 220 deletions(-) create mode 100644 ui/src/shared/components/ResizeDivision.tsx diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 8d7944239a..608af50fef 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -2,21 +2,14 @@ import React, {PureComponent} from 'react' import BodyBuilder from 'src/ifql/components/BodyBuilder' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineVis from 'src/ifql/components/TimeMachineVis' -import ResizeContainer from 'src/shared/components/ResizeContainer' -import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' -import { - Suggestion, - OnChangeScript, - OnSubmitScript, - FlatBody, -} from 'src/types/ifql' +import Resizer from 'src/shared/components/ResizeContainer' +import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { script: string suggestions: Suggestion[] body: Body[] - onSubmitScript: OnSubmitScript onChangeScript: OnChangeScript } @@ -28,41 +21,53 @@ interface Body extends FlatBody { class TimeMachine extends PureComponent { public render() { return ( - ) } - private renderTop = height => { - const { - body, - script, - onChangeScript, - onSubmitScript, - suggestions, - } = this.props - return ( -
- - -
- ) + private get divisions() { + return [ + { + minSize: 200, + render: () => ( + + ), + }, + { + minSize: 200, + render: () => , + }, + ] } - private renderBottom = (top, height) => ( - - ) + private get renderEditorDivisions() { + const {script, body, suggestions, onChangeScript} = this.props + + return [ + { + name: 'IFQL', + render: () => ( + + ), + }, + { + name: 'Builder', + render: () => , + }, + { + name: 'Schema Explorer', + render: () =>
Weeeeee
, + }, + ] + } } export default TimeMachine diff --git a/ui/src/ifql/components/TimeMachineEditor.tsx b/ui/src/ifql/components/TimeMachineEditor.tsx index e448f3e49f..1251f496a1 100644 --- a/ui/src/ifql/components/TimeMachineEditor.tsx +++ b/ui/src/ifql/components/TimeMachineEditor.tsx @@ -3,12 +3,11 @@ import {Controlled as CodeMirror, IInstance} from 'react-codemirror2' import {EditorChange} from 'codemirror' import 'src/external/codemirror' import {ErrorHandling} from 'src/shared/decorators/errors' -import {OnSubmitScript, OnChangeScript} from 'src/types/ifql' +import {OnChangeScript} from 'src/types/ifql' interface Props { script: string onChangeScript: OnChangeScript - onSubmitScript: OnSubmitScript } @ErrorHandling @@ -28,23 +27,15 @@ class TimeMachineEditor extends PureComponent { } return ( -
-
- -
- +
+
) } diff --git a/ui/src/ifql/components/TimeMachineVis.tsx b/ui/src/ifql/components/TimeMachineVis.tsx index d66234f37e..fe73eeeba8 100644 --- a/ui/src/ifql/components/TimeMachineVis.tsx +++ b/ui/src/ifql/components/TimeMachineVis.tsx @@ -1,11 +1,8 @@ import React, {SFC} from 'react' interface Props { - bottomHeight: number - topHeight: number + blob: string } -const TimeMachineVis: SFC = ({bottomHeight, topHeight}) => ( -
-) +const TimeMachineVis: SFC = ({blob}) =>
{blob}
export default TimeMachineVis diff --git a/ui/src/ifql/containers/IFQLPage.tsx b/ui/src/ifql/containers/IFQLPage.tsx index 65156bb498..f68386057b 100644 --- a/ui/src/ifql/containers/IFQLPage.tsx +++ b/ui/src/ifql/containers/IFQLPage.tsx @@ -74,13 +74,20 @@ export class IFQLPage extends PureComponent {

Time Machine

+
+ +
diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 08773fead4..3e9dd3798c 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -1,80 +1,58 @@ -import React, {Component, ReactNode} from 'react' +import React, {Component, ReactElement} from 'react' import classnames from 'classnames' +import uuid from 'uuid' import ResizeHandle from 'src/shared/components/ResizeHandle' +import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' - -const hundred = 100 -const maximumNumChildren = 2 -const defaultMinTopHeight = 200 -const defaultMinBottomHeight = 200 -const defaultInitialTopHeight = '50%' -const defaultInitialBottomHeight = '50%' +import {MIN_DIVISIONS, ORIENTATION_HORIZONTAL} from 'src/shared/constants/' interface State { isDragging: boolean - topHeight: number - topHeightPixels: number - bottomHeight: number - bottomHeightPixels: number + divisions: DivisionState[] +} + +interface Division { + name?: string + minSize?: number + render: () => ReactElement +} + +interface DivisionState extends Division { + id: string + size: number + offset: number } interface Props { - renderTop: (height: number) => ReactNode - renderBottom: (height: number, top: number) => ReactNode + divisions: Division[] + orientation: string containerClass: string - minTopHeight: number - minBottomHeight: number - initialTopHeight: string - initialBottomHeight: string - theme?: string } @ErrorHandling -class ResizeContainer extends Component { +class Resizer extends Component { public static defaultProps: Partial = { - minTopHeight: defaultMinTopHeight, - minBottomHeight: defaultMinBottomHeight, - initialTopHeight: defaultInitialTopHeight, - initialBottomHeight: defaultInitialBottomHeight, - theme: '', + orientation: ORIENTATION_HORIZONTAL, } - private topRef: HTMLElement - private bottomRef: HTMLElement - private containerRef: HTMLElement + public containerRef: HTMLElement constructor(props) { super(props) this.state = { isDragging: false, - topHeight: props.initialTopHeight, - topHeightPixels: 0, - bottomHeight: props.initialBottomHeight, - bottomHeightPixels: 0, + divisions: this.initialDivisions, } } - public componentDidMount() { - this.setState({ - bottomHeightPixels: this.bottomRef.getBoundingClientRect().height, - topHeightPixels: this.topRef.getBoundingClientRect().height, - }) - } - public render() { - const {topHeightPixels, bottomHeightPixels, isDragging} = this.state - const { - containerClass, - children, - theme, - renderTop, - renderBottom, - } = this.props + const {isDragging, divisions} = this.state + const {containerClass, orientation} = this.props - if (React.Children.count(children) > maximumNumChildren) { + if (divisions.length < MIN_DIVISIONS) { console.error( - `There cannot be more than ${maximumNumChildren}' children in ResizeContainer` + `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` ) return } @@ -89,46 +67,46 @@ class ResizeContainer extends Component { onMouseMove={this.handleDrag} ref={r => (this.containerRef = r)} > -
(this.topRef = r)} - > - {renderTop(topHeightPixels)} -
- -
(this.bottomRef = r)} - > - {renderBottom(topHeightPixels, bottomHeightPixels)} -
+ {divisions.map(d => ( + + ))} + {divisions.map((d, i) => { + if (i === 0) { + return null + } + return ( + + ) + })}
) } - private get topStyle() { - const {topHeight} = this.state + private get initialDivisions() { + const {divisions} = this.props - return {height: `${topHeight}%`} - } + const size = 1 / divisions.length - private get bottomStyle() { - const {topHeight, bottomHeight} = this.state - - return {top: `${topHeight}%`, bottom: `${bottomHeight}%`} - } - - private get topHandle() { - const {topHeight} = this.state - - return `${topHeight}%` + return divisions.map((d, i) => ({ + ...d, + id: uuid.v4(), + size, + offset: size * i, + })) } private handleStartDrag = () => { @@ -143,47 +121,46 @@ class ResizeContainer extends Component { this.setState({isDragging: false}) } - private handleDrag = e => { + private handleDrag = () => { if (!this.state.isDragging) { return } - const {minTopHeight, minBottomHeight} = this.props - const {height} = getComputedStyle(this.containerRef) - const containerHeight = parseInt(height, 10) - // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. - const verticalOffset = window.innerHeight - containerHeight - const newTopPanelPercent = Math.ceil( - (e.pageY - verticalOffset) / containerHeight * hundred - ) - const newBottomPanelPercent = hundred - newTopPanelPercent + // const {height} = getComputedStyle(this.containerRef) + // const containerHeight = parseInt(height, 10) + // // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. + // const verticalOffset = window.innerHeight - containerHeight + // const newTopPanelPercent = Math.ceil( + // (e.pageY - verticalOffset) / containerHeight * HUNDRED + // ) + // const newBottomPanelPercent = HUNDRED - newTopPanelPercent - // Don't trigger a resize unless the change in size is greater than minResizePercentage - const minResizePercentage = 0.5 - if ( - Math.abs(newTopPanelPercent - this.state.topHeight) < minResizePercentage - ) { - return - } + // // Don't trigger a resize unless the change in size is greater than minResizePercentage + // const minResizePercentage = 0.5 + // if ( + // Math.abs(newTopPanelPercent - this.state.topHeight) < minResizePercentage + // ) { + // return + // } - const topHeightPixels = newTopPanelPercent / hundred * containerHeight - const bottomHeightPixels = newBottomPanelPercent / hundred * containerHeight + // const topHeightPixels = newTopPanelPercent / HUNDRED * containerHeight + // const bottomHeightPixels = newBottomPanelPercent / HUNDRED * containerHeight - // Don't trigger a resize if the new sizes are too small - if ( - topHeightPixels < minTopHeight || - bottomHeightPixels < minBottomHeight - ) { - return - } + // // Don't trigger a resize if the new sizes are too small + // if ( + // topHeightPixels < minTopHeight || + // bottomHeightPixels < minBottomHeight + // ) { + // return + // } - this.setState({ - topHeight: newTopPanelPercent, - topHeightPixels, - bottomHeight: newBottomPanelPercent, - bottomHeightPixels, - }) + // this.setState({ + // topHeight: newTopPanelPercent, + // topHeightPixels, + // bottomHeight: newBottomPanelPercent, + // bottomHeightPixels, + // }) } } -export default ResizeContainer +export default Resizer diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx new file mode 100644 index 0000000000..81dd36600f --- /dev/null +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -0,0 +1,62 @@ +import React, {PureComponent, ReactElement} from 'react' +import classnames from 'classnames' + +import { + ORIENTATION_VERTICAL, + ORIENTATION_HORIZONTAL, + HUNDRED, +} from 'src/shared/constants/' + +interface Props { + id: string + name?: string + size: number + offset: number + orientation: string + render: () => ReactElement +} + +class Division extends PureComponent { + public render() { + const {name, render} = this.props + + return ( +
+ {name &&
{name}
} + {render()} +
+ ) + } + + private get style() { + const {orientation, size, offset} = this.props + + const sizePercent = `${size * HUNDRED}%` + const offsetPercent = `${offset * HUNDRED}%` + + if (orientation === ORIENTATION_VERTICAL) { + return { + top: '0', + width: sizePercent, + left: offsetPercent, + } + } + + return { + left: '0', + height: sizePercent, + top: offsetPercent, + } + } + + private get className(): string { + const {orientation} = this.props + // todo use constants instead of "vertical" / "horizontal" + return classnames('resizer--division', { + resizer__vertical: orientation === ORIENTATION_VERTICAL, + resizer__horizontal: orientation === ORIENTATION_HORIZONTAL, + }) + } +} + +export default Division diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index 09112d3b1c..ebe7a638aa 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -1,27 +1,45 @@ -import React, {SFC} from 'react' +import React, {PureComponent} from 'react' import classnames from 'classnames' +import { + ORIENTATION_VERTICAL, + ORIENTATION_HORIZONTAL, + HUNDRED, +} from 'src/shared/constants/' + interface Props { onHandleStartDrag: () => void isDragging: boolean - theme?: string - top?: string + offset: number + orientation: string } -const ResizeHandle: SFC = ({ - onHandleStartDrag, - isDragging, - theme, - top, -}) => ( -
-) +class ResizeHandle extends PureComponent { + public render() { + const {onHandleStartDrag, isDragging, orientation} = this.props + + return ( +
+ ) + } + + private get style() { + const {orientation, offset} = this.props + + if (orientation === ORIENTATION_VERTICAL) { + return {left: `${offset * HUNDRED}%`} + } + + return {top: `${offset * HUNDRED}%`} + } +} export default ResizeHandle diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index 26acce1c77..ba575404fb 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -477,3 +477,8 @@ export const NOTIFICATION_TRANSITION = 250 export const FIVE_SECONDS = 5000 export const TEN_SECONDS = 10000 export const INFINITE = -1 + +export const HUNDRED = 100 +export const MIN_DIVISIONS = 2 +export const ORIENTATION_VERTICAL = 'vertical' +export const ORIENTATION_HORIZONTAL = 'horizontal' diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 5c3ffaaaa6..75b0ef029f 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -1,6 +1,6 @@ /* - Resizable Container - ---------------------------------------------- + Resizable Container + ------------------------------------------------------------------------------ */ $resizer-line-width: 2px; @@ -22,11 +22,18 @@ $resizer-color-kapacitor: $c-rainforest; @include no-user-select(); } } -.resize--top, -.resize--bottom { + +.resizer--division { + border: 1px solid #f00; position: absolute; - width: 100%; - left: 0; + + &.resizer__horizontal { + width: 100%; + } + + &.resizer__vertical { + height: 100%; + } } .resizer--full-size { @@ -38,16 +45,10 @@ $resizer-color-kapacitor: $c-rainforest; } /* - Resizable Container Handle - ---------------------------------------------- + Resizable Container Handle + ------------------------------------------------------------------------------ */ .resizer--handle { - top: 60%; - left: 0; - height: $resizer-click-area; - margin-top: -$resizer-click-area/2; - margin-bottom: -$resizer-click-area/2; - width: 100%; z-index: 1; user-select: none; -webkit-user-select: none; @@ -80,11 +81,6 @@ $resizer-color-kapacitor: $c-rainforest; content: ''; display: block; position: absolute; - top: 50%; - left: 0; - transform: translateY(-50%); - width: 100%; - height: $resizer-line-width; background-color: $resizer-color; box-shadow: 0 0 0 transparent; transition: @@ -112,11 +108,50 @@ $resizer-color-kapacitor: $c-rainforest; } } -/* Kapacitor Theme */ -.resizer--handle.resizer--malachite.dragging { - &:before, +/* + Horizontal Handle + ------------------------------------------------------------------------------ +*/ +.resizer--handle.horizontal { + height: $resizer-click-area; + margin-top: -$resizer-click-area/2; + margin-bottom: -$resizer-click-area/2; + width: 100%; + + // Psuedo element for handle + &:before { + transform: translate(-50%,-50%); + } + // Psuedo element for line &:after { - background-color: $resizer-color-kapacitor; - box-shadow: 0 0 $resizer-glow $resizer-color-kapacitor; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 100%; + height: $resizer-line-width; + } +} + +/* + Vertical Handle + ------------------------------------------------------------------------------ +*/ +.resizer--handle.vertical { + width: $resizer-click-area; + margin-left: -$resizer-click-area/2; + margin-right: -$resizer-click-area/2; + height: 100%; + + // Psuedo element for handle + &:before { + transform: translate(-50%,-50%) rotate(90deg); + } + // Psuedo element for line + &:after { + left: 50%; + top: 0; + transform: translateX(-50%); + height: 100%; + width: $resizer-line-width; } } From 8902be156905516ce23eb202b14f7a17021f9c3a Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 09:36:24 -0700 Subject: [PATCH 007/104] Add some styles and logic for rendering division "names" --- ui/src/ifql/components/TimeMachine.tsx | 2 +- ui/src/shared/components/ResizeContainer.tsx | 20 +++-------- ui/src/shared/components/ResizeDivision.tsx | 32 +++++++++++++++-- ui/src/shared/components/ResizeHandle.tsx | 13 ------- ui/src/style/components/resizer.scss | 36 ++++++++++++++++++-- 5 files changed, 68 insertions(+), 35 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 608af50fef..268436c1fe 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -64,7 +64,7 @@ class TimeMachine extends PureComponent { }, { name: 'Schema Explorer', - render: () =>
Weeeeee
, + render: () =>
Explorin all yer schemas
, }, ] } diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 3e9dd3798c..e638771df5 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -2,7 +2,6 @@ import React, {Component, ReactElement} from 'react' import classnames from 'classnames' import uuid from 'uuid' -import ResizeHandle from 'src/shared/components/ResizeHandle' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' import {MIN_DIVISIONS, ORIENTATION_HORIZONTAL} from 'src/shared/constants/' @@ -67,7 +66,7 @@ class Resizer extends Component { onMouseMove={this.handleDrag} ref={r => (this.containerRef = r)} > - {divisions.map(d => ( + {divisions.map((d, i) => ( { offset={d.offset} render={d.render} orientation={orientation} + draggable={i > 0} + isDragging={isDragging} + onHandleStartDrag={this.handleStartDrag} /> ))} - {divisions.map((d, i) => { - if (i === 0) { - return null - } - return ( - - ) - })}
) } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 81dd36600f..e28e10a920 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,5 +1,6 @@ import React, {PureComponent, ReactElement} from 'react' import classnames from 'classnames' +import ResizeHandle from 'src/shared/components/ResizeHandle' import { ORIENTATION_VERTICAL, @@ -12,22 +13,49 @@ interface Props { name?: string size: number offset: number + isDragging: boolean + draggable: boolean orientation: string render: () => ReactElement + onHandleStartDrag: () => void } class Division extends PureComponent { public render() { - const {name, render} = this.props + const {render} = this.props return (
- {name &&
{name}
} + {this.dragHandle} {render()}
) } + private get dragHandle() { + const { + name, + isDragging, + orientation, + onHandleStartDrag, + draggable, + } = this.props + + if (name) { + return
{name}
+ } + + if (draggable) { + return ( + + ) + } + } + private get style() { const {orientation, size, offset} = this.props diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index ebe7a638aa..9ba7f4bae0 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -4,13 +4,11 @@ import classnames from 'classnames' import { ORIENTATION_VERTICAL, ORIENTATION_HORIZONTAL, - HUNDRED, } from 'src/shared/constants/' interface Props { onHandleStartDrag: () => void isDragging: boolean - offset: number orientation: string } @@ -26,20 +24,9 @@ class ResizeHandle extends PureComponent { horizontal: orientation === ORIENTATION_HORIZONTAL, })} onMouseDown={onHandleStartDrag} - style={this.style} /> ) } - - private get style() { - const {orientation, offset} = this.props - - if (orientation === ORIENTATION_VERTICAL) { - return {left: `${offset * HUNDRED}%`} - } - - return {top: `${offset * HUNDRED}%`} - } } export default ResizeHandle diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 75b0ef029f..884ab335d4 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -3,10 +3,13 @@ ------------------------------------------------------------------------------ */ +$resizer-division-z: 1; +$resizer-clickable-z: 2; +$resizer-line-z: 3; +$resizer-handle-z: 4; + $resizer-line-width: 2px; -$resizer-line-z: 2; $resizer-handle-width: 10px; -$resizer-handle-z: 3; $resizer-click-area: 28px; $resizer-glow: 14px; $resizer-dots: $g3-castle; @@ -24,6 +27,7 @@ $resizer-color-kapacitor: $c-rainforest; } .resizer--division { + z-index: $resizer-division-z; border: 1px solid #f00; position: absolute; @@ -49,7 +53,7 @@ $resizer-color-kapacitor: $c-rainforest; ------------------------------------------------------------------------------ */ .resizer--handle { - z-index: 1; + z-index: $resizer-clickable-z; user-select: none; -webkit-user-select: none; position: absolute; @@ -113,6 +117,7 @@ $resizer-color-kapacitor: $c-rainforest; ------------------------------------------------------------------------------ */ .resizer--handle.horizontal { + top: 0; height: $resizer-click-area; margin-top: -$resizer-click-area/2; margin-bottom: -$resizer-click-area/2; @@ -137,6 +142,7 @@ $resizer-color-kapacitor: $c-rainforest; ------------------------------------------------------------------------------ */ .resizer--handle.vertical { + left: 0; width: $resizer-click-area; margin-left: -$resizer-click-area/2; margin-right: -$resizer-click-area/2; @@ -155,3 +161,27 @@ $resizer-color-kapacitor: $c-rainforest; width: $resizer-line-width; } } + +/* + Resizable Container Draggable Title + ------------------------------------------------------------------------------ +*/ + +.resizer--title { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 30px; + line-height: 30px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + background-color: $g3-castle; + padding: 0 11px; + z-index: $resizer-clickable-z; + font-size: 13px; + font-weight: 600; + color: $g13-mist; + @include no-user-select(); +} From 8d89acd955b6c862d52ef473ebefc14559235de9 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 10:06:20 -0700 Subject: [PATCH 008/104] Handle specific drag --- ui/src/shared/components/ResizeContainer.tsx | 20 ++++----- ui/src/shared/components/ResizeDivision.tsx | 47 ++++++++++++-------- ui/src/shared/components/ResizeHandle.tsx | 41 ++++++++++++----- ui/src/style/components/resizer.scss | 11 +++++ 4 files changed, 78 insertions(+), 41 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index e638771df5..e9a97830cd 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -7,7 +7,7 @@ import {ErrorHandling} from 'src/shared/decorators/errors' import {MIN_DIVISIONS, ORIENTATION_HORIZONTAL} from 'src/shared/constants/' interface State { - isDragging: boolean + activeHandleID: string divisions: DivisionState[] } @@ -40,13 +40,13 @@ class Resizer extends Component { constructor(props) { super(props) this.state = { - isDragging: false, + activeHandleID: null, divisions: this.initialDivisions, } } public render() { - const {isDragging, divisions} = this.state + const {activeHandleID, divisions} = this.state const {containerClass, orientation} = this.props if (divisions.length < MIN_DIVISIONS) { @@ -59,7 +59,7 @@ class Resizer extends Component { return (
{ render={d.render} orientation={orientation} draggable={i > 0} - isDragging={isDragging} + activeHandleID={activeHandleID} onHandleStartDrag={this.handleStartDrag} /> ))} @@ -97,20 +97,20 @@ class Resizer extends Component { })) } - private handleStartDrag = () => { - this.setState({isDragging: true}) + private handleStartDrag = activeHandleID => { + this.setState({activeHandleID}) } private handleStopDrag = () => { - this.setState({isDragging: false}) + this.setState({activeHandleID: ''}) } private handleMouseLeave = () => { - this.setState({isDragging: false}) + this.setState({activeHandleID: ''}) } private handleDrag = () => { - if (!this.state.isDragging) { + if (!this.state.activeHandleID) { return } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index e28e10a920..27c232b984 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -8,19 +8,25 @@ import { HUNDRED, } from 'src/shared/constants/' +const NOOP = () => {} + interface Props { id: string name?: string size: number offset: number - isDragging: boolean + activeHandleID: string draggable: boolean orientation: string render: () => ReactElement - onHandleStartDrag: () => void + onHandleStartDrag: (activeHandleID: string) => void } class Division extends PureComponent { + public static defaultProps: Partial = { + name: '', + } + public render() { const {render} = this.props @@ -33,27 +39,30 @@ class Division extends PureComponent { } private get dragHandle() { - const { - name, - isDragging, - orientation, - onHandleStartDrag, - draggable, - } = this.props + const {name, activeHandleID, orientation, id, draggable} = this.props - if (name) { - return
{name}
+ if (!name && !draggable) { + return null } - if (draggable) { - return ( - - ) + return ( + + ) + } + + private get dragCallback() { + const {draggable} = this.props + if (!draggable) { + return NOOP } + + return this.props.onHandleStartDrag } private get style() { diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index 9ba7f4bae0..190bf088a5 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -7,26 +7,43 @@ import { } from 'src/shared/constants/' interface Props { - onHandleStartDrag: () => void - isDragging: boolean + name: string + id: string + onHandleStartDrag: (activeHandleID: string) => void + activeHandleID: string orientation: string } class ResizeHandle extends PureComponent { public render() { - const {onHandleStartDrag, isDragging, orientation} = this.props - return ( -
+
+ {this.props.name} +
) } + + private get className(): string { + const {name, orientation} = this.props + + return classnames({ + 'resizer--handle': !name, + 'resizer--title': name, + dragging: this.isActive, + vertical: orientation === ORIENTATION_VERTICAL, + horizontal: orientation === ORIENTATION_HORIZONTAL, + }) + } + + private get isActive(): boolean { + const {id, activeHandleID} = this.props + + return id === activeHandleID + } + + private handleMouseDown = (): void => { + this.props.onHandleStartDrag(this.props.id) + } } export default ResizeHandle diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 884ab335d4..e713258504 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -184,4 +184,15 @@ $resizer-color-kapacitor: $c-rainforest; font-weight: 600; color: $g13-mist; @include no-user-select(); + + &:hover { + cursor: ns-resize; + background-color: $g4-onyx; + } + + &.dragging { + cursor: ns-resize; + color: $c-laser; + background-color: $g5-pepper; + } } From 594d0f4a91c92cd8c684269bc528ce03e5958660 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 10:59:58 -0700 Subject: [PATCH 009/104] Remove concept of offset in resizer --- ui/src/shared/components/ResizeContainer.tsx | 45 ++++++++++++-------- ui/src/shared/components/ResizeDivision.tsx | 6 +-- ui/src/style/components/resizer.scss | 37 ++++++++++++++-- 3 files changed, 62 insertions(+), 26 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index e9a97830cd..942e7b8493 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -4,7 +4,11 @@ import uuid from 'uuid' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' -import {MIN_DIVISIONS, ORIENTATION_HORIZONTAL} from 'src/shared/constants/' +import { + MIN_DIVISIONS, + ORIENTATION_HORIZONTAL, + ORIENTATION_VERTICAL, +} from 'src/shared/constants/' interface State { activeHandleID: string @@ -20,7 +24,6 @@ interface Division { interface DivisionState extends Division { id: string size: number - offset: number } interface Props { @@ -47,7 +50,7 @@ class Resizer extends Component { public render() { const {activeHandleID, divisions} = this.state - const {containerClass, orientation} = this.props + const {orientation} = this.props if (divisions.length < MIN_DIVISIONS) { console.error( @@ -58,9 +61,7 @@ class Resizer extends Component { return (
{ id={d.id} name={d.name} size={d.size} - offset={d.offset} render={d.render} orientation={orientation} draggable={i > 0} @@ -84,16 +84,26 @@ class Resizer extends Component { ) } + private get className(): string { + const {orientation, containerClass} = this.props + const {activeHandleID} = this.state + + return classnames(`resize--container ${containerClass}`, { + 'resize--dragging': activeHandleID, + horizontal: orientation === ORIENTATION_HORIZONTAL, + vertical: orientation === ORIENTATION_VERTICAL, + }) + } + private get initialDivisions() { const {divisions} = this.props const size = 1 / divisions.length - return divisions.map((d, i) => ({ + return divisions.map(d => ({ ...d, id: uuid.v4(), size, - offset: size * i, })) } @@ -110,10 +120,15 @@ class Resizer extends Component { } private handleDrag = () => { - if (!this.state.activeHandleID) { - return - } - + // const {divisions, activeHandleID} = this.state + // if (!this.state.activeHandleID) { + // return + // } + // const activeDivision = divisions.find(d => d.id === activeHandleID) + // if (!activeDivision) { + // return + // } + // const {size, offset} = activeDivision // const {height} = getComputedStyle(this.containerRef) // const containerHeight = parseInt(height, 10) // // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. @@ -122,7 +137,6 @@ class Resizer extends Component { // (e.pageY - verticalOffset) / containerHeight * HUNDRED // ) // const newBottomPanelPercent = HUNDRED - newTopPanelPercent - // // Don't trigger a resize unless the change in size is greater than minResizePercentage // const minResizePercentage = 0.5 // if ( @@ -130,10 +144,8 @@ class Resizer extends Component { // ) { // return // } - // const topHeightPixels = newTopPanelPercent / HUNDRED * containerHeight // const bottomHeightPixels = newBottomPanelPercent / HUNDRED * containerHeight - // // Don't trigger a resize if the new sizes are too small // if ( // topHeightPixels < minTopHeight || @@ -141,7 +153,6 @@ class Resizer extends Component { // ) { // return // } - // this.setState({ // topHeight: newTopPanelPercent, // topHeightPixels, diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 27c232b984..59ea9baddc 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -14,7 +14,6 @@ interface Props { id: string name?: string size: number - offset: number activeHandleID: string draggable: boolean orientation: string @@ -66,23 +65,20 @@ class Division extends PureComponent { } private get style() { - const {orientation, size, offset} = this.props + const {orientation, size} = this.props const sizePercent = `${size * HUNDRED}%` - const offsetPercent = `${offset * HUNDRED}%` if (orientation === ORIENTATION_VERTICAL) { return { top: '0', width: sizePercent, - left: offsetPercent, } } return { left: '0', height: sizePercent, - top: offsetPercent, } } diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index e713258504..d825f7a010 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -20,6 +20,18 @@ $resizer-color-kapacitor: $c-rainforest; .resize--container { overflow: hidden !important; + display: flex; + align-items: stretch; + + &.vertical { + width: 100%; + flex-direction: row; + } + + &.horizontal { + height: 100%; + flex-direction: column; + } &.resize--dragging * { @include no-user-select(); @@ -29,7 +41,7 @@ $resizer-color-kapacitor: $c-rainforest; .resizer--division { z-index: $resizer-division-z; border: 1px solid #f00; - position: absolute; + position: relative; &.resizer__horizontal { width: 100%; @@ -91,8 +103,6 @@ $resizer-color-kapacitor: $c-rainforest; background-color 0.19s ease; } &:hover { - cursor: ns-resize; - &:before { background-color: $resizer-color-hover; } @@ -123,6 +133,10 @@ $resizer-color-kapacitor: $c-rainforest; margin-bottom: -$resizer-click-area/2; width: 100%; + &:hover { + cursor: ns-resize; + } + // Psuedo element for handle &:before { transform: translate(-50%,-50%); @@ -148,6 +162,10 @@ $resizer-color-kapacitor: $c-rainforest; margin-right: -$resizer-click-area/2; height: 100%; + &:hover { + cursor: ew-resize; + } + // Psuedo element for handle &:before { transform: translate(-50%,-50%) rotate(90deg); @@ -186,7 +204,6 @@ $resizer-color-kapacitor: $c-rainforest; @include no-user-select(); &:hover { - cursor: ns-resize; background-color: $g4-onyx; } @@ -196,3 +213,15 @@ $resizer-color-kapacitor: $c-rainforest; background-color: $g5-pepper; } } + +.resizer--title.horizontal { + &:hover { + cursor: ns-resize; + } +} + +.resizer--title.vertical { + &:hover { + cursor: ew-resize; + } +} From 92203b6aec08317fbdadf8a91a85af1fd1fe01de Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 15:10:57 -0700 Subject: [PATCH 010/104] Implement basic dragging Minimum sizes are not enforced yet but it is a big step in that direction --- ui/src/shared/components/ResizeContainer.tsx | 247 +++++++++++++++---- ui/src/shared/components/ResizeDivision.tsx | 16 +- ui/src/shared/components/ResizeHandle.tsx | 22 +- ui/src/shared/constants/index.tsx | 4 +- 4 files changed, 219 insertions(+), 70 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 942e7b8493..b238993674 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -1,18 +1,28 @@ -import React, {Component, ReactElement} from 'react' +import React, {Component, ReactElement, MouseEvent} from 'react' import classnames from 'classnames' import uuid from 'uuid' +import _ from 'lodash' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' import { MIN_DIVISIONS, - ORIENTATION_HORIZONTAL, - ORIENTATION_VERTICAL, + HANDLE_HORIZONTAL, + HANDLE_VERTICAL, } from 'src/shared/constants/' +const initialDragEvent = { + percentX: 0, + percentY: 0, + mouseX: null, + mouseY: null, +} + interface State { activeHandleID: string divisions: DivisionState[] + dragDirection: string + dragEvent: any } interface Division { @@ -35,16 +45,58 @@ interface Props { @ErrorHandling class Resizer extends Component { public static defaultProps: Partial = { - orientation: ORIENTATION_HORIZONTAL, + orientation: HANDLE_HORIZONTAL, } - public containerRef: HTMLElement + private containerRef: HTMLElement + private percentChangeX: number = 0 + private percentChangeY: number = 0 constructor(props) { super(props) this.state = { activeHandleID: null, divisions: this.initialDivisions, + dragEvent: initialDragEvent, + dragDirection: '', + } + } + + public componentDidUpdate(__, prevState) { + const {dragEvent} = this.state + const {orientation} = this.props + + if (_.isEqual(dragEvent, prevState.dragEvent)) { + return + } + + this.percentChangeX = this.pixelsToPercentX( + prevState.dragEvent.mouseX, + dragEvent.mouseX + ) + + this.percentChangeY = this.pixelsToPercentY( + prevState.dragEvent.mouseY, + dragEvent.mouseY + ) + + if (orientation === HANDLE_VERTICAL) { + const left = dragEvent.percentX < prevState.dragEvent.percentX + + if (left) { + return this.move.left() + } + + return this.move.right() + } + + const up = dragEvent.percentY < prevState.dragEvent.percentY + const down = dragEvent.percentY > prevState.dragEvent.percentY + + if (up) { + return this.move.up() + } else if (down) { + return this.move.down() } } @@ -90,8 +142,8 @@ class Resizer extends Component { return classnames(`resize--container ${containerClass}`, { 'resize--dragging': activeHandleID, - horizontal: orientation === ORIENTATION_HORIZONTAL, - vertical: orientation === ORIENTATION_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, + vertical: orientation === HANDLE_VERTICAL, }) } @@ -107,58 +159,151 @@ class Resizer extends Component { })) } - private handleStartDrag = activeHandleID => { - this.setState({activeHandleID}) + private handleStartDrag = (activeHandleID, e: MouseEvent) => { + const dragEvent = this.mousePosWithinContainer(e) + this.setState({activeHandleID, dragEvent}) } private handleStopDrag = () => { - this.setState({activeHandleID: ''}) + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) } private handleMouseLeave = () => { - this.setState({activeHandleID: ''}) + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) } - private handleDrag = () => { - // const {divisions, activeHandleID} = this.state - // if (!this.state.activeHandleID) { - // return - // } - // const activeDivision = divisions.find(d => d.id === activeHandleID) - // if (!activeDivision) { - // return - // } - // const {size, offset} = activeDivision - // const {height} = getComputedStyle(this.containerRef) - // const containerHeight = parseInt(height, 10) - // // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. - // const verticalOffset = window.innerHeight - containerHeight - // const newTopPanelPercent = Math.ceil( - // (e.pageY - verticalOffset) / containerHeight * HUNDRED - // ) - // const newBottomPanelPercent = HUNDRED - newTopPanelPercent - // // Don't trigger a resize unless the change in size is greater than minResizePercentage - // const minResizePercentage = 0.5 - // if ( - // Math.abs(newTopPanelPercent - this.state.topHeight) < minResizePercentage - // ) { - // return - // } - // const topHeightPixels = newTopPanelPercent / HUNDRED * containerHeight - // const bottomHeightPixels = newBottomPanelPercent / HUNDRED * containerHeight - // // Don't trigger a resize if the new sizes are too small - // if ( - // topHeightPixels < minTopHeight || - // bottomHeightPixels < minBottomHeight - // ) { - // return - // } - // this.setState({ - // topHeight: newTopPanelPercent, - // topHeightPixels, - // bottomHeight: newBottomPanelPercent, - // bottomHeightPixels, - // }) + private mousePosWithinContainer = (e: MouseEvent) => { + const {pageY, pageX} = e + const {top, left, width, height} = this.containerRef.getBoundingClientRect() + + const mouseX = pageX - left + const mouseY = pageY - top + + const percentX = mouseX / width + const percentY = mouseY / height + + return { + mouseX, + mouseY, + percentX, + percentY, + } + } + + private pixelsToPercentX = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {width} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / width) + } + + private pixelsToPercentY = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {height} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / height) + } + + private handleDrag = (e: MouseEvent) => { + const {activeHandleID} = this.state + if (!activeHandleID) { + return + } + + const dragEvent = this.mousePosWithinContainer(e) + this.setState({dragEvent}) + } + + private get move() { + const {activeHandleID} = this.state + + const activePosition = _.findIndex( + this.state.divisions, + d => d.id === activeHandleID + ) + + return { + up: this.up(activePosition), + down: this.down(activePosition), + left: this.left(activePosition), + right: this.right(activePosition), + } + } + + private up = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size - this.percentChangeY} + } else if (active) { + return {...d, size: d.size + this.percentChangeY} + } + + return d + }) + + this.setState({divisions}) + } + + private down = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size + this.percentChangeY} + } else if (active) { + return {...d, size: d.size - this.percentChangeY} + } + + return d + }) + + this.setState({divisions}) + } + + private left = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size - this.percentChangeX} + } else if (active) { + return {...d, size: d.size + this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) + } + + private right = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size + this.percentChangeX} + } else if (active) { + return {...d, size: d.size - this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) } } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 59ea9baddc..cfed33c2df 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,10 +1,12 @@ import React, {PureComponent, ReactElement} from 'react' import classnames from 'classnames' -import ResizeHandle from 'src/shared/components/ResizeHandle' +import ResizeHandle, { + OnHandleStartDrag, +} from 'src/shared/components/ResizeHandle' import { - ORIENTATION_VERTICAL, - ORIENTATION_HORIZONTAL, + HANDLE_VERTICAL, + HANDLE_HORIZONTAL, HUNDRED, } from 'src/shared/constants/' @@ -18,7 +20,7 @@ interface Props { draggable: boolean orientation: string render: () => ReactElement - onHandleStartDrag: (activeHandleID: string) => void + onHandleStartDrag: OnHandleStartDrag } class Division extends PureComponent { @@ -69,7 +71,7 @@ class Division extends PureComponent { const sizePercent = `${size * HUNDRED}%` - if (orientation === ORIENTATION_VERTICAL) { + if (orientation === HANDLE_VERTICAL) { return { top: '0', width: sizePercent, @@ -86,8 +88,8 @@ class Division extends PureComponent { const {orientation} = this.props // todo use constants instead of "vertical" / "horizontal" return classnames('resizer--division', { - resizer__vertical: orientation === ORIENTATION_VERTICAL, - resizer__horizontal: orientation === ORIENTATION_HORIZONTAL, + resizer__vertical: orientation === HANDLE_VERTICAL, + resizer__horizontal: orientation === HANDLE_HORIZONTAL, }) } } diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index 190bf088a5..dd45063fc2 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -1,15 +1,17 @@ -import React, {PureComponent} from 'react' +import React, {PureComponent, MouseEvent} from 'react' import classnames from 'classnames' -import { - ORIENTATION_VERTICAL, - ORIENTATION_HORIZONTAL, -} from 'src/shared/constants/' +import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/' + +export type OnHandleStartDrag = ( + activeHandleID: string, + e: MouseEvent +) => void interface Props { name: string id: string - onHandleStartDrag: (activeHandleID: string) => void + onHandleStartDrag: OnHandleStartDrag activeHandleID: string orientation: string } @@ -30,8 +32,8 @@ class ResizeHandle extends PureComponent { 'resizer--handle': !name, 'resizer--title': name, dragging: this.isActive, - vertical: orientation === ORIENTATION_VERTICAL, - horizontal: orientation === ORIENTATION_HORIZONTAL, + vertical: orientation === HANDLE_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, }) } @@ -41,8 +43,8 @@ class ResizeHandle extends PureComponent { return id === activeHandleID } - private handleMouseDown = (): void => { - this.props.onHandleStartDrag(this.props.id) + private handleMouseDown = (e: MouseEvent): void => { + this.props.onHandleStartDrag(this.props.id, e) } } diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index ba575404fb..93e6c57339 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -480,5 +480,5 @@ export const INFINITE = -1 export const HUNDRED = 100 export const MIN_DIVISIONS = 2 -export const ORIENTATION_VERTICAL = 'vertical' -export const ORIENTATION_HORIZONTAL = 'horizontal' +export const HANDLE_VERTICAL = 'vertical' +export const HANDLE_HORIZONTAL = 'horizontal' From 2db00e6d5df0151ebe59422ab29f6d5f663ada06 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 30 Apr 2018 17:18:18 -0700 Subject: [PATCH 011/104] WIP enforce division minimum sizes Co-authored by: Andrew Watkins Co-authored by: Alex Paxton --- ui/src/ifql/components/TimeMachine.tsx | 12 +++- ui/src/shared/components/ResizeContainer.tsx | 62 ++++++++++++++++---- ui/src/shared/components/ResizeDivision.tsx | 1 + ui/src/style/components/resizer.scss | 13 ++-- 4 files changed, 71 insertions(+), 17 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 268436c1fe..6871dbb201 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -32,7 +32,7 @@ class TimeMachine extends PureComponent { private get divisions() { return [ { - minSize: 200, + minPixels: 200, render: () => ( { ), }, { - minSize: 200, + minPixels: 200, render: () => , }, ] @@ -54,18 +54,26 @@ class TimeMachine extends PureComponent { return [ { name: 'IFQL', + minPixels: 60, render: () => ( ), }, { name: 'Builder', + minPixels: 60, render: () => , }, { name: 'Schema Explorer', + minPixels: 60, render: () =>
Explorin all yer schemas
, }, + { + name: '4th Item', + minPixels: 60, + render: () =>
Oh boy!
, + }, ] } } diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index b238993674..d363e64ad5 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -27,13 +27,14 @@ interface State { interface Division { name?: string - minSize?: number render: () => ReactElement + minPixels?: number } interface DivisionState extends Division { id: string size: number + minPixels?: number } interface Props { @@ -125,6 +126,7 @@ class Resizer extends Component { id={d.id} name={d.name} size={d.size} + minPixels={d.minPixels} render={d.render} orientation={orientation} draggable={i > 0} @@ -156,6 +158,7 @@ class Resizer extends Component { ...d, id: uuid.v4(), size, + minPixels: d.minPixels || 0, })) } @@ -212,6 +215,18 @@ class Resizer extends Component { return Math.abs(delta / height) } + private minPercentX = (xMinPixels: number): number => { + const {height} = this.containerRef.getBoundingClientRect() + + return xMinPixels / height + } + + private minPercentY = (yMinPixels: number): number => { + const {height} = this.containerRef.getBoundingClientRect() + + return yMinPixels / height + } + private handleDrag = (e: MouseEvent) => { const {activeHandleID} = this.state if (!activeHandleID) { @@ -241,32 +256,57 @@ class Resizer extends Component { private up = activePosition => () => { const divisions = this.state.divisions.map((d, i) => { const before = i === activePosition - 1 - const active = i === activePosition + const current = i === activePosition - if (before) { - return {...d, size: d.size - this.percentChangeY} - } else if (active) { + if (current) { return {...d, size: d.size + this.percentChangeY} + } else if (before) { + let size = d.size - this.percentChangeY + const minSize = this.minPercentY(d.minPixels) + + if (size < minSize) { + size = minSize + } + + return {...d, size} } - return d + return {...d} }) this.setState({divisions}) } private down = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { + const divisions = this.state.divisions.map((d, i, divs) => { const before = i === activePosition - 1 - const active = i === activePosition + const current = i === activePosition + const after = i > activePosition if (before) { return {...d, size: d.size + this.percentChangeY} - } else if (active) { - return {...d, size: d.size - this.percentChangeY} + } else if (current) { + let size = d.size - this.percentChangeY + const minSize = this.minPercentY(d.minPixels) + + if (size < minSize) { + size = minSize + } + + return {...d, size} + } else if (after) { + const previous = divs[i - 1] + const prevAtMinimum = + previous.size <= this.minPercentY(previous.minPixels) + const canBeShrunk = d.size > this.minPercentY(d.minPixels) + + if (prevAtMinimum && canBeShrunk) { + const size = d.size - this.percentChangeY + return {...d, size} + } } - return d + return {...d} }) this.setState({divisions}) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index cfed33c2df..259cfa3f4a 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -15,6 +15,7 @@ const NOOP = () => {} interface Props { id: string name?: string + minPixels: number size: number activeHandleID: string draggable: boolean diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index d825f7a010..f1e92bf982 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -20,17 +20,17 @@ $resizer-color-kapacitor: $c-rainforest; .resize--container { overflow: hidden !important; - display: flex; - align-items: stretch; + // display: flex; + // align-items: stretch; &.vertical { width: 100%; - flex-direction: row; + // flex-direction: row; } &.horizontal { height: 100%; - flex-direction: column; + // flex-direction: column; } &.resize--dragging * { @@ -42,6 +42,7 @@ $resizer-color-kapacitor: $c-rainforest; z-index: $resizer-division-z; border: 1px solid #f00; position: relative; + float: left; &.resizer__horizontal { width: 100%; @@ -50,6 +51,10 @@ $resizer-color-kapacitor: $c-rainforest; &.resizer__vertical { height: 100%; } + + .resizer--division { + border-color: #0f0; + } } .resizer--full-size { From 3bd60f4fce4353d7c3a3ca3de347b4fcb4f01c03 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 1 May 2018 11:14:15 -0700 Subject: [PATCH 012/104] WIP enforce min sizes of divisions --- ui/src/ifql/components/TimeMachine.tsx | 8 ++-- ui/src/shared/components/ResizeContainer.tsx | 39 ++++++++++++++++++-- ui/src/style/components/resizer.scss | 6 +++ 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 6871dbb201..d69dc73194 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -54,24 +54,24 @@ class TimeMachine extends PureComponent { return [ { name: 'IFQL', - minPixels: 60, + minPixels: 32, render: () => ( ), }, { name: 'Builder', - minPixels: 60, + minPixels: 32, render: () => , }, { name: 'Schema Explorer', - minPixels: 60, + minPixels: 32, render: () =>
Explorin all yer schemas
, }, { name: '4th Item', - minPixels: 60, + minPixels: 32, render: () =>
Oh boy!
, }, ] diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index d363e64ad5..291e1d2151 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -282,10 +282,17 @@ class Resizer extends Component { const before = i === activePosition - 1 const current = i === activePosition const after = i > activePosition + const last = i === divs.length - 1 if (before) { - return {...d, size: d.size + this.percentChangeY} - } else if (current) { + const size = d.size + this.percentChangeY + return {...d, size} + + // if current and all after cannot be shrunk + // stop resizing this one + } + + if (current) { let size = d.size - this.percentChangeY const minSize = this.minPercentY(d.minPixels) @@ -294,7 +301,9 @@ class Resizer extends Component { } return {...d, size} - } else if (after) { + } + + if (after && !last) { const previous = divs[i - 1] const prevAtMinimum = previous.size <= this.minPercentY(previous.minPixels) @@ -306,10 +315,21 @@ class Resizer extends Component { } } + if (last) { + const canBeShrunk = d.size > this.minPercentY(d.minPixels) + + if (canBeShrunk) { + const size = d.size - this.percentChangeY + return {...d, size} + } + + return {...d} + } + return {...d} }) - this.setState({divisions}) + this.setState({divisions: this.enforceHundredTotal(divisions)}) } private left = activePosition => () => { @@ -345,6 +365,17 @@ class Resizer extends Component { this.setState({divisions}) } + + private enforceHundredTotal = divisions => { + const indexLast = divisions.length - 1 + const subTotal = divisions + .slice(0, indexLast) + .reduce((acc, div) => acc + div.size, 0) + + divisions[indexLast].size = 1 - subTotal + + return divisions + } } export default Resizer diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index f1e92bf982..b386a4f39d 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -230,3 +230,9 @@ $resizer-color-kapacitor: $c-rainforest; cursor: ew-resize; } } + +.resizer--title.horizontal + div { + height: calc(100% - 30px); + position: absolute; + top: 30px; +} From 7eebef230a392da74279a0a5a2403d450b5d0642 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 1 May 2018 13:05:56 -0700 Subject: [PATCH 013/104] Enforce min & max sizes using CSS --- ui/src/shared/components/ResizeContainer.tsx | 60 +++++++++----------- ui/src/shared/components/ResizeDivision.tsx | 16 +++++- 2 files changed, 43 insertions(+), 33 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 291e1d2151..3d5fbac135 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -132,6 +132,8 @@ class Resizer extends Component { draggable={i > 0} activeHandleID={activeHandleID} onHandleStartDrag={this.handleStartDrag} + minPercent={this.minPercentY} + maxPercent={this.maximumHeightPercent} /> ))}
@@ -222,9 +224,30 @@ class Resizer extends Component { } private minPercentY = (yMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + + const {height} = this.containerRef.getBoundingClientRect() + return yMinPixels / height + } + + private get maximumHeightPercent(): number { + if (!this.containerRef) { + return 1 + } + + const {divisions} = this.state const {height} = this.containerRef.getBoundingClientRect() - return yMinPixels / height + const totalMinPixels = divisions.reduce( + (acc, div) => acc + div.minPixels, + 0 + ) + + const maximumPixels = height - totalMinPixels + + return this.minPercentY(maximumPixels) } private handleDrag = (e: MouseEvent) => { @@ -261,13 +284,7 @@ class Resizer extends Component { if (current) { return {...d, size: d.size + this.percentChangeY} } else if (before) { - let size = d.size - this.percentChangeY - const minSize = this.minPercentY(d.minPixels) - - if (size < minSize) { - size = minSize - } - + const size = d.size - this.percentChangeY return {...d, size} } @@ -282,50 +299,29 @@ class Resizer extends Component { const before = i === activePosition - 1 const current = i === activePosition const after = i > activePosition - const last = i === divs.length - 1 if (before) { const size = d.size + this.percentChangeY return {...d, size} - - // if current and all after cannot be shrunk - // stop resizing this one } if (current) { - let size = d.size - this.percentChangeY - const minSize = this.minPercentY(d.minPixels) - - if (size < minSize) { - size = minSize - } + const size = d.size - this.percentChangeY return {...d, size} } - if (after && !last) { + if (after) { const previous = divs[i - 1] const prevAtMinimum = previous.size <= this.minPercentY(previous.minPixels) - const canBeShrunk = d.size > this.minPercentY(d.minPixels) - if (prevAtMinimum && canBeShrunk) { + if (prevAtMinimum) { const size = d.size - this.percentChangeY return {...d, size} } } - if (last) { - const canBeShrunk = d.size > this.minPercentY(d.minPixels) - - if (canBeShrunk) { - const size = d.size - this.percentChangeY - return {...d, size} - } - - return {...d} - } - return {...d} }) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 259cfa3f4a..4e2b39adb4 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -22,6 +22,8 @@ interface Props { orientation: string render: () => ReactElement onHandleStartDrag: OnHandleStartDrag + minPercent: (pixels: number) => number + maxPercent: number } class Division extends PureComponent { @@ -67,21 +69,33 @@ class Division extends PureComponent { return this.props.onHandleStartDrag } + private get minPercent(): number { + const {minPercent, minPixels} = this.props + + return minPercent(minPixels) + } + private get style() { - const {orientation, size} = this.props + const {orientation, size, maxPercent} = this.props const sizePercent = `${size * HUNDRED}%` + const min = `${this.minPercent * HUNDRED}%` + const max = `${maxPercent * HUNDRED}%` if (orientation === HANDLE_VERTICAL) { return { top: '0', width: sizePercent, + minWidth: min, + maxWidth: max, } } return { left: '0', height: sizePercent, + minHeight: min, + maxHeight: max, } } From 7fdc7c7612f116836e68c971c689226a744b37bc Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 1 May 2018 14:26:45 -0700 Subject: [PATCH 014/104] Fix component not rendering --- ui/src/shared/components/ResizeContainer.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 3d5fbac135..47ff58e299 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -126,14 +126,14 @@ class Resizer extends Component { id={d.id} name={d.name} size={d.size} - minPixels={d.minPixels} - render={d.render} - orientation={orientation} draggable={i > 0} + minPixels={d.minPixels} + orientation={orientation} activeHandleID={activeHandleID} - onHandleStartDrag={this.handleStartDrag} minPercent={this.minPercentY} + onHandleStartDrag={this.handleStartDrag} maxPercent={this.maximumHeightPercent} + render={this.props.divisions[i].render} /> ))}
@@ -217,11 +217,11 @@ class Resizer extends Component { return Math.abs(delta / height) } - private minPercentX = (xMinPixels: number): number => { - const {height} = this.containerRef.getBoundingClientRect() + // private minPercentX = (xMinPixels: number): number => { + // const {height} = this.containerRef.getBoundingClientRect() - return xMinPixels / height - } + // return xMinPixels / height + // } private minPercentY = (yMinPixels: number): number => { if (!this.containerRef) { From 8672228f94b1272b0d0352e9c8f4a952194f0063 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 1 May 2018 14:38:01 -0700 Subject: [PATCH 015/104] Contain contents of each division --- ui/src/shared/components/ResizeDivision.tsx | 2 +- ui/src/style/components/resizer.scss | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 4e2b39adb4..9c95401cd2 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -37,7 +37,7 @@ class Division extends PureComponent { return (
{this.dragHandle} - {render()} +
{render()}
) } diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index b386a4f39d..e66b100c83 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -231,8 +231,16 @@ $resizer-color-kapacitor: $c-rainforest; } } -.resizer--title.horizontal + div { +.resizer--contents { + width: 100%; + height: 100%; +} + +.resizer--title + .resizer--contents { + width: 100%; height: calc(100% - 30px); position: absolute; - top: 30px; + top: 30px; + overflow: hidden; } + From 7edb9897dcf9584ca6764378c5bd750e07c5fa01 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 1 May 2018 15:19:04 -0700 Subject: [PATCH 016/104] Make drag up work again --- ui/src/shared/components/ResizeContainer.tsx | 27 +++++++++++++++----- ui/src/shared/components/ResizeDivision.tsx | 14 +++------- 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 47ff58e299..40bfa40c77 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -130,7 +130,6 @@ class Resizer extends Component { minPixels={d.minPixels} orientation={orientation} activeHandleID={activeHandleID} - minPercent={this.minPercentY} onHandleStartDrag={this.handleStartDrag} maxPercent={this.maximumHeightPercent} render={this.props.divisions[i].render} @@ -277,15 +276,29 @@ class Resizer extends Component { } private up = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i < activePosition const current = i === activePosition + const after = i === activePosition + 1 + + if (before) { + const below = divs[i + 1] + const belowAtMinimum = below.size <= this.minPercentY(below.minPixels) + + const aboveCurrent = i === activePosition - 1 + + if (belowAtMinimum || aboveCurrent) { + const size = d.size - this.percentChangeY + return {...d, size} + } + } if (current) { return {...d, size: d.size + this.percentChangeY} - } else if (before) { - const size = d.size - this.percentChangeY - return {...d, size} + } + + if (after) { + return {...d} } return {...d} @@ -325,7 +338,7 @@ class Resizer extends Component { return {...d} }) - this.setState({divisions: this.enforceHundredTotal(divisions)}) + this.setState({divisions}) } private left = activePosition => () => { diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 9c95401cd2..038964ad6c 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -22,7 +22,6 @@ interface Props { orientation: string render: () => ReactElement onHandleStartDrag: OnHandleStartDrag - minPercent: (pixels: number) => number maxPercent: number } @@ -69,24 +68,17 @@ class Division extends PureComponent { return this.props.onHandleStartDrag } - private get minPercent(): number { - const {minPercent, minPixels} = this.props - - return minPercent(minPixels) - } - private get style() { - const {orientation, size, maxPercent} = this.props + const {orientation, size, maxPercent, minPixels} = this.props const sizePercent = `${size * HUNDRED}%` - const min = `${this.minPercent * HUNDRED}%` const max = `${maxPercent * HUNDRED}%` if (orientation === HANDLE_VERTICAL) { return { top: '0', width: sizePercent, - minWidth: min, + minWidth: minPixels, maxWidth: max, } } @@ -94,7 +86,7 @@ class Division extends PureComponent { return { left: '0', height: sizePercent, - minHeight: min, + minHeight: minPixels, maxHeight: max, } } From da6280d466966114f5db5df2d05ea169d7718487 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 1 May 2018 16:11:10 -0700 Subject: [PATCH 017/104] Reduce jitter and enforce size in state --- ui/src/shared/components/ResizeContainer.tsx | 95 +++++++++++++------- ui/src/shared/components/ResizeDivision.tsx | 4 +- 2 files changed, 67 insertions(+), 32 deletions(-) diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 40bfa40c77..e0e897055f 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -139,6 +139,14 @@ class Resizer extends Component { ) } + private minPercent = (minPixels: number): number => { + if (this.props.orientation === HANDLE_VERTICAL) { + return this.minPercentX(minPixels) + } + + return this.minPercentY(minPixels) + } + private get className(): string { const {orientation, containerClass} = this.props const {activeHandleID} = this.state @@ -216,11 +224,14 @@ class Resizer extends Component { return Math.abs(delta / height) } - // private minPercentX = (xMinPixels: number): number => { - // const {height} = this.containerRef.getBoundingClientRect() + private minPercentX = (xMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + const {width} = this.containerRef.getBoundingClientRect() - // return xMinPixels / height - // } + return xMinPixels / width + } private minPercentY = (yMinPixels: number): number => { if (!this.containerRef) { @@ -259,6 +270,20 @@ class Resizer extends Component { this.setState({dragEvent}) } + private taller = (size: number): number => { + const newSize = size + this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private shorter = (size: number): number => { + const newSize = size - this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private isAtMinHeight = (division: DivisionState): boolean => { + return division.size <= this.minPercentY(division.minPixels) + } + private get move() { const {activeHandleID} = this.state @@ -283,18 +308,27 @@ class Resizer extends Component { if (before) { const below = divs[i + 1] - const belowAtMinimum = below.size <= this.minPercentY(below.minPixels) - const aboveCurrent = i === activePosition - 1 - if (belowAtMinimum || aboveCurrent) { - const size = d.size - this.percentChangeY - return {...d, size} + if (this.isAtMinHeight(below) || aboveCurrent) { + return {...d, size: this.shorter(d.size)} } } if (current) { - return {...d, size: d.size + this.percentChangeY} + const stayStill = divs.every((div, idx) => { + if (idx >= i) { + return true + } + + return this.isAtMinHeight(div) + }) + + if (stayStill) { + return {...d} + } + + return {...d, size: this.taller(d.size)} } if (after) { @@ -304,7 +338,7 @@ class Resizer extends Component { return {...d} }) - this.setState({divisions}) + this.setState({divisions: this.cleanDivisions(divisions)}) } private down = activePosition => () => { @@ -314,31 +348,25 @@ class Resizer extends Component { const after = i > activePosition if (before) { - const size = d.size + this.percentChangeY - return {...d, size} + return {...d, size: this.taller(d.size)} } if (current) { - const size = d.size - this.percentChangeY - - return {...d, size} + return {...d, size: this.shorter(d.size)} } if (after) { - const previous = divs[i - 1] - const prevAtMinimum = - previous.size <= this.minPercentY(previous.minPixels) + const above = divs[i - 1] - if (prevAtMinimum) { - const size = d.size - this.percentChangeY - return {...d, size} + if (this.isAtMinHeight(above)) { + return {...d, size: this.shorter(d.size)} } } return {...d} }) - this.setState({divisions}) + this.setState({divisions: this.cleanDivisions(divisions)}) } private left = activePosition => () => { @@ -375,15 +403,22 @@ class Resizer extends Component { this.setState({divisions}) } - private enforceHundredTotal = divisions => { - const indexLast = divisions.length - 1 - const subTotal = divisions - .slice(0, indexLast) - .reduce((acc, div) => acc + div.size, 0) + private enforceSize = (size, minPixels): number => { + const minPercent = this.minPercent(minPixels) - divisions[indexLast].size = 1 - subTotal + let enforcedSize = size + if (size < minPercent) { + enforcedSize = minPercent + } - return divisions + return enforcedSize + } + + private cleanDivisions = divisions => { + return divisions.map(d => { + const size = this.enforceSize(d.size, d.minPixels) + return {...d, size} + }) } } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 038964ad6c..510d897d44 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -69,7 +69,7 @@ class Division extends PureComponent { } private get style() { - const {orientation, size, maxPercent, minPixels} = this.props + const {orientation, maxPercent, minPixels, size} = this.props const sizePercent = `${size * HUNDRED}%` const max = `${maxPercent * HUNDRED}%` @@ -93,7 +93,7 @@ class Division extends PureComponent { private get className(): string { const {orientation} = this.props - // todo use constants instead of "vertical" / "horizontal" + return classnames('resizer--division', { resizer__vertical: orientation === HANDLE_VERTICAL, resizer__horizontal: orientation === HANDLE_HORIZONTAL, From 245602c2bd30e9105617e8d42541b773a07c1f2f Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 09:29:34 -0700 Subject: [PATCH 018/104] WIP Split resizer into 2 part "Resizer" and the "Threesizer" --- ui/src/shared/components/ResizeContainer.tsx | 277 ++--------- ui/src/shared/components/ResizeDivision.tsx | 3 +- ui/src/shared/components/ResizeHalf.tsx | 58 +++ ui/src/shared/components/Threesizer.tsx | 456 +++++++++++++++++++ ui/src/style/components/resizer.scss | 11 +- 5 files changed, 569 insertions(+), 236 deletions(-) create mode 100644 ui/src/shared/components/ResizeHalf.tsx create mode 100644 ui/src/shared/components/Threesizer.tsx diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index e0e897055f..1d61bae36d 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -3,43 +3,23 @@ import classnames from 'classnames' import uuid from 'uuid' import _ from 'lodash' -import ResizeDivision from 'src/shared/components/ResizeDivision' +import ResizeHalf from 'src/shared/components/ResizeHalf' +import ResizeHandle from 'src/shared/components/ResizeHandle' import {ErrorHandling} from 'src/shared/decorators/errors' -import { - MIN_DIVISIONS, - HANDLE_HORIZONTAL, - HANDLE_VERTICAL, -} from 'src/shared/constants/' - -const initialDragEvent = { - percentX: 0, - percentY: 0, - mouseX: null, - mouseY: null, -} +import {HANDLE_HORIZONTAL, HANDLE_VERTICAL} from 'src/shared/constants/index' interface State { - activeHandleID: string - divisions: DivisionState[] - dragDirection: string - dragEvent: any -} - -interface Division { - name?: string - render: () => ReactElement - minPixels?: number -} - -interface DivisionState extends Division { - id: string - size: number - minPixels?: number + topPercent: number + bottomPercent: number + isDragging: boolean } interface Props { - divisions: Division[] - orientation: string + topHalf: () => ReactElement + topMinPixels: number + bottomHalf: () => ReactElement + bottomMinPixels: number + orientation?: string containerClass: string } @@ -56,10 +36,9 @@ class Resizer extends Component { constructor(props) { super(props) this.state = { - activeHandleID: null, - divisions: this.initialDivisions, - dragEvent: initialDragEvent, - dragDirection: '', + topPercent: 0.5, + bottomPercent: 0.5, + isDragging: false, } } @@ -102,15 +81,14 @@ class Resizer extends Component { } public render() { - const {activeHandleID, divisions} = this.state - const {orientation} = this.props - - if (divisions.length < MIN_DIVISIONS) { - console.error( - `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` - ) - return - } + const {isDragging, topPercent, bottomPercent} = this.state + const { + topHalf, + topMinPixels, + bottomHalf, + bottomMinPixels, + orientation, + } = this.props return (
{ onMouseMove={this.handleDrag} ref={r => (this.containerRef = r)} > - {divisions.map((d, i) => ( - 0} - minPixels={d.minPixels} - orientation={orientation} - activeHandleID={activeHandleID} - onHandleStartDrag={this.handleStartDrag} - maxPercent={this.maximumHeightPercent} - render={this.props.divisions[i].render} - /> - ))} + + +
) } @@ -149,39 +129,23 @@ class Resizer extends Component { private get className(): string { const {orientation, containerClass} = this.props - const {activeHandleID} = this.state return classnames(`resize--container ${containerClass}`, { - 'resize--dragging': activeHandleID, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, }) } - private get initialDivisions() { - const {divisions} = this.props - - const size = 1 / divisions.length - - return divisions.map(d => ({ - ...d, - id: uuid.v4(), - size, - minPixels: d.minPixels || 0, - })) - } - - private handleStartDrag = (activeHandleID, e: MouseEvent) => { - const dragEvent = this.mousePosWithinContainer(e) - this.setState({activeHandleID, dragEvent}) + private handleStartDrag = () => { + this.setState({isDragging: true}) } private handleStopDrag = () => { - this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + this.setState({isDragging: false}) } private handleMouseLeave = () => { - this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + this.setState({isDragging: false}) } private mousePosWithinContainer = (e: MouseEvent) => { @@ -242,24 +206,6 @@ class Resizer extends Component { return yMinPixels / height } - private get maximumHeightPercent(): number { - if (!this.containerRef) { - return 1 - } - - const {divisions} = this.state - const {height} = this.containerRef.getBoundingClientRect() - - const totalMinPixels = divisions.reduce( - (acc, div) => acc + div.minPixels, - 0 - ) - - const maximumPixels = height - totalMinPixels - - return this.minPercentY(maximumPixels) - } - private handleDrag = (e: MouseEvent) => { const {activeHandleID} = this.state if (!activeHandleID) { @@ -283,143 +229,6 @@ class Resizer extends Component { private isAtMinHeight = (division: DivisionState): boolean => { return division.size <= this.minPercentY(division.minPixels) } - - private get move() { - const {activeHandleID} = this.state - - const activePosition = _.findIndex( - this.state.divisions, - d => d.id === activeHandleID - ) - - return { - up: this.up(activePosition), - down: this.down(activePosition), - left: this.left(activePosition), - right: this.right(activePosition), - } - } - - private up = activePosition => () => { - const divisions = this.state.divisions.map((d, i, divs) => { - const before = i < activePosition - const current = i === activePosition - const after = i === activePosition + 1 - - if (before) { - const below = divs[i + 1] - const aboveCurrent = i === activePosition - 1 - - if (this.isAtMinHeight(below) || aboveCurrent) { - return {...d, size: this.shorter(d.size)} - } - } - - if (current) { - const stayStill = divs.every((div, idx) => { - if (idx >= i) { - return true - } - - return this.isAtMinHeight(div) - }) - - if (stayStill) { - return {...d} - } - - return {...d, size: this.taller(d.size)} - } - - if (after) { - return {...d} - } - - return {...d} - }) - - this.setState({divisions: this.cleanDivisions(divisions)}) - } - - private down = activePosition => () => { - const divisions = this.state.divisions.map((d, i, divs) => { - const before = i === activePosition - 1 - const current = i === activePosition - const after = i > activePosition - - if (before) { - return {...d, size: this.taller(d.size)} - } - - if (current) { - return {...d, size: this.shorter(d.size)} - } - - if (after) { - const above = divs[i - 1] - - if (this.isAtMinHeight(above)) { - return {...d, size: this.shorter(d.size)} - } - } - - return {...d} - }) - - this.setState({divisions: this.cleanDivisions(divisions)}) - } - - private left = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size - this.percentChangeX} - } else if (active) { - return {...d, size: d.size + this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - - private right = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size + this.percentChangeX} - } else if (active) { - return {...d, size: d.size - this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - - private enforceSize = (size, minPixels): number => { - const minPercent = this.minPercent(minPixels) - - let enforcedSize = size - if (size < minPercent) { - enforcedSize = minPercent - } - - return enforcedSize - } - - private cleanDivisions = divisions => { - return divisions.map(d => { - const size = this.enforceSize(d.size, d.minPixels) - return {...d, size} - }) - } } export default Resizer diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 510d897d44..a00c655e0b 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -72,7 +72,8 @@ class Division extends PureComponent { const {orientation, maxPercent, minPixels, size} = this.props const sizePercent = `${size * HUNDRED}%` - const max = `${maxPercent * HUNDRED}%` + // const max = `${maxPercent * HUNDRED}%` + const max = '100%' if (orientation === HANDLE_VERTICAL) { return { diff --git a/ui/src/shared/components/ResizeHalf.tsx b/ui/src/shared/components/ResizeHalf.tsx new file mode 100644 index 0000000000..e1bbc00f75 --- /dev/null +++ b/ui/src/shared/components/ResizeHalf.tsx @@ -0,0 +1,58 @@ +import React, {PureComponent, ReactElement} from 'react' +import classnames from 'classnames' + +import { + HANDLE_VERTICAL, + HANDLE_HORIZONTAL, + HUNDRED, +} from 'src/shared/constants/' + +interface Props { + percent: number + minPixels: number + render: () => ReactElement + orientation: string +} + +class ResizerHalf extends PureComponent { + public render() { + const {render} = this.props + + return ( +
+ {render()} +
+ ) + } + + private get style() { + const {orientation, minPixels, percent} = this.props + + const size = `${percent * HUNDRED}%` + + if (orientation === HANDLE_VERTICAL) { + return { + top: '0', + width: size, + minWidth: minPixels, + } + } + + return { + left: '0', + height: size, + minHeight: minPixels, + } + } + + private get className(): string { + const {orientation} = this.props + + return classnames('resizer--half', { + vertical: orientation === HANDLE_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, + }) + } +} + +export default ResizerHalf diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx new file mode 100644 index 0000000000..cc2b93f365 --- /dev/null +++ b/ui/src/shared/components/Threesizer.tsx @@ -0,0 +1,456 @@ +import React, {Component, ReactElement, MouseEvent} from 'react' +import classnames from 'classnames' +import uuid from 'uuid' +import _ from 'lodash' + +import ResizeDivision from 'src/shared/components/ResizeDivision' +import {ErrorHandling} from 'src/shared/decorators/errors' +import { + MIN_DIVISIONS, + HANDLE_HORIZONTAL, + HANDLE_VERTICAL, +} from 'src/shared/constants/' + +const initialDragEvent = { + percentX: 0, + percentY: 0, + mouseX: null, + mouseY: null, +} + +interface State { + activeHandleID: string + divisions: DivisionState[] + dragDirection: string + dragEvent: any +} + +interface Division { + name?: string + render: () => ReactElement + minPixels?: number +} + +interface DivisionState extends Division { + id: string + size: number + minPixels?: number +} + +interface Props { + divisions: Division[] + orientation: string + containerClass: string +} + +@ErrorHandling +class Resizer extends Component { + public static defaultProps: Partial = { + orientation: HANDLE_HORIZONTAL, + } + + private containerRef: HTMLElement + private percentChangeX: number = 0 + private percentChangeY: number = 0 + + constructor(props) { + super(props) + this.state = { + activeHandleID: null, + divisions: this.initialDivisions, + dragEvent: initialDragEvent, + dragDirection: '', + } + } + + public componentDidUpdate(__, prevState) { + const {dragEvent} = this.state + const {orientation} = this.props + + if (_.isEqual(dragEvent, prevState.dragEvent)) { + return + } + + this.percentChangeX = this.pixelsToPercentX( + prevState.dragEvent.mouseX, + dragEvent.mouseX + ) + + this.percentChangeY = this.pixelsToPercentY( + prevState.dragEvent.mouseY, + dragEvent.mouseY + ) + + if (orientation === HANDLE_VERTICAL) { + const left = dragEvent.percentX < prevState.dragEvent.percentX + + if (left) { + return this.move.left() + } + + return this.move.right() + } + + const up = dragEvent.percentY < prevState.dragEvent.percentY + const down = dragEvent.percentY > prevState.dragEvent.percentY + + if (up) { + return this.move.up() + } else if (down) { + return this.move.down() + } + } + + public render() { + const {activeHandleID, divisions} = this.state + const {orientation} = this.props + + if (divisions.length < MIN_DIVISIONS) { + console.error( + `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` + ) + return + } + + return ( +
(this.containerRef = r)} + > + {divisions.map((d, i) => ( + 0} + minPixels={d.minPixels} + orientation={orientation} + activeHandleID={activeHandleID} + onHandleStartDrag={this.handleStartDrag} + maxPercent={this.maximumHeightPercent} + render={this.props.divisions[i].render} + /> + ))} +
+ ) + } + + private minPercent = (minPixels: number): number => { + if (this.props.orientation === HANDLE_VERTICAL) { + return this.minPercentX(minPixels) + } + + return this.minPercentY(minPixels) + } + + private get className(): string { + const {orientation, containerClass} = this.props + const {activeHandleID} = this.state + + return classnames(`resize--container ${containerClass}`, { + 'resize--dragging': activeHandleID, + horizontal: orientation === HANDLE_HORIZONTAL, + vertical: orientation === HANDLE_VERTICAL, + }) + } + + private get initialDivisions() { + const {divisions} = this.props + + const size = 1 / divisions.length + + return divisions.map(d => ({ + ...d, + id: uuid.v4(), + size, + minPixels: d.minPixels || 0, + })) + } + + private handleStartDrag = (activeHandleID, e: MouseEvent) => { + const dragEvent = this.mousePosWithinContainer(e) + this.setState({activeHandleID, dragEvent}) + } + + private handleStopDrag = () => { + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + } + + private handleMouseLeave = () => { + this.setState({activeHandleID: '', dragEvent: initialDragEvent}) + } + + private mousePosWithinContainer = (e: MouseEvent) => { + const {pageY, pageX} = e + const {top, left, width, height} = this.containerRef.getBoundingClientRect() + + const mouseX = pageX - left + const mouseY = pageY - top + + const percentX = mouseX / width + const percentY = mouseY / height + + return { + mouseX, + mouseY, + percentX, + percentY, + } + } + + private pixelsToPercentX = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {width} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / width) + } + + private pixelsToPercentY = (startValue, endValue) => { + if (!startValue) { + return 0 + } + + const delta = startValue - endValue + const {height} = this.containerRef.getBoundingClientRect() + + return Math.abs(delta / height) + } + + private minPercentX = (xMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + const {width} = this.containerRef.getBoundingClientRect() + + return xMinPixels / width + } + + private minPercentY = (yMinPixels: number): number => { + if (!this.containerRef) { + return 0 + } + + const {height} = this.containerRef.getBoundingClientRect() + return yMinPixels / height + } + + private get maximumHeightPercent(): number { + if (!this.containerRef) { + return 1 + } + + const {divisions} = this.state + const {height} = this.containerRef.getBoundingClientRect() + + const totalMinPixels = divisions.reduce( + (acc, div) => acc + div.minPixels, + 0 + ) + + const maximumPixels = height - totalMinPixels + + return this.minPercentY(maximumPixels) + } + + private handleDrag = (e: MouseEvent) => { + const {activeHandleID} = this.state + if (!activeHandleID) { + return + } + + const dragEvent = this.mousePosWithinContainer(e) + this.setState({dragEvent}) + } + + private taller = (size: number): number => { + const newSize = size + this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private shorter = (size: number): number => { + const newSize = size - this.percentChangeY + return Number(newSize.toFixed(3)) + } + + private isAtMinHeight = (division: DivisionState): boolean => { + return division.size <= this.minPercentY(division.minPixels) + } + + private get move() { + const {activeHandleID} = this.state + + const activePosition = _.findIndex( + this.state.divisions, + d => d.id === activeHandleID + ) + + return { + up: this.up(activePosition), + down: this.down(activePosition), + left: this.left(activePosition), + right: this.right(activePosition), + } + } + + private up = activePosition => () => { + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i < activePosition + const current = i === activePosition + + if (before) { + const below = divs[i + 1] + const aboveCurrent = i === activePosition - 1 + + const belowIsCurrent = below.id === divs[activePosition].id + + if (belowIsCurrent) { + return {...d} + } + + if (this.isAtMinHeight(below) || aboveCurrent) { + const size = this.shorter(d.size) + if (size < 0) { + debugger + } + + return {...d, size} + } + } + + if (current) { + const stayStill = divs.every((div, idx) => { + if (idx >= i) { + return true + } + + return this.isAtMinHeight(div) + }) + + if (stayStill) { + return {...d} + } + + return {...d, size: this.taller(d.size)} + } + + return {...d} + }) + + this.setState({divisions}) + } + + private down = activePosition => () => { + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i === activePosition - 1 + const current = i === activePosition + const after = i > activePosition + + if (before) { + return {...d, size: this.taller(d.size)} + } + + if (current) { + return {...d, size: this.shorter(d.size)} + } + + if (after) { + const above = divs[i - 1] + + if (this.isAtMinHeight(d)) { + return {...d} + } + + if (this.isAtMinHeight(above)) { + return {...d, size: this.shorter(d.size)} + } + } + + return {...d} + }) + + this.setState({divisions}) + } + + private left = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size - this.percentChangeX} + } else if (active) { + return {...d, size: d.size + this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) + } + + private right = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + const before = i === activePosition - 1 + const active = i === activePosition + + if (before) { + return {...d, size: d.size + this.percentChangeX} + } else if (active) { + return {...d, size: d.size - this.percentChangeX} + } + + return d + }) + + this.setState({divisions}) + } + + private enforceSize = (size, minPixels): number => { + const minPercent = this.minPercent(minPixels) + + let enforcedSize = size + if (size < minPercent) { + enforcedSize = minPercent + } + + return enforcedSize + } + + private cleanDivisions = divisions => { + const minSizes = divisions.map(d => { + const size = this.enforceSize(d.size, d.minPixels) + return {...d, size} + }) + + const sumSizes = minSizes.reduce((acc, div, i) => { + if (i <= divisions.length - 1) { + return acc + div.size + } + + return acc + }, 0) + + const under100percent = 1 - sumSizes > 0 + const over100percent = 1 - sumSizes < 0 + + if (under100percent) { + minSizes[divisions.length - 1].size += Math.abs(1 - sumSizes) + } + + if (over100percent) { + minSizes[divisions.length - 1].size -= Math.abs(1 - sumSizes) + } + + return minSizes + } +} + +export default Resizer diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index e66b100c83..8ae32df584 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -52,9 +52,18 @@ $resizer-color-kapacitor: $c-rainforest; height: 100%; } - .resizer--division { + .resizer--division:nth-child(1) { border-color: #0f0; } + .resizer--division:nth-child(2) { + border-color: #0ff; + } + .resizer--division:nth-child(3) { + border-color: #00f; + } + .resizer--division:nth-child(4) { + border-color: #f0f; + } } .resizer--full-size { From 9c1b9698840cfd7e441688f3feec7e89f0e235ac Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 11:55:13 -0700 Subject: [PATCH 019/104] Rebuild resizer component --- .../components/CellEditorOverlay.tsx | 135 +++++++------ ui/src/data_explorer/constants/index.js | 4 - .../data_explorer/containers/DataExplorer.tsx | 19 +- ui/src/ifql/components/TimeMachine.tsx | 63 ++---- ui/src/shared/components/ResizeContainer.tsx | 189 ++++++------------ ui/src/shared/components/ResizeHalf.tsx | 18 +- ui/src/shared/components/ResizeHandle.tsx | 57 +++--- ui/src/shared/constants/index.tsx | 2 +- ui/src/style/components/resizer.scss | 132 +++--------- ui/src/style/pages/dashboards.scss | 11 + 10 files changed, 240 insertions(+), 390 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index c22680c912..723a7f834a 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -166,13 +166,46 @@ class CellEditorOverlay extends Component { } public render() { - const { - onCancel, - templates, - timeRange, - autoRefresh, - editQueryStatus, - } = this.props + return ( +
+ + {this.renderVisualization()} + {this.renderControls()} + +
+ ) + } + + private renderVisualization = () => { + const {templates, timeRange, autoRefresh, editQueryStatus} = this.props + + const {queriesWorkingDraft, isStaticLegend} = this.state + + return ( + + ) + } + + private renderControls = () => { + const {onCancel, templates, timeRange} = this.props const { activeQueryIndex, @@ -182,65 +215,41 @@ class CellEditorOverlay extends Component { } = this.state return ( -
- - + + {isDisplayOptionsTabActive ? ( + - - - {isDisplayOptionsTabActive ? ( - - ) : ( - - )} - - -
+ ) : ( + + )} + ) } diff --git a/ui/src/data_explorer/constants/index.js b/ui/src/data_explorer/constants/index.js index c6e9bf0767..82d37421ce 100644 --- a/ui/src/data_explorer/constants/index.js +++ b/ui/src/data_explorer/constants/index.js @@ -15,10 +15,6 @@ export const MINIMUM_HEIGHTS = { queryMaker: 350, visualization: 200, } -export const INITIAL_HEIGHTS = { - queryMaker: '66.666%', - visualization: '33.334%', -} const SEPARATOR = 'SEPARATOR' diff --git a/ui/src/data_explorer/containers/DataExplorer.tsx b/ui/src/data_explorer/containers/DataExplorer.tsx index 2d7696036e..adb372dd31 100644 --- a/ui/src/data_explorer/containers/DataExplorer.tsx +++ b/ui/src/data_explorer/containers/DataExplorer.tsx @@ -13,12 +13,12 @@ import QueryMaker from 'src/data_explorer/components/QueryMaker' import Visualization from 'src/data_explorer/components/Visualization' import WriteDataForm from 'src/data_explorer/components/WriteDataForm' import Header from 'src/data_explorer/containers/Header' -import ResizeContainer from 'src/shared/components/ResizeContainer' +import Resizer from 'src/shared/components/ResizeContainer' import OverlayTechnologies from 'src/shared/components/OverlayTechnologies' import ManualRefresh from 'src/shared/components/ManualRefresh' import {VIS_VIEWS, AUTO_GROUP_BY, TEMPLATES} from 'src/shared/constants' -import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' +import {MINIMUM_HEIGHTS} from 'src/data_explorer/constants' import {errorThrown} from 'src/shared/actions/errors' import {setAutoRefresh} from 'src/shared/actions/app' import * as dataExplorerActionCreators from 'src/data_explorer/actions/view' @@ -120,15 +120,14 @@ export class DataExplorer extends PureComponent { onChooseAutoRefresh={handleChooseAutoRefresh} onManualRefresh={onManualRefresh} /> - + > + {this.renderTop()} + {this.renderBottom()} +
) } diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index d69dc73194..28d2932d7a 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -5,6 +5,7 @@ import TimeMachineVis from 'src/ifql/components/TimeMachineVis' import Resizer from 'src/shared/components/ResizeContainer' import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' +import {HANDLE_VERTICAL} from 'src/shared/constants/index' interface Props { script: string @@ -22,59 +23,31 @@ class TimeMachine extends PureComponent { public render() { return ( + > + {this.renderVisualization()} + {this.renderEditor()} + ) } - private get divisions() { - return [ - { - minPixels: 200, - render: () => ( - - ), - }, - { - minPixels: 200, - render: () => , - }, - ] + private renderVisualization = () => { + return } - private get renderEditorDivisions() { + private renderEditor = () => { const {script, body, suggestions, onChangeScript} = this.props - return [ - { - name: 'IFQL', - minPixels: 32, - render: () => ( - - ), - }, - { - name: 'Builder', - minPixels: 32, - render: () => , - }, - { - name: 'Schema Explorer', - minPixels: 32, - render: () =>
Explorin all yer schemas
, - }, - { - name: '4th Item', - minPixels: 32, - render: () =>
Oh boy!
, - }, - ] + return ( +
+ + +
Explorin all yer schemas
+
+ ) } } diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx index 1d61bae36d..2b5e098997 100644 --- a/ui/src/shared/components/ResizeContainer.tsx +++ b/ui/src/shared/components/ResizeContainer.tsx @@ -1,12 +1,14 @@ -import React, {Component, ReactElement, MouseEvent} from 'react' +import React, {Component, ReactNode, MouseEvent} from 'react' import classnames from 'classnames' -import uuid from 'uuid' -import _ from 'lodash' import ResizeHalf from 'src/shared/components/ResizeHalf' import ResizeHandle from 'src/shared/components/ResizeHandle' import {ErrorHandling} from 'src/shared/decorators/errors' -import {HANDLE_HORIZONTAL, HANDLE_VERTICAL} from 'src/shared/constants/index' +import { + HANDLE_HORIZONTAL, + HANDLE_VERTICAL, + REQUIRED_HALVES, +} from 'src/shared/constants/index' interface State { topPercent: number @@ -15,9 +17,8 @@ interface State { } interface Props { - topHalf: () => ReactElement + children: ReactNode topMinPixels: number - bottomHalf: () => ReactElement bottomMinPixels: number orientation?: string containerClass: string @@ -30,8 +31,6 @@ class Resizer extends Component { } private containerRef: HTMLElement - private percentChangeX: number = 0 - private percentChangeY: number = 0 constructor(props) { super(props) @@ -42,53 +41,14 @@ class Resizer extends Component { } } - public componentDidUpdate(__, prevState) { - const {dragEvent} = this.state - const {orientation} = this.props - - if (_.isEqual(dragEvent, prevState.dragEvent)) { - return - } - - this.percentChangeX = this.pixelsToPercentX( - prevState.dragEvent.mouseX, - dragEvent.mouseX - ) - - this.percentChangeY = this.pixelsToPercentY( - prevState.dragEvent.mouseY, - dragEvent.mouseY - ) - - if (orientation === HANDLE_VERTICAL) { - const left = dragEvent.percentX < prevState.dragEvent.percentX - - if (left) { - return this.move.left() - } - - return this.move.right() - } - - const up = dragEvent.percentY < prevState.dragEvent.percentY - const down = dragEvent.percentY > prevState.dragEvent.percentY - - if (up) { - return this.move.up() - } else if (down) { - return this.move.down() - } - } - public render() { const {isDragging, topPercent, bottomPercent} = this.state - const { - topHalf, - topMinPixels, - bottomHalf, - bottomMinPixels, - orientation, - } = this.props + const {children, topMinPixels, bottomMinPixels, orientation} = this.props + + if (React.Children.count(children) !== REQUIRED_HALVES) { + console.error('ResizeContainer requires exactly 2 children') + return null + } return (
{ ref={r => (this.containerRef = r)} >
) } - private minPercent = (minPixels: number): number => { - if (this.props.orientation === HANDLE_VERTICAL) { - return this.minPercentX(minPixels) - } - - return this.minPercentY(minPixels) - } - private get className(): string { const {orientation, containerClass} = this.props + const {isDragging} = this.state return classnames(`resize--container ${containerClass}`, { + dragging: isDragging, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, }) @@ -148,6 +105,48 @@ class Resizer extends Component { this.setState({isDragging: false}) } + private handleDrag = (e: MouseEvent) => { + const {isDragging} = this.state + const {orientation} = this.props + if (!isDragging) { + return + } + + const {percentX, percentY} = this.mousePosWithinContainer(e) + + if (orientation === HANDLE_HORIZONTAL && this.dragIsWithinBounds(e)) { + this.setState({ + topPercent: percentY, + bottomPercent: this.invertPercent(percentY), + }) + } + + if (orientation === HANDLE_VERTICAL && this.dragIsWithinBounds(e)) { + this.setState({ + topPercent: percentX, + bottomPercent: this.invertPercent(percentX), + }) + } + } + + private dragIsWithinBounds = (e: MouseEvent): boolean => { + const {orientation, topMinPixels, bottomMinPixels} = this.props + const {mouseX, mouseY} = this.mousePosWithinContainer(e) + const {width, height} = this.containerRef.getBoundingClientRect() + + if (orientation === HANDLE_HORIZONTAL) { + const doesNotExceedTop = mouseY > topMinPixels + const doesNotExceedBottom = Math.abs(mouseY - height) > bottomMinPixels + + return doesNotExceedTop && doesNotExceedBottom + } + + const doesNotExceedLeft = mouseX > topMinPixels + const doesNotExceedRight = Math.abs(mouseX - width) > bottomMinPixels + + return doesNotExceedLeft && doesNotExceedRight + } + private mousePosWithinContainer = (e: MouseEvent) => { const {pageY, pageX} = e const {top, left, width, height} = this.containerRef.getBoundingClientRect() @@ -166,68 +165,8 @@ class Resizer extends Component { } } - private pixelsToPercentX = (startValue, endValue) => { - if (!startValue) { - return 0 - } - - const delta = startValue - endValue - const {width} = this.containerRef.getBoundingClientRect() - - return Math.abs(delta / width) - } - - private pixelsToPercentY = (startValue, endValue) => { - if (!startValue) { - return 0 - } - - const delta = startValue - endValue - const {height} = this.containerRef.getBoundingClientRect() - - return Math.abs(delta / height) - } - - private minPercentX = (xMinPixels: number): number => { - if (!this.containerRef) { - return 0 - } - const {width} = this.containerRef.getBoundingClientRect() - - return xMinPixels / width - } - - private minPercentY = (yMinPixels: number): number => { - if (!this.containerRef) { - return 0 - } - - const {height} = this.containerRef.getBoundingClientRect() - return yMinPixels / height - } - - private handleDrag = (e: MouseEvent) => { - const {activeHandleID} = this.state - if (!activeHandleID) { - return - } - - const dragEvent = this.mousePosWithinContainer(e) - this.setState({dragEvent}) - } - - private taller = (size: number): number => { - const newSize = size + this.percentChangeY - return Number(newSize.toFixed(3)) - } - - private shorter = (size: number): number => { - const newSize = size - this.percentChangeY - return Number(newSize.toFixed(3)) - } - - private isAtMinHeight = (division: DivisionState): boolean => { - return division.size <= this.minPercentY(division.minPixels) + private invertPercent = percent => { + return 1 - percent } } diff --git a/ui/src/shared/components/ResizeHalf.tsx b/ui/src/shared/components/ResizeHalf.tsx index e1bbc00f75..4f7920b199 100644 --- a/ui/src/shared/components/ResizeHalf.tsx +++ b/ui/src/shared/components/ResizeHalf.tsx @@ -10,30 +10,33 @@ import { interface Props { percent: number minPixels: number - render: () => ReactElement + component: ReactElement orientation: string + offset: number } -class ResizerHalf extends PureComponent { +class ResizeHalf extends PureComponent { public render() { - const {render} = this.props + const {component} = this.props return (
- {render()} + {component}
) } private get style() { - const {orientation, minPixels, percent} = this.props + const {orientation, minPixels, percent, offset} = this.props const size = `${percent * HUNDRED}%` + const gap = `${offset * HUNDRED}%` if (orientation === HANDLE_VERTICAL) { return { top: '0', width: size, + left: gap, minWidth: minPixels, } } @@ -41,6 +44,7 @@ class ResizerHalf extends PureComponent { return { left: '0', height: size, + top: gap, minHeight: minPixels, } } @@ -48,11 +52,11 @@ class ResizerHalf extends PureComponent { private get className(): string { const {orientation} = this.props - return classnames('resizer--half', { + return classnames('resize--half', { vertical: orientation === HANDLE_VERTICAL, horizontal: orientation === HANDLE_HORIZONTAL, }) } } -export default ResizerHalf +export default ResizeHalf diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index dd45063fc2..f05528c8ca 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -1,50 +1,53 @@ import React, {PureComponent, MouseEvent} from 'react' import classnames from 'classnames' -import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/' - -export type OnHandleStartDrag = ( - activeHandleID: string, - e: MouseEvent -) => void +import { + HANDLE_VERTICAL, + HANDLE_HORIZONTAL, + HUNDRED, +} from 'src/shared/constants/' interface Props { - name: string - id: string - onHandleStartDrag: OnHandleStartDrag - activeHandleID: string + onStartDrag: (e: MouseEvent) => void + isDragging: boolean orientation: string + percent: number } class ResizeHandle extends PureComponent { public render() { return ( -
- {this.props.name} -
+
) } - private get className(): string { - const {name, orientation} = this.props + private get style() { + const {percent, orientation} = this.props + const size = `${percent * HUNDRED}%` - return classnames({ - 'resizer--handle': !name, - 'resizer--title': name, - dragging: this.isActive, + if (orientation === HANDLE_VERTICAL) { + return {left: size} + } + + return {top: size} + } + + private get className(): string { + const {isDragging, orientation} = this.props + + return classnames('resizer--handle', { + dragging: isDragging, vertical: orientation === HANDLE_VERTICAL, horizontal: orientation === HANDLE_HORIZONTAL, }) } - private get isActive(): boolean { - const {id, activeHandleID} = this.props - - return id === activeHandleID - } - - private handleMouseDown = (e: MouseEvent): void => { - this.props.onHandleStartDrag(this.props.id, e) + public handleMouseDown = (e: MouseEvent): void => { + this.props.onStartDrag(e) } } diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index 93e6c57339..46fc9322b9 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -479,6 +479,6 @@ export const TEN_SECONDS = 10000 export const INFINITE = -1 export const HUNDRED = 100 -export const MIN_DIVISIONS = 2 +export const REQUIRED_HALVES = 2 export const HANDLE_VERTICAL = 'vertical' export const HANDLE_HORIZONTAL = 'horizontal' diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 8ae32df584..18724ba884 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -4,6 +4,7 @@ */ $resizer-division-z: 1; +$resizer-division-hover-z: 5; $resizer-clickable-z: 2; $resizer-line-z: 3; $resizer-handle-z: 4; @@ -19,59 +20,34 @@ $resizer-color-active: $c-pool; $resizer-color-kapacitor: $c-rainforest; .resize--container { - overflow: hidden !important; - // display: flex; - // align-items: stretch; + position: relative; - &.vertical { - width: 100%; - // flex-direction: row; - } - - &.horizontal { - height: 100%; - // flex-direction: column; - } - - &.resize--dragging * { + &.dragging * { @include no-user-select(); } } -.resizer--division { +.resize--half { z-index: $resizer-division-z; - border: 1px solid #f00; - position: relative; - float: left; - - &.resizer__horizontal { - width: 100%; - } - - &.resizer__vertical { - height: 100%; - } - - .resizer--division:nth-child(1) { - border-color: #0f0; - } - .resizer--division:nth-child(2) { - border-color: #0ff; - } - .resizer--division:nth-child(3) { - border-color: #00f; - } - .resizer--division:nth-child(4) { - border-color: #f0f; - } -} - -.resizer--full-size { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; + + &.horizontal { + width: 100%; + left: 0; + } + + &.vertical { + height: 100%; + top: 0; + } + + &:hover { + z-index: $resizer-division-hover-z; + } + + .dragging & { + pointer-events: none; + } } /* @@ -148,7 +124,7 @@ $resizer-color-kapacitor: $c-rainforest; width: 100%; &:hover { - cursor: ns-resize; + cursor: row-resize; } // Psuedo element for handle @@ -177,7 +153,7 @@ $resizer-color-kapacitor: $c-rainforest; height: 100%; &:hover { - cursor: ew-resize; + cursor: col-resize; } // Psuedo element for handle @@ -193,63 +169,3 @@ $resizer-color-kapacitor: $c-rainforest; width: $resizer-line-width; } } - -/* - Resizable Container Draggable Title - ------------------------------------------------------------------------------ -*/ - -.resizer--title { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 30px; - line-height: 30px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - background-color: $g3-castle; - padding: 0 11px; - z-index: $resizer-clickable-z; - font-size: 13px; - font-weight: 600; - color: $g13-mist; - @include no-user-select(); - - &:hover { - background-color: $g4-onyx; - } - - &.dragging { - cursor: ns-resize; - color: $c-laser; - background-color: $g5-pepper; - } -} - -.resizer--title.horizontal { - &:hover { - cursor: ns-resize; - } -} - -.resizer--title.vertical { - &:hover { - cursor: ew-resize; - } -} - -.resizer--contents { - width: 100%; - height: 100%; -} - -.resizer--title + .resizer--contents { - width: 100%; - height: calc(100% - 30px); - position: absolute; - top: 30px; - overflow: hidden; -} - diff --git a/ui/src/style/pages/dashboards.scss b/ui/src/style/pages/dashboards.scss index afcd69a0c6..50fd0db458 100644 --- a/ui/src/style/pages/dashboards.scss +++ b/ui/src/style/pages/dashboards.scss @@ -489,7 +489,18 @@ $dash-graph-options-arrow: 8px; Cell Editor Overlay ------------------------------------------------------ */ +<<<<<<< HEAD @import 'cell-editor-overlay'; +======= +.ceo-resizer { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; +} +@import 'overlay-technology'; +>>>>>>> Rebuild resizer component /* Template Variables Manager From 107b47f2030c0252ae002d1fedf4846571839541 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 12:11:26 -0700 Subject: [PATCH 020/104] Reintroduce divisions for Threesizer --- ui/src/ifql/components/TimeMachine.tsx | 39 +++++++++++++++++-------- ui/src/shared/components/Threesizer.tsx | 15 ++-------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 28d2932d7a..4c3c77bffa 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -3,9 +3,10 @@ import BodyBuilder from 'src/ifql/components/BodyBuilder' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineVis from 'src/ifql/components/TimeMachineVis' import Resizer from 'src/shared/components/ResizeContainer' +import Threesizer from 'src/shared/components/Threesizer' import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' -import {HANDLE_VERTICAL} from 'src/shared/constants/index' +import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' interface Props { script: string @@ -28,27 +29,41 @@ class TimeMachine extends PureComponent { orientation={HANDLE_VERTICAL} containerClass="page-contents" > - {this.renderVisualization()} - {this.renderEditor()} + {this.renderEditor} + {this.renderVisualization} ) } - private renderVisualization = () => { + private get renderVisualization() { return } - private renderEditor = () => { - const {script, body, suggestions, onChangeScript} = this.props - + private get renderEditor() { return ( -
- - -
Explorin all yer schemas
-
+ ) } + + private get divisions() { + const {script, body, suggestions, onChangeScript} = this.props + return [ + { + name: 'Editor', + render: () => ( + + ), + }, + { + name: 'Builder', + render: () => , + }, + { + name: 'Schema', + render: () =>
Explorin all yer schemas
, + }, + ] + } } export default TimeMachine diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index cc2b93f365..8ca43ab4b4 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -5,11 +5,7 @@ import _ from 'lodash' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' -import { - MIN_DIVISIONS, - HANDLE_HORIZONTAL, - HANDLE_VERTICAL, -} from 'src/shared/constants/' +import {HANDLE_HORIZONTAL, HANDLE_VERTICAL} from 'src/shared/constants/' const initialDragEvent = { percentX: 0, @@ -40,7 +36,7 @@ interface DivisionState extends Division { interface Props { divisions: Division[] orientation: string - containerClass: string + containerClass?: string } @ErrorHandling @@ -105,13 +101,6 @@ class Resizer extends Component { const {activeHandleID, divisions} = this.state const {orientation} = this.props - if (divisions.length < MIN_DIVISIONS) { - console.error( - `There must be at least ${MIN_DIVISIONS}' divisions in Resizer` - ) - return - } - return (
Date: Wed, 2 May 2018 12:17:29 -0700 Subject: [PATCH 021/104] First pass at Threesizer styles --- ui/src/style/chronograf.scss | 1 + ui/src/style/components/threesizer.scss | 57 +++++++++++++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 ui/src/style/components/threesizer.scss diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index 6bb48c8834..d41eb59e87 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -68,6 +68,7 @@ @import 'components/source-selector'; @import 'components/tables'; @import 'components/table-graph'; +@import 'components/threesizer'; @import 'components/threshold-controls'; @import 'components/kapacitor-logs-table'; @import 'components/func-node.scss'; diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss new file mode 100644 index 0000000000..84e0f76460 --- /dev/null +++ b/ui/src/style/components/threesizer.scss @@ -0,0 +1,57 @@ +/* + Resizable Container with 3 divisions + ------------------------------------------------------------------------------ +*/ + +.threesizer { + position: relative; + width: 100%; + height: 100%; + display: flex; + flex-direction: column; + align-items: stretch; + + &.dragging * { + @include no-user-select(); + } +} + +/* + Draggable Handle With Title + ------------------------------------------------------------------------------ +*/ +.threesizer--handle { + height: 30px; + background-color: $g4-onyx; + padding: 0 12px; + line-height: 30px; + font-size: 13px; + font-weight: 500; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + color: $g11-sidewalk; + transition: background-color 0.25s ease, color 0.25s ease; + + &:hover { + cursor: row-resize; + color: $g16-pearl; + background-color: $g5-pepper; + } + + &.dragging { + cursor: row-resize; + color: $c-laser; + background-color: $g5-pepper; + } +} + +.threesizer--division { + position: relative; + border: 2px solid $g4-onyx; + border-top: 0; + transition: border-color 0.25s ease; +} +.threesizer--handle.dragging + .threesizer--division { + border-color: $g5-pepper; +} \ No newline at end of file From 8b28676bf6b2fbb1792e162739397c27096fa935 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 12:26:28 -0700 Subject: [PATCH 022/104] Contain three sizer contents --- ui/src/style/components/threesizer.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 84e0f76460..410d7c000d 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -51,6 +51,7 @@ border: 2px solid $g4-onyx; border-top: 0; transition: border-color 0.25s ease; + overflow: hidden; } .threesizer--handle.dragging + .threesizer--division { border-color: $g5-pepper; From 93f492272033d547d202ba8d184a151da1d148d7 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 12:26:30 -0700 Subject: [PATCH 023/104] Remove a lot from divisor --- ui/src/shared/components/ResizeDivision.tsx | 67 +++++---------------- 1 file changed, 15 insertions(+), 52 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index a00c655e0b..358327628a 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -31,34 +31,30 @@ class Division extends PureComponent { } public render() { - const {render} = this.props - - return ( -
- {this.dragHandle} -
{render()}
-
- ) - } - - private get dragHandle() { - const {name, activeHandleID, orientation, id, draggable} = this.props + const {render, draggable} = this.props if (!name && !draggable) { return null } return ( - + <> +
+ {name} +
+
+ {render()} +
+ ) } + private get style() { + return { + height: `calc(${this.props.size}% - 30px)`, + } + } + private get dragCallback() { const {draggable} = this.props if (!draggable) { @@ -67,39 +63,6 @@ class Division extends PureComponent { return this.props.onHandleStartDrag } - - private get style() { - const {orientation, maxPercent, minPixels, size} = this.props - - const sizePercent = `${size * HUNDRED}%` - // const max = `${maxPercent * HUNDRED}%` - const max = '100%' - - if (orientation === HANDLE_VERTICAL) { - return { - top: '0', - width: sizePercent, - minWidth: minPixels, - maxWidth: max, - } - } - - return { - left: '0', - height: sizePercent, - minHeight: minPixels, - maxHeight: max, - } - } - - private get className(): string { - const {orientation} = this.props - - return classnames('resizer--division', { - resizer__vertical: orientation === HANDLE_VERTICAL, - resizer__horizontal: orientation === HANDLE_HORIZONTAL, - }) - } } export default Division From 3b33cc8f7bd10bf8c4e4bfeefdab331f78f254f1 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 13:32:38 -0700 Subject: [PATCH 024/104] Make the threesizer great again --- ui/src/shared/components/ResizeDivision.tsx | 35 ++++--------- ui/src/shared/components/Threesizer.tsx | 55 +++++++-------------- 2 files changed, 27 insertions(+), 63 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 358327628a..f6763dd817 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,14 +1,4 @@ -import React, {PureComponent, ReactElement} from 'react' -import classnames from 'classnames' -import ResizeHandle, { - OnHandleStartDrag, -} from 'src/shared/components/ResizeHandle' - -import { - HANDLE_VERTICAL, - HANDLE_HORIZONTAL, - HUNDRED, -} from 'src/shared/constants/' +import React, {PureComponent, ReactElement, MouseEvent} from 'react' const NOOP = () => {} @@ -21,7 +11,7 @@ interface Props { draggable: boolean orientation: string render: () => ReactElement - onHandleStartDrag: OnHandleStartDrag + onHandleStartDrag: (id: string, e: MouseEvent) => any maxPercent: number } @@ -31,18 +21,13 @@ class Division extends PureComponent { } public render() { - const {render, draggable} = this.props - - if (!name && !draggable) { - return null - } - + const {name, render} = this.props return ( <> -
- {name} -
+
+ {name} +
{render()}
@@ -51,17 +36,17 @@ class Division extends PureComponent { private get style() { return { - height: `calc(${this.props.size}% - 30px)`, + height: `calc((100% - 90px) * ${this.props.size} + 30px)`, } } - private get dragCallback() { - const {draggable} = this.props + private dragCallback = e => { + const {draggable, id} = this.props if (!draggable) { return NOOP } - return this.props.onHandleStartDrag + return this.props.onHandleStartDrag(id, e) } } diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 8ca43ab4b4..2ee1f679bb 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -137,10 +137,10 @@ class Resizer extends Component { } private get className(): string { - const {orientation, containerClass} = this.props + const {orientation} = this.props const {activeHandleID} = this.state - return classnames(`resize--container ${containerClass}`, { + return classnames(`threesizer`, { 'resize--dragging': activeHandleID, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, @@ -261,12 +261,12 @@ class Resizer extends Component { private taller = (size: number): number => { const newSize = size + this.percentChangeY - return Number(newSize.toFixed(3)) + return newSize > 1 ? 1 : newSize } private shorter = (size: number): number => { const newSize = size - this.percentChangeY - return Number(newSize.toFixed(3)) + return newSize < 0 ? 0 : newSize } private isAtMinHeight = (division: DivisionState): boolean => { @@ -291,42 +291,24 @@ class Resizer extends Component { private up = activePosition => () => { const divisions = this.state.divisions.map((d, i, divs) => { - const before = i < activePosition + const first = i === 0 + const before = i === activePosition - 1 const current = i === activePosition - if (before) { + if (first) { const below = divs[i + 1] - const aboveCurrent = i === activePosition - 1 - - const belowIsCurrent = below.id === divs[activePosition].id - - if (belowIsCurrent) { - return {...d} + if (below.size === 0) { + return {...d, size: this.shorter(d.size)} } - if (this.isAtMinHeight(below) || aboveCurrent) { - const size = this.shorter(d.size) - if (size < 0) { - debugger - } + return {...d} + } - return {...d, size} - } + if (before) { + return {...d, size: this.shorter(d.size)} } if (current) { - const stayStill = divs.every((div, idx) => { - if (idx >= i) { - return true - } - - return this.isAtMinHeight(div) - }) - - if (stayStill) { - return {...d} - } - return {...d, size: this.taller(d.size)} } @@ -340,7 +322,7 @@ class Resizer extends Component { const divisions = this.state.divisions.map((d, i, divs) => { const before = i === activePosition - 1 const current = i === activePosition - const after = i > activePosition + const after = i === activePosition + 1 if (before) { return {...d, size: this.taller(d.size)} @@ -352,14 +334,11 @@ class Resizer extends Component { if (after) { const above = divs[i - 1] - - if (this.isAtMinHeight(d)) { - return {...d} - } - - if (this.isAtMinHeight(above)) { + if (above.size === 0) { return {...d, size: this.shorter(d.size)} } + + return {...d} } return {...d} From e0e3d4fa2f5d8b0f907901cbae3edc990b96ce82 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 14:09:41 -0700 Subject: [PATCH 025/104] Stop resizing when there is no active handle --- ui/src/shared/components/Threesizer.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 2ee1f679bb..79a06b96b0 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -77,6 +77,10 @@ class Resizer extends Component { dragEvent.mouseY ) + if (!this.state.activeHandleID) { + return + } + if (orientation === HANDLE_VERTICAL) { const left = dragEvent.percentX < prevState.dragEvent.percentX @@ -166,6 +170,7 @@ class Resizer extends Component { } private handleStopDrag = () => { + console.log('handleStopDrag') this.setState({activeHandleID: '', dragEvent: initialDragEvent}) } @@ -290,18 +295,13 @@ class Resizer extends Component { } private up = activePosition => () => { - const divisions = this.state.divisions.map((d, i, divs) => { + const divisions = this.state.divisions.map((d, i) => { const first = i === 0 const before = i === activePosition - 1 const current = i === activePosition - if (first) { - const below = divs[i + 1] - if (below.size === 0) { - return {...d, size: this.shorter(d.size)} - } - - return {...d} + if (first && activePosition) { + return {...d, size: this.shorter(d.size)} } if (before) { From 54810d97df6e65b5c0be65319c0b32ccb5085e83 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 14:25:09 -0700 Subject: [PATCH 026/104] Fix resize up --- ui/src/shared/components/ResizeDivision.tsx | 1 - ui/src/shared/components/Threesizer.tsx | 69 ++++----------------- 2 files changed, 13 insertions(+), 57 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index f6763dd817..d06ee0a38f 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -12,7 +12,6 @@ interface Props { orientation: string render: () => ReactElement onHandleStartDrag: (id: string, e: MouseEvent) => any - maxPercent: number } class Division extends PureComponent { diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 79a06b96b0..c679dc21a5 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -40,7 +40,7 @@ interface Props { } @ErrorHandling -class Resizer extends Component { +class Threesizer extends Component { public static defaultProps: Partial = { orientation: HANDLE_HORIZONTAL, } @@ -124,7 +124,6 @@ class Resizer extends Component { orientation={orientation} activeHandleID={activeHandleID} onHandleStartDrag={this.handleStartDrag} - maxPercent={this.maximumHeightPercent} render={this.props.divisions[i].render} /> ))} @@ -132,14 +131,6 @@ class Resizer extends Component { ) } - private minPercent = (minPixels: number): number => { - if (this.props.orientation === HANDLE_VERTICAL) { - return this.minPercentX(minPixels) - } - - return this.minPercentY(minPixels) - } - private get className(): string { const {orientation} = this.props const {activeHandleID} = this.state @@ -274,10 +265,6 @@ class Resizer extends Component { return newSize < 0 ? 0 : newSize } - private isAtMinHeight = (division: DivisionState): boolean => { - return division.size <= this.minPercentY(division.minPixels) - } - private get move() { const {activeHandleID} = this.state @@ -296,12 +283,21 @@ class Resizer extends Component { private up = activePosition => () => { const divisions = this.state.divisions.map((d, i) => { + if (!activePosition) { + return d + } + const first = i === 0 const before = i === activePosition - 1 const current = i === activePosition - if (first && activePosition) { - return {...d, size: this.shorter(d.size)} + if (first && !before) { + const second = this.state.divisions[1] + if (second.size === 0) { + return {...d, size: this.shorter(d.size)} + } + + return {...d} } if (before) { @@ -380,45 +376,6 @@ class Resizer extends Component { this.setState({divisions}) } - - private enforceSize = (size, minPixels): number => { - const minPercent = this.minPercent(minPixels) - - let enforcedSize = size - if (size < minPercent) { - enforcedSize = minPercent - } - - return enforcedSize - } - - private cleanDivisions = divisions => { - const minSizes = divisions.map(d => { - const size = this.enforceSize(d.size, d.minPixels) - return {...d, size} - }) - - const sumSizes = minSizes.reduce((acc, div, i) => { - if (i <= divisions.length - 1) { - return acc + div.size - } - - return acc - }, 0) - - const under100percent = 1 - sumSizes > 0 - const over100percent = 1 - sumSizes < 0 - - if (under100percent) { - minSizes[divisions.length - 1].size += Math.abs(1 - sumSizes) - } - - if (over100percent) { - minSizes[divisions.length - 1].size -= Math.abs(1 - sumSizes) - } - - return minSizes - } } -export default Resizer +export default Threesizer From e5a5bcb297c06c16e2e37d146198b07a126edf1a Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 14:27:01 -0700 Subject: [PATCH 027/104] WIP style IFQL builder --- ui/src/ifql/components/BodyBuilder.tsx | 2 +- ui/src/ifql/components/From.tsx | 7 ++-- ui/src/ifql/components/FuncArg.tsx | 9 +++-- ui/src/ifql/components/FuncArgBool.tsx | 8 ++-- ui/src/ifql/components/FuncArgInput.tsx | 31 +++++++++------- ui/src/ifql/components/TimeMachine.tsx | 2 +- ui/src/style/components/func-node.scss | 49 ++++++++++++++++++++++--- 7 files changed, 78 insertions(+), 30 deletions(-) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index 256d80038b..dc327f0975 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -52,7 +52,7 @@ class BodyBuilder extends PureComponent { ) }) - return _.flatten(bodybuilder) + return
{_.flatten(bodybuilder)}
} private get funcNames() { diff --git a/ui/src/ifql/components/From.tsx b/ui/src/ifql/components/From.tsx index 4ecdf9b73b..4b4f038a6f 100644 --- a/ui/src/ifql/components/From.tsx +++ b/ui/src/ifql/components/From.tsx @@ -41,12 +41,13 @@ class From extends PureComponent { public render() { const {value, argKey} = this.props + return ( -
- +
+ { // TODO: make separate function component return (
- {argKey} : {value} + +
{value}
) } @@ -96,14 +97,16 @@ class FuncArg extends PureComponent { // TODO: handle nil type return (
- {argKey} : {value} + +
{value}
) } default: { return (
- {argKey} : {value} + +
{value}
) } diff --git a/ui/src/ifql/components/FuncArgBool.tsx b/ui/src/ifql/components/FuncArgBool.tsx index e87b0f8dcd..792f36c1cb 100644 --- a/ui/src/ifql/components/FuncArgBool.tsx +++ b/ui/src/ifql/components/FuncArgBool.tsx @@ -16,9 +16,11 @@ interface Props { class FuncArgBool extends PureComponent { public render() { return ( -
- {this.props.argKey}: - +
+ +
+ +
) } diff --git a/ui/src/ifql/components/FuncArgInput.tsx b/ui/src/ifql/components/FuncArgInput.tsx index 644e8f316e..29fff6a4a3 100644 --- a/ui/src/ifql/components/FuncArgInput.tsx +++ b/ui/src/ifql/components/FuncArgInput.tsx @@ -17,20 +17,25 @@ interface Props { class FuncArgInput extends PureComponent { public render() { const {argKey, value, type} = this.props + return ( -
- - +
+ +
+ +
) } diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 4c3c77bffa..e45438651d 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -24,7 +24,7 @@ class TimeMachine extends PureComponent { public render() { return ( Date: Wed, 2 May 2018 14:29:46 -0700 Subject: [PATCH 028/104] Undo resizer z-index hack Will find a better solution for this later --- ui/src/style/components/resizer.scss | 5 ----- 1 file changed, 5 deletions(-) diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 18724ba884..246a8c4525 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -4,7 +4,6 @@ */ $resizer-division-z: 1; -$resizer-division-hover-z: 5; $resizer-clickable-z: 2; $resizer-line-z: 3; $resizer-handle-z: 4; @@ -41,10 +40,6 @@ $resizer-color-kapacitor: $c-rainforest; top: 0; } - &:hover { - z-index: $resizer-division-hover-z; - } - .dragging & { pointer-events: none; } From 64898d14e3002038420c9b1ea7dbe1cc21d7ffa9 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 14:43:33 -0700 Subject: [PATCH 029/104] Add dbclick to full size --- ui/src/shared/components/ResizeDivision.tsx | 23 ++++++- ui/src/shared/components/Threesizer.tsx | 70 +++++++-------------- 2 files changed, 44 insertions(+), 49 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index d06ee0a38f..7d482c6ded 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -11,7 +11,8 @@ interface Props { draggable: boolean orientation: string render: () => ReactElement - onHandleStartDrag: (id: string, e: MouseEvent) => any + onHandleStartDrag: (id: string, e: MouseEvent) => void + onDoubleClick: (id: string) => void } class Division extends PureComponent { @@ -24,7 +25,11 @@ class Division extends PureComponent { return ( <>
-
+
{name}
{render()} @@ -33,12 +38,26 @@ class Division extends PureComponent { ) } + private get disabled(): string { + const {draggable} = this.props + if (draggable) { + return '' + } + + return 'disabled' + } + private get style() { return { height: `calc((100% - 90px) * ${this.props.size} + 30px)`, } } + private handleDoubleClick = () => { + const {id, onDoubleClick} = this.props + return onDoubleClick(id) + } + private dragCallback = e => { const {draggable, id} = this.props if (!draggable) { diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index c679dc21a5..01f1ba6f31 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -123,6 +123,7 @@ class Threesizer extends Component { minPixels={d.minPixels} orientation={orientation} activeHandleID={activeHandleID} + onDoubleClick={this.handleDoubleClick} onHandleStartDrag={this.handleStartDrag} render={this.props.divisions[i].render} /> @@ -155,13 +156,24 @@ class Threesizer extends Component { })) } + private handleDoubleClick = (id: string): void => { + const divisions = this.state.divisions.map(d => { + if (d.id !== id) { + return {...d, size: 0} + } + + return {...d, size: 1} + }) + + this.setState({divisions}) + } + private handleStartDrag = (activeHandleID, e: MouseEvent) => { const dragEvent = this.mousePosWithinContainer(e) this.setState({activeHandleID, dragEvent}) } private handleStopDrag = () => { - console.log('handleStopDrag') this.setState({activeHandleID: '', dragEvent: initialDragEvent}) } @@ -209,42 +221,6 @@ class Threesizer extends Component { return Math.abs(delta / height) } - private minPercentX = (xMinPixels: number): number => { - if (!this.containerRef) { - return 0 - } - const {width} = this.containerRef.getBoundingClientRect() - - return xMinPixels / width - } - - private minPercentY = (yMinPixels: number): number => { - if (!this.containerRef) { - return 0 - } - - const {height} = this.containerRef.getBoundingClientRect() - return yMinPixels / height - } - - private get maximumHeightPercent(): number { - if (!this.containerRef) { - return 1 - } - - const {divisions} = this.state - const {height} = this.containerRef.getBoundingClientRect() - - const totalMinPixels = divisions.reduce( - (acc, div) => acc + div.minPixels, - 0 - ) - - const maximumPixels = height - totalMinPixels - - return this.minPercentY(maximumPixels) - } - private handleDrag = (e: MouseEvent) => { const {activeHandleID} = this.state if (!activeHandleID) { @@ -255,16 +231,6 @@ class Threesizer extends Component { this.setState({dragEvent}) } - private taller = (size: number): number => { - const newSize = size + this.percentChangeY - return newSize > 1 ? 1 : newSize - } - - private shorter = (size: number): number => { - const newSize = size - this.percentChangeY - return newSize < 0 ? 0 : newSize - } - private get move() { const {activeHandleID} = this.state @@ -376,6 +342,16 @@ class Threesizer extends Component { this.setState({divisions}) } + + private taller = (size: number): number => { + const newSize = size + this.percentChangeY + return newSize > 1 ? 1 : newSize + } + + private shorter = (size: number): number => { + const newSize = size - this.percentChangeY + return newSize < 0 ? 0 : newSize + } } export default Threesizer From ace6ebb083289a8d22a02d877d7e973852f5f082 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 15:02:50 -0700 Subject: [PATCH 030/104] Add builder to right side of ifql page --- ui/src/ifql/components/TimeMachine.tsx | 28 ++++++++++++++++++-------- 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index e45438651d..ccdbfe6d6a 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -30,13 +30,29 @@ class TimeMachine extends PureComponent { containerClass="page-contents" > {this.renderEditor} - {this.renderVisualization} + {this.renderRightSide} ) } - private get renderVisualization() { - return + private get renderRightSide() { + return ( + + ) + } + + private get visPlusBuilder() { + const {body, suggestions} = this.props + return [ + { + name: 'Builder', + render: () => , + }, + { + name: 'Visualization', + render: () => , + + }, } private get renderEditor() { @@ -46,7 +62,7 @@ class TimeMachine extends PureComponent { } private get divisions() { - const {script, body, suggestions, onChangeScript} = this.props + const {script, onChangeScript} = this.props return [ { name: 'Editor', @@ -54,10 +70,6 @@ class TimeMachine extends PureComponent { ), }, - { - name: 'Builder', - render: () => , - }, { name: 'Schema', render: () =>
Explorin all yer schemas
, From 3c9d38621223fa4177d63d342614b0b0cf7e0c1c Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 15:06:14 -0700 Subject: [PATCH 031/104] Style threesizer dragging state --- ui/src/shared/components/ResizeDivision.tsx | 31 +++++++++------- ui/src/shared/components/Threesizer.tsx | 2 +- ui/src/style/components/threesizer.scss | 40 ++++++++++++--------- 3 files changed, 43 insertions(+), 30 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 7d482c6ded..b6ccdd199f 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,4 +1,7 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react' +import classnames from 'classnames' + +import FancyScrollbar from 'src/shared/components/FancyScrollbar' const NOOP = () => {} @@ -24,35 +27,37 @@ class Division extends PureComponent { const {name, render} = this.props return ( <> -
+
{name}
- {render()} + + {render()} +
) } - private get disabled(): string { - const {draggable} = this.props - if (draggable) { - return '' - } - - return 'disabled' - } - - private get style() { + private get containerStyle() { return { height: `calc((100% - 90px) * ${this.props.size} + 30px)`, } } + private get className(): string { + const {draggable, id, activeHandleID} = this.props + + return classnames('threesizer--handle', { + disabled: !draggable, + dragging: id === activeHandleID, + }) + } + private handleDoubleClick = () => { const {id, onDoubleClick} = this.props return onDoubleClick(id) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 01f1ba6f31..f9cee43f31 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -137,7 +137,7 @@ class Threesizer extends Component { const {activeHandleID} = this.state return classnames(`threesizer`, { - 'resize--dragging': activeHandleID, + dragging: activeHandleID, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, }) diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 410d7c000d..1dc9bac23f 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -3,6 +3,10 @@ ------------------------------------------------------------------------------ */ +$threesizer-handle: 30px; +$threesizer-handle-z: 2; +$threesizer-contents-z: 1; + .threesizer { position: relative; width: 100%; @@ -11,20 +15,26 @@ flex-direction: column; align-items: stretch; - &.dragging * { + &.dragging .threesizer--division { @include no-user-select(); + pointer-events: none; } } -/* - Draggable Handle With Title - ------------------------------------------------------------------------------ -*/ +.threesizer--division { + position: relative; + overflow: hidden; +} + +/* Draggable Handle With Title */ .threesizer--handle { - height: 30px; + position: relative; + z-index: $threesizer-handle-z; + @include no-user-select(); + height: $threesizer-handle; background-color: $g4-onyx; padding: 0 12px; - line-height: 30px; + line-height: $threesizer-handle; font-size: 13px; font-weight: 500; overflow: hidden; @@ -46,13 +56,11 @@ } } -.threesizer--division { - position: relative; - border: 2px solid $g4-onyx; - border-top: 0; - transition: border-color 0.25s ease; - overflow: hidden; +/* Division Contents */ +.threesizer--contents { + position: absolute !important; + top: $threesizer-handle; + z-index: $threesizer-contents-z; + width: 100%; + height: calc(100% - #{$threesizer-handle}) !important; } -.threesizer--handle.dragging + .threesizer--division { - border-color: $g5-pepper; -} \ No newline at end of file From d146d326d5b6410aae15f83d52c3b5cf35fd3691 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 16:01:14 -0700 Subject: [PATCH 032/104] Add logic to expand all on double click --- ui/src/shared/components/Threesizer.tsx | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index f9cee43f31..7bb791612b 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -157,6 +157,18 @@ class Threesizer extends Component { } private handleDoubleClick = (id: string): void => { + const clickedDiv = this.state.divisions.find(d => d.id === id) + + if (!clickedDiv) { + return + } + + const isFullSized = clickedDiv.size === 1 + + if (isFullSized) { + return this.expandAll() + } + const divisions = this.state.divisions.map(d => { if (d.id !== id) { return {...d, size: 0} @@ -168,6 +180,14 @@ class Threesizer extends Component { this.setState({divisions}) } + private expandAll = () => { + const divisions = this.state.divisions.map(d => { + return {...d, size: 1 / this.state.divisions.length} + }) + + this.setState({divisions}) + } + private handleStartDrag = (activeHandleID, e: MouseEvent) => { const dragEvent = this.mousePosWithinContainer(e) this.setState({activeHandleID, dragEvent}) From 59241770c3fd1e75ada2b8cb8f461800bb5360e1 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 17:05:52 -0700 Subject: [PATCH 033/104] Add 1.0 schema explorer to IFQL Builder --- ui/src/ifql/components/DatabaseList.tsx | 82 +++++++++++++++++++++ ui/src/ifql/components/DatabaseListItem.tsx | 32 ++++++++ ui/src/ifql/components/SchemaExplorer.tsx | 47 ++++++++++++ ui/src/ifql/components/TimeMachine.tsx | 11 +-- ui/src/shared/components/DatabaseList.tsx | 2 +- 5 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 ui/src/ifql/components/DatabaseList.tsx create mode 100644 ui/src/ifql/components/DatabaseListItem.tsx create mode 100644 ui/src/ifql/components/SchemaExplorer.tsx diff --git a/ui/src/ifql/components/DatabaseList.tsx b/ui/src/ifql/components/DatabaseList.tsx new file mode 100644 index 0000000000..3a5e6e3413 --- /dev/null +++ b/ui/src/ifql/components/DatabaseList.tsx @@ -0,0 +1,82 @@ +import React, {PureComponent} from 'react' +import _ from 'lodash' + +import DatabaseListItem from 'src/ifql/components/DatabaseListItem' + +import {Source} from 'src/types' + +import {showDatabases} from 'src/shared/apis/metaQuery' +import showDatabasesParser from 'src/shared/parsing/showDatabases' + +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface DatabaseListProps { + db: string + source: Source + onChooseDatabase: (database: string) => void +} + +interface DatabaseListState { + databases: string[] +} + +@ErrorHandling +class DatabaseList extends PureComponent { + constructor(props) { + super(props) + this.state = { + databases: [], + } + } + + public componentDidMount() { + this.getDatabases() + } + + public async getDatabases() { + const {source} = this.props + + try { + const {data} = await showDatabases(source.links.proxy) + const {databases} = showDatabasesParser(data) + const dbs = databases.map(database => { + if (database === '_internal') { + return `${database}.monitor` + } + + return `${database}.autogen` + }) + + const sorted = dbs.sort() + + this.setState({databases: sorted}) + const db = _.get(sorted, '0', '') + this.props.onChooseDatabase(db) + } catch (err) { + console.error(err) + } + } + + public render() { + const {onChooseDatabase} = this.props + + return ( +
+
+ {this.state.databases.map(db => { + return ( + + ) + })} +
+
+ ) + } +} + +export default DatabaseList diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx new file mode 100644 index 0000000000..1b2b2c664f --- /dev/null +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -0,0 +1,32 @@ +import React, {PureComponent} from 'react' + +import classnames from 'classnames' + +export interface Props { + isActive: boolean + db: string + onChooseDatabase: (db: string) => void +} + +class DatabaseListItem extends PureComponent { + public render() { + return ( +
+ {this.props.db} +
+ ) + } + + private get className(): string { + return classnames('query-builder--list-item', { + active: this.props.isActive, + }) + } + + private handleChooseDatabase = () => { + const {onChooseDatabase, db} = this.props + onChooseDatabase(db) + } +} + +export default DatabaseListItem diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx new file mode 100644 index 0000000000..ec187ccf8a --- /dev/null +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -0,0 +1,47 @@ +import React, {PureComponent} from 'react' +import PropTypes from 'prop-types' +import DatabaseList from 'src/ifql/components/DatabaseList' +import {Source} from 'src/types' + +interface Props { + source: Source +} + +interface State { + db: string +} + +const {shape} = PropTypes + +class SchemaExplorer extends PureComponent { + public static contextTypes = { + source: shape({ + links: shape({}).isRequired, + }).isRequired, + } + + constructor(props) { + super(props) + this.state = { + db: '', + } + } + + public render() { + return ( +
+ +
+ ) + } + + private handleChooseDatabase = (db: string): void => { + this.setState({db}) + } +} + +export default SchemaExplorer diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index ccdbfe6d6a..2ca53c887f 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -1,4 +1,5 @@ import React, {PureComponent} from 'react' +import SchemaExplorer from 'src/ifql/components/SchemaExplorer' import BodyBuilder from 'src/ifql/components/BodyBuilder' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineVis from 'src/ifql/components/TimeMachineVis' @@ -45,11 +46,11 @@ class TimeMachine extends PureComponent { const {body, suggestions} = this.props return [ { - name: 'Builder', + name: 'Build', render: () => , }, { - name: 'Visualization', + name: 'Visualize', render: () => , }, @@ -65,14 +66,14 @@ class TimeMachine extends PureComponent { const {script, onChangeScript} = this.props return [ { - name: 'Editor', + name: 'Script', render: () => ( ), }, { - name: 'Schema', - render: () =>
Explorin all yer schemas
, + name: 'Explore', + render: () => (), }, ] } diff --git a/ui/src/shared/components/DatabaseList.tsx b/ui/src/shared/components/DatabaseList.tsx index 032fcb1530..ce5d5854bf 100644 --- a/ui/src/shared/components/DatabaseList.tsx +++ b/ui/src/shared/components/DatabaseList.tsx @@ -1,5 +1,5 @@ -import PropTypes from 'prop-types' import React, {PureComponent} from 'react' +import PropTypes from 'prop-types' import _ from 'lodash' From c4035a3236528913f201305f781c00bdb55b10a9 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 18:03:32 -0700 Subject: [PATCH 034/104] Make IFQL builder stack horizontally --- ui/src/ifql/components/FuncArgs.tsx | 10 +- ui/src/ifql/components/FuncNode.tsx | 52 +++++-- ui/src/ifql/components/FuncSelector.tsx | 10 +- ui/src/style/components/func-node.scss | 167 ++++++++++++++-------- ui/src/style/components/funcs-button.scss | 33 ++++- 5 files changed, 198 insertions(+), 74 deletions(-) diff --git a/ui/src/ifql/components/FuncArgs.tsx b/ui/src/ifql/components/FuncArgs.tsx index 10c75d4cf1..6f68dea86c 100644 --- a/ui/src/ifql/components/FuncArgs.tsx +++ b/ui/src/ifql/components/FuncArgs.tsx @@ -10,6 +10,7 @@ interface Props { onChangeArg: OnChangeArg declarationID: string onGenerateScript: () => void + onDeleteFunc: () => void } @ErrorHandling @@ -19,12 +20,13 @@ export default class FuncArgs extends PureComponent { func, bodyID, onChangeArg, + onDeleteFunc, declarationID, onGenerateScript, } = this.props return ( -
+
{func.args.map(({key, value, type}) => { return ( { /> ) })} +
+ Delete +
) } diff --git a/ui/src/ifql/components/FuncNode.tsx b/ui/src/ifql/components/FuncNode.tsx index 2f1d679e1c..747d630d40 100644 --- a/ui/src/ifql/components/FuncNode.tsx +++ b/ui/src/ifql/components/FuncNode.tsx @@ -13,7 +13,7 @@ interface Props { } interface State { - isOpen: boolean + isExpanded: boolean } @ErrorHandling @@ -25,7 +25,7 @@ export default class FuncNode extends PureComponent { constructor(props) { super(props) this.state = { - isOpen: true, + isExpanded: false, } } @@ -37,37 +37,65 @@ export default class FuncNode extends PureComponent { declarationID, onGenerateScript, } = this.props - const {isOpen} = this.state + const {isExpanded} = this.state return ( -
-
-
{func.name}
-
- {isOpen && ( +
+
{func.name}
+
{this.stringifyArgs}
+ {isExpanded && ( )} -
) } + private get stringifyArgs(): string { + const { + func: {args}, + } = this.props + + if (!args) { + return + } + + return args.reduce((acc, arg, i) => { + if (!arg.value) { + return acc + } + + const separator = i === 0 ? '' : ', ' + + return `${acc}${separator}${arg.key}: ${arg.value}` + }, '') + } + private handleDelete = (): void => { const {func, bodyID, declarationID} = this.props this.props.onDelete({funcID: func.id, bodyID, declarationID}) } - private handleClick = (e: MouseEvent): void => { + private handleMouseEnter = (e: MouseEvent): void => { e.stopPropagation() - const {isOpen} = this.state - this.setState({isOpen: !isOpen}) + this.setState({isExpanded: true}) + } + + private handleMouseLeave = (e: MouseEvent): void => { + e.stopPropagation() + + this.setState({isExpanded: false}) } } diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 6e2fdae10e..88ab442cdd 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -1,5 +1,6 @@ import React, {PureComponent, ChangeEvent, KeyboardEvent} from 'react' import _ from 'lodash' +import classnames from 'classnames' import {ClickOutside} from 'src/shared/components/ClickOutside' import FuncList from 'src/ifql/components/FuncList' @@ -36,7 +37,8 @@ export class FuncSelector extends PureComponent { return ( -
+
+
{isOpen ? ( { ) } + private get className(): string { + const {isOpen} = this.state + + return classnames('ifql-func--selector', {open: isOpen}) + } + private handleCloseList = () => { this.setState({isOpen: false, selectedFunc: ''}) } diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss index a51d83c009..a256ceed10 100644 --- a/ui/src/style/components/func-node.scss +++ b/ui/src/style/components/func-node.scss @@ -1,13 +1,36 @@ +$ifql-node-height: 30px; +$ifql-node-tooltip-gap: $ifql-node-height + 4px; +$ifql-node-gap: 5px; +$ifql-node-padding: 10px; +$ifql-arg-min-width: 120px; + +/* + Shared Node styles + ------------------ +*/ +%ifql-node { + height: $ifql-node-height; + border-radius: $radius; + padding: 0 $ifql-node-padding; + font-size: 13px; + font-weight: 600; + position: relative; +} + .body-builder { - padding: 12px; + padding: 12px 30px; min-width: 440px; overflow: hidden; - background-color: $g2-kevlar; + height: 100%; + width: 100%; + background-color: $g1-raven; } .declaration { width: 100%; - margin-bottom: 12px; + margin-bottom: 24px; + display: flex; + flex-wrap: wrap; &:last-of-type { margin-bottom: 0; @@ -15,84 +38,114 @@ } .variable-name { - font-size: 13px; - font-family: $code-font; - color: $c-honeydew; - padding: 5px 10px; + @extend %ifql-node; + // font-family: $code-font; + color: $c-laser; + line-height: $ifql-node-height; + white-space: nowrap; background-color: $g3-castle; - border-radius: $radius; - margin-bottom: 2px; - width: 100%; @include no-user-select(); } - .expression-node { - width: 100%; display: flex; - flex-direction: column; } .func-node { - width: 100%; + @extend %ifql-node; display: flex; - align-items: stretch; - position: relative; - margin-bottom: 2px; + align-items: center; + background-color: $g4-onyx; + margin-left: $ifql-node-gap; + transition: background-color 0.25s ease; + + // Connection Line + &:after { + content: ''; + height: 4px; + width: $ifql-node-gap; + background-color: $g4-onyx; + position: absolute; + top: 50%; + left: 0; + transform: translate(-100%, -50%); + } + + &:hover { + background-color: $g6-smoke; + } +} +.func-node--name, +.func-node--preview { + font-size: 13px; + @include no-user-select(); + white-space: nowrap; + transition: color 0.25s ease; + font-weight: 600; } .func-node--name { - background-color: $g4-onyx; - padding: 10px; - color: $ix-text-default; - font-size: 13px; - border-radius: $radius 0 0 $radius; - font-weight: 600; - width: 130px; - @include no-user-select(); - display: flex; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - align-items: center; - align-content: center; + color: $c-comet; + + .func-node:hover & { + color: $c-potassium; + } } -.func-args { +.func-node--preview { + color: $g13-mist; + margin-left: 4px; + + .func-node:hover & { + color: $g17-whisper; + } +} + + +.func-node--tooltip { background-color: $g3-castle; - border-radius: 0 $radius $radius 0; + border-radius: $radius; padding: 10px; - flex: 1 0 0; display: flex; align-items: stretch; flex-direction: column; - color: $ix-text-default; - font-family: $ix-text-font; - font-weight: 500; + position: absolute; + top: $ifql-node-tooltip-gap; + left: 0; + z-index: 9999; + box-shadow: 0 0 10px 2px $g2-kevlar; + + // Caret + &:before { + content: ''; + border-width: 9px; + border-style: solid; + border-color: transparent; + border-bottom-color: $g3-castle; + position: absolute; + top: 0; + left: $ifql-node-padding + 3px; + transform: translate(-50%, -100%); + } + + // Invisible block to continue hovering + &:after { + content: ''; + width: 80%; + height: 7px; + position: absolute; + top: -7px; + left: 0; + } } .func-node--delete { - position: absolute; - width: 22px; - height: 22px; - top: 0; - right: -26px; - border-radius: 50%; - background-color: $c-curacao; - opacity: 0; - transition: background-color 0.25s ease, opacity 0.25s ease; - - &:hover { - background-color: $c-dreamsicle; - cursor: pointer; - } - - .func-node:hover & { - opacity: 1; - } + margin-top: 12px; + width: 60px; } .func-arg { + min-width: $ifql-arg-min-width; display: flex; flex-wrap: nowrap; align-items: center; @@ -104,10 +157,10 @@ } .func-arg--label { white-space: nowrap; - font-size: 12px; + font-size: 13px; font-weight: 600; color: $g10-wolf; - min-width: 40px; + padding-right: 8px; @include no-user-select(); } .func-arg--value { diff --git a/ui/src/style/components/funcs-button.scss b/ui/src/style/components/funcs-button.scss index 6862571ec3..ec200d9b3d 100644 --- a/ui/src/style/components/funcs-button.scss +++ b/ui/src/style/components/funcs-button.scss @@ -3,11 +3,37 @@ ---------------------------------------------------------------------------- */ +$ifql-func-selector--gap: 10px; +$ifql-func-selector--height: 30px; + .ifql-func--selector { + display: flex; + align-items: center; position: relative; + + &.open { + z-index: 9999; + } +} + +.func-selector--connector { + width: $ifql-func-selector--gap; + height: $ifql-func-selector--height; + position: relative; + + &:after { + content: ''; + position: absolute; + top: 50%; + width: 100%; + height: 4px; + transform: translateY(-50%); + @include gradient-h($g4-onyx, $c-pool); + } } .ifql-func--button { + float: left; &:focus { box-shadow: 0 0 8px 3px $c-amethyst; } @@ -16,17 +42,18 @@ .ifql-func--autocomplete, .ifql-func--list { position: absolute; - left: 0; width: 166px; } .ifql-func--autocomplete { + left: $ifql-func-selector--gap; top: 0; } .ifql-func--list { - border-radius: 4px; - top: 30px; + left: 0; + border-radius: $radius; + top: $ifql-func-selector--height; padding: 0; margin: 0; @extend %no-user-select; From 3427b99db4c82a4d3880711ddabbd489bd5e1089 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 19:11:44 -0700 Subject: [PATCH 035/104] Use fragment --- ui/src/ifql/components/ExpressionNode.tsx | 4 ++-- ui/src/style/components/func-node.scss | 4 ---- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/ui/src/ifql/components/ExpressionNode.tsx b/ui/src/ifql/components/ExpressionNode.tsx index 92b84251af..893b9c014b 100644 --- a/ui/src/ifql/components/ExpressionNode.tsx +++ b/ui/src/ifql/components/ExpressionNode.tsx @@ -21,7 +21,7 @@ class ExpressionNode extends PureComponent { {({onDeleteFuncNode, onAddNode, onChangeArg, onGenerateScript}) => { return ( -
+ <> {funcs.map(func => ( { onAddNode={onAddNode} declarationID={declarationID} /> -
+ ) }}
diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss index a256ceed10..e71a577cfb 100644 --- a/ui/src/style/components/func-node.scss +++ b/ui/src/style/components/func-node.scss @@ -47,10 +47,6 @@ $ifql-arg-min-width: 120px; @include no-user-select(); } -.expression-node { - display: flex; -} - .func-node { @extend %ifql-node; display: flex; From 876368605a7966e1326f6e8812803d45f9620bba Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 19:47:22 -0700 Subject: [PATCH 036/104] Add "function" icon and use in add func button Also adding Okta icon --- ui/src/ifql/components/FuncSelector.tsx | 2 +- ui/src/style/components/func-node.scss | 1 - ui/src/style/components/funcs-button.scss | 6 ++---- ui/src/style/fonts/icomoon.eot | Bin 12832 -> 13400 bytes ui/src/style/fonts/icomoon.svg | 2 ++ ui/src/style/fonts/icomoon.ttf | Bin 12668 -> 13236 bytes ui/src/style/fonts/icomoon.woff | Bin 12744 -> 13312 bytes ui/src/style/fonts/icomoon.woff2 | Bin 6280 -> 6588 bytes ui/src/style/fonts/icon-font.scss | 2 ++ 9 files changed, 7 insertions(+), 6 deletions(-) diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 88ab442cdd..e0986d77c8 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -55,7 +55,7 @@ export class FuncSelector extends PureComponent { onClick={this.handleOpenList} tabIndex={0} > - 𝑓⟮𝑥⟯ + )}
diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/func-node.scss index e71a577cfb..bdb0ba17d1 100644 --- a/ui/src/style/components/func-node.scss +++ b/ui/src/style/components/func-node.scss @@ -39,7 +39,6 @@ $ifql-arg-min-width: 120px; .variable-name { @extend %ifql-node; - // font-family: $code-font; color: $c-laser; line-height: $ifql-node-height; white-space: nowrap; diff --git a/ui/src/style/components/funcs-button.scss b/ui/src/style/components/funcs-button.scss index ec200d9b3d..f40a78e798 100644 --- a/ui/src/style/components/funcs-button.scss +++ b/ui/src/style/components/funcs-button.scss @@ -32,7 +32,8 @@ $ifql-func-selector--height: 30px; } } -.ifql-func--button { +.btn.btn-sm.ifql-func--button { + border-radius: 50%; float: left; &:focus { box-shadow: 0 0 8px 3px $c-amethyst; @@ -60,9 +61,6 @@ $ifql-func-selector--height: 30px; @include gradient-h($c-star, $c-pool); } -.ifql-func--input { -} - .ifql-func--item { height: 28px; line-height: 28px; diff --git a/ui/src/style/fonts/icomoon.eot b/ui/src/style/fonts/icomoon.eot index d4aca11f4d679b8dd0a7e6a14b2dc5adfd7da2a4..538f905f782689c6cdaf573f59f6ababe0eead95 100755 GIT binary patch delta 976 zcmYjQZD?C%6h800H#axwP3}i>-`kqCiTS$eSGpxPH_?_&KbZP)njbd-){@J-#a&vzzqI_q<=PnlxQF-5*=l*NBQ$mwfE+@+cfNda4xY(JFWMLdsq zw{qcZ`Dn_T1UOPbe5G1mn1f?+La?xdg}`jNdhY#~9s2+Y5r?=wcj4kZu*g92<^c~Z zyIr3m?|3#5nTR>?0)asE!#~veBa*K7GG;fU6S_q--P+gLGP4E!Xd5SXH<3sv$ZKBOPgpmx^HHNw)jLQs zVWqPq?ZKEbqut~HSQH6N47MB?j}J_=wH1A_n6GEf$wmcXGgNfF0bBhRbjbcTlnfr!(wcsw-|{GjV()iW}MEBvk(@LffMT zDoC(VPnnG+k_oEgu1$qj8z8CY8{2xR=EWDW^C1$9rc4WUnwMvfBu7S)w}&ljn7pN` z+0L%M@X#~8lV-Uzb~S?M>R72fSt`{c5tfPdJ=rMa@-zMpzkep5I}v)(FG`%A@C{}% zqpf&GGnv6=&N(?HAhG_M|IK{9KwdgNJ9`{2bM5IbLcy{|`S+}XK>U0$)7RCRRaNTo z&`6|KDor-cn#M{{4osL&7CZbixqP9J&!KgTs2G~y{J&WgVF-vxk=MuU|SX4LpVx4nI494LSV!f3dz6Pm^qYC;p1-7P2QfApHlb`cU6Dul;Qc z+lsu*U*XsJ9s3=jRag`rIo6y(XT|wx3nx~@&%|xlQP)TAF84j@x_nZJDkWt}b*nej kEzgQ)&%5B=)`qknw1+fIbM!m9hpq8%f$Zw3!(Wqs0e5EFFaQ7m delta 461 zcmcbSu^@#_!H9vO#&9B=8B6u%%+|>ZjYKAfB-h6?Ffi-^;)LYf#DbR*#SIJ$j50vX zlAc&x02CKsU|>=J(j4hIm1)lxZk`V0YcMcqAIV5fOcCS|*~q}4`vxd)mH`xCS744| zV9-+m@>Mc&ODZ0H;C}_=djR<#a`KZC)j6fVFfiy_0Qpt9i4_G5?s|-jK!Ghl0foH8 z+|-$Vod1FRD?kUd733F}FfapU7z}}iGAJ-GGrydi!dNZ(0?0E1@>v+f7#JB8)E2(v zdnq{i6Qen!z+??3Q(YEu2C@HQ-^9L(eHMEo_DbxT*i*45Vvocgirp1mE7~T&E5ObF zkpCY48ldTnlP5CiOkTjmy?HN_9p~iDJli%u<-4ya)0(-tI-cL=D+4zR$OR17zL+kD z(UTt-m^0c;mM{!vjbdS7w4PjHcsTI{%Qw~()@7`h*!tN1vFEYh;+VzB!5PFkjq??E z5ceGJ3p^S;UAzLki}-T*9Rzp;Yy=7fz6cfxo)W4MdL$etd_hD@WS7V_Q7%y((KVuv cfOY``hQWs6KLZ!YUIr$hLxneQHd@LE06b8C>i_@% diff --git a/ui/src/style/fonts/icomoon.svg b/ui/src/style/fonts/icomoon.svg index fbf49ee755..b8e1c4b5d8 100755 --- a/ui/src/style/fonts/icomoon.svg +++ b/ui/src/style/fonts/icomoon.svg @@ -23,8 +23,10 @@ + + diff --git a/ui/src/style/fonts/icomoon.ttf b/ui/src/style/fonts/icomoon.ttf index 32fe638b860f4c8272086b4a17d604b189d551c6..dead09541e9cbd67ded91d0fb458ed1e17f75ff4 100755 GIT binary patch delta 1015 zcmZ`&U1(cn7=FKVl9Q9Q1C)0EB=%sQ=ImqTYZxvc;g2j@B6%e z&-)zCw_UqA-2?&vPIwzw7&>|^o;dosa0(;0@SZx|Fz0)Er)~g{J&3ob&2#f;B><)i zEk9j<^OdV#-}?k{0AT;t%o+1#dGwFH07LD(F@phHfPD$^1me(4V_|XQp>!GX0^+Uu z+0zHigK47%FkHvL`G&bT4~Ge^G`xv8FlRQ-y!V`a2OuHf5|`%Bo?F00@g%Rc^T4tj z@2O&R^UJ0cV5iRl%o^Ch4H!B!c@hqRVvMJm}R2z$7`iRan|4C_s&J+x%lKqzGN7a+&e~zCw{J+ zjraHE6@_{{G#qYLDz#m*U1B*X1*)mXOFjPCLa|gT7LeL0$hwbr{FiLkM@kFFxvk~m z7?zRM4k3~PvqFXM_wBjz&FX6>#gRwgwBX+fuqK6G{MWXmScc?VW3gkd>zF-Z0g`K1 zwcd~2dEqZ>$Xezk?mV~3ZQ4HOyZ9ykPy09zgMmT&NJeU6iXex`Mg|7mH$Zu_44?qJ0&@%lgPsbI zuac2lQt{{m|0^Kh1IYi7lb`IEsLm<A<$3;1qNp3mlF?Ei@pGI%z#`L1~H(26x0^Jb9Fqw%~u9)7LdgZ*S?r8htZR549ppACYKllvqrHn zFj`N(VsJR|1j{$p6xL;|m)QE){;}t=-{P3X$-x=KIgRrbcM$g+?h8B`JYBp3yo>m9 z_#Fgz1Z)Hf1ilCs37!(F5PBpWCwxIfOJtYGHBl~69nm$SkAQZ8qR58fKLZ!YUIwPg N9~p%=^BXN?1OTl#c~SrX diff --git a/ui/src/style/fonts/icomoon.woff b/ui/src/style/fonts/icomoon.woff index 8e1ef4e99d0f9822d54680bf817f7fb7c74a9660..6deea0a9790b25b65e8a995f0bea13d7f34a65fa 100755 GIT binary patch delta 1041 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 diff --git a/ui/src/style/fonts/icon-font.scss b/ui/src/style/fonts/icon-font.scss index 070056c590..24c2c83750 100644 --- a/ui/src/style/fonts/icon-font.scss +++ b/ui/src/style/fonts/icon-font.scss @@ -30,6 +30,8 @@ -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; + &.function:before {content: "\e90f";} + &.okta:before {content: "\e912";} &.user-remove:before {content: "\e904";} &.user-add:before {content: "\e907";} &.group:before {content: "\e908";} From 7c40c417dc8685551180b4ce612533b05aeddd21 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 19:47:34 -0700 Subject: [PATCH 037/104] Improve micro copy --- ui/src/ifql/components/FuncList.tsx | 2 +- ui/src/shared/components/FuncSelectorInput.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/ifql/components/FuncList.tsx b/ui/src/ifql/components/FuncList.tsx index 902ee9adad..ea64c22972 100644 --- a/ui/src/ifql/components/FuncList.tsx +++ b/ui/src/ifql/components/FuncList.tsx @@ -48,7 +48,7 @@ const FuncList: SFC = ({ /> )) ) : ( -
No results
+
No matches
)} diff --git a/ui/src/shared/components/FuncSelectorInput.tsx b/ui/src/shared/components/FuncSelectorInput.tsx index 2415f89f52..8bff3ffde5 100644 --- a/ui/src/shared/components/FuncSelectorInput.tsx +++ b/ui/src/shared/components/FuncSelectorInput.tsx @@ -18,7 +18,7 @@ const FuncSelectorInput: SFC = ({ className="form-control input-sm ifql-func--input" type="text" autoFocus={true} - placeholder="Add Function..." + placeholder="Add a Function..." spellCheck={false} onChange={onFilterChange} onKeyDown={onFilterKeyPress} From 96ffc67d76bccf1d4f4dd357f21f1e77b6d0d0d8 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 18:04:25 -0700 Subject: [PATCH 038/104] Introduce MeasurementList to schema explorer --- ui/src/ifql/components/DatabaseList.tsx | 35 +++-- ui/src/ifql/components/DatabaseListItem.tsx | 11 +- ui/src/ifql/components/MeasurementList.tsx | 131 ++++++++++++++++++ .../ifql/components/MeasurementListItem.tsx | 56 ++++++++ 4 files changed, 217 insertions(+), 16 deletions(-) create mode 100644 ui/src/ifql/components/MeasurementList.tsx create mode 100644 ui/src/ifql/components/MeasurementListItem.tsx diff --git a/ui/src/ifql/components/DatabaseList.tsx b/ui/src/ifql/components/DatabaseList.tsx index 3a5e6e3413..f1b829e42b 100644 --- a/ui/src/ifql/components/DatabaseList.tsx +++ b/ui/src/ifql/components/DatabaseList.tsx @@ -1,7 +1,9 @@ import React, {PureComponent} from 'react' import _ from 'lodash' +import uuid from 'uuid' import DatabaseListItem from 'src/ifql/components/DatabaseListItem' +import MeasurementList from 'src/ifql/components/MeasurementList' import {Source} from 'src/types' @@ -18,6 +20,7 @@ interface DatabaseListProps { interface DatabaseListState { databases: string[] + measurement: string } @ErrorHandling @@ -26,6 +29,7 @@ class DatabaseList extends PureComponent { super(props) this.state = { databases: [], + measurement: '', } } @@ -39,15 +43,7 @@ class DatabaseList extends PureComponent { try { const {data} = await showDatabases(source.links.proxy) const {databases} = showDatabasesParser(data) - const dbs = databases.map(database => { - if (database === '_internal') { - return `${database}.monitor` - } - - return `${database}.autogen` - }) - - const sorted = dbs.sort() + const sorted = databases.sort() this.setState({databases: sorted}) const db = _.get(sorted, '0', '') @@ -65,12 +61,21 @@ class DatabaseList extends PureComponent {
{this.state.databases.map(db => { return ( - + <> + + {this.props.db === db && ( + + )} + ) })}
diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx index 1b2b2c664f..1b78a2fdb4 100644 --- a/ui/src/ifql/components/DatabaseListItem.tsx +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -9,10 +9,19 @@ export interface Props { } class DatabaseListItem extends PureComponent { + constructor(props) { + super(props) + this.state = { + measurement: '', + } + } + public render() { + const {db} = this.props + return (
- {this.props.db} + {db}
) } diff --git a/ui/src/ifql/components/MeasurementList.tsx b/ui/src/ifql/components/MeasurementList.tsx new file mode 100644 index 0000000000..9c56ac7e9e --- /dev/null +++ b/ui/src/ifql/components/MeasurementList.tsx @@ -0,0 +1,131 @@ +import React, {PureComponent} from 'react' +import PropTypes from 'prop-types' + +import {showMeasurements} from 'src/shared/apis/metaQuery' +import showMeasurementsParser from 'src/shared/parsing/showMeasurements' + +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import MeasurementListFilter from 'src/shared/components/MeasurementListFilter' +import MeasurementListItem from 'src/shared/components/MeasurementListItem' +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + onChooseMeasurement: (measurement: string) => void + db: string +} + +interface State { + measurements: string[] + filterText: string + filtered: string[] + selected: string +} + +const {shape} = PropTypes + +@ErrorHandling +class MeasurementList extends PureComponent { + public static contextTypes = { + source: shape({ + links: shape({}).isRequired, + }).isRequired, + } + + constructor(props) { + super(props) + this.state = { + filterText: '', + filtered: [], + measurements: [], + selected: '', + } + } + + public componentDidMount() { + if (!this.props.db) { + return + } + + this.getMeasurements() + } + + public render() { + const {filtered} = this.state + + return ( +
+
+ Measurements & Tags + +
+
+ {filtered.map(measurement => ( + + ))} +
+
+ ) + } + + private handleChooseTag = () => { + console.log('Choose a tag') + } + + private async getMeasurements() { + const {source} = this.context + const {db} = this.props + + try { + const {data} = await showMeasurements(source.links.proxy, db) + const {measurementSets} = showMeasurementsParser(data) + const measurements = measurementSets[0].measurements + + const selected = measurements[0] + this.setState({measurements, filtered: measurements, selected}) + } catch (err) { + console.error(err) + } + } + + private handleChooseMeasurement = (selected: string): void => { + this.setState({selected}) + } + + private handleFilterText = e => { + e.stopPropagation() + const filterText = e.target.value + this.setState({ + filterText, + filtered: this.handleFilterMeasuremet(filterText), + }) + } + + private handleFilterMeasuremet = filter => { + return this.state.measurements.filter(m => + m.toLowerCase().includes(filter.toLowerCase()) + ) + } + + private handleEscape = e => { + if (e.key !== 'Escape') { + return + } + + e.stopPropagation() + this.setState({ + filterText: '', + }) + } +} + +export default MeasurementList diff --git a/ui/src/ifql/components/MeasurementListItem.tsx b/ui/src/ifql/components/MeasurementListItem.tsx new file mode 100644 index 0000000000..654938a76a --- /dev/null +++ b/ui/src/ifql/components/MeasurementListItem.tsx @@ -0,0 +1,56 @@ +import React, {PureComponent} from 'react' +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + selected: string + measurement: string + onChooseTag: () => void + onChooseMeasurement: (measurement: string) => void +} + +interface State { + isOpen: boolean +} + +@ErrorHandling +class MeasurementListItem extends PureComponent { + constructor(props) { + super(props) + + this.state = {isOpen: this.isCurrentMeasurement} + } + + public render() { + const {measurement} = this.props + + return ( +
+
+ +
+ {measurement} + +
+
+ ) + } + + private handleClick = () => { + const {measurement, onChooseMeasurement} = this.props + + if (!this.isCurrentMeasurement) { + this.setState({isOpen: true}, () => { + onChooseMeasurement(measurement) + }) + } else { + this.setState({isOpen: !this.state.isOpen}) + } + } + + private get isCurrentMeasurement(): boolean { + const {selected, measurement} = this.props + return selected === measurement + } +} + +export default MeasurementListItem From 7b50bf1dcb09a3b07e7ee0e090a1f62e718ba744 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 19:51:12 -0700 Subject: [PATCH 039/104] Introduce dummy schema explorer --- ui/src/ifql/components/DatabaseList.tsx | 14 +-- ui/src/ifql/components/DatabaseListItem.tsx | 5 +- ui/src/ifql/components/MeasurementList.tsx | 12 +- .../ifql/components/MeasurementListItem.tsx | 22 ++-- ui/src/ifql/components/SchemaExplorer.tsx | 7 +- ui/src/ifql/components/TagList.tsx | 103 +++++++++++++++ ui/src/ifql/components/TagListItem.tsx | 119 ++++++++++++++++++ ui/src/ifql/components/TimeMachine.tsx | 9 +- 8 files changed, 247 insertions(+), 44 deletions(-) create mode 100644 ui/src/ifql/components/TagList.tsx create mode 100644 ui/src/ifql/components/TagListItem.tsx diff --git a/ui/src/ifql/components/DatabaseList.tsx b/ui/src/ifql/components/DatabaseList.tsx index f1b829e42b..bff9a884c9 100644 --- a/ui/src/ifql/components/DatabaseList.tsx +++ b/ui/src/ifql/components/DatabaseList.tsx @@ -1,6 +1,5 @@ import React, {PureComponent} from 'react' import _ from 'lodash' -import uuid from 'uuid' import DatabaseListItem from 'src/ifql/components/DatabaseListItem' import MeasurementList from 'src/ifql/components/MeasurementList' @@ -61,21 +60,14 @@ class DatabaseList extends PureComponent {
{this.state.databases.map(db => { return ( - <> + - {this.props.db === db && ( - - )} - + {this.props.db === db && } + ) })}
diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx index 1b78a2fdb4..e7e553ef8b 100644 --- a/ui/src/ifql/components/DatabaseListItem.tsx +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -21,7 +21,10 @@ class DatabaseListItem extends PureComponent { return (
- {db} + +
+ {db} +
) } diff --git a/ui/src/ifql/components/MeasurementList.tsx b/ui/src/ifql/components/MeasurementList.tsx index 9c56ac7e9e..14c1a5fec5 100644 --- a/ui/src/ifql/components/MeasurementList.tsx +++ b/ui/src/ifql/components/MeasurementList.tsx @@ -4,13 +4,11 @@ import PropTypes from 'prop-types' import {showMeasurements} from 'src/shared/apis/metaQuery' import showMeasurementsParser from 'src/shared/parsing/showMeasurements' -import FancyScrollbar from 'src/shared/components/FancyScrollbar' import MeasurementListFilter from 'src/shared/components/MeasurementListFilter' -import MeasurementListItem from 'src/shared/components/MeasurementListItem' +import MeasurementListItem from 'src/ifql/components/MeasurementListItem' import {ErrorHandling} from 'src/shared/decorators/errors' interface Props { - onChooseMeasurement: (measurement: string) => void db: string } @@ -62,13 +60,13 @@ class MeasurementList extends PureComponent { filterText={this.state.filterText} />
-
+
{filtered.map(measurement => ( ))} @@ -77,10 +75,6 @@ class MeasurementList extends PureComponent { ) } - private handleChooseTag = () => { - console.log('Choose a tag') - } - private async getMeasurements() { const {source} = this.context const {db} = this.props diff --git a/ui/src/ifql/components/MeasurementListItem.tsx b/ui/src/ifql/components/MeasurementListItem.tsx index 654938a76a..a2f87f5ca0 100644 --- a/ui/src/ifql/components/MeasurementListItem.tsx +++ b/ui/src/ifql/components/MeasurementListItem.tsx @@ -1,10 +1,11 @@ import React, {PureComponent} from 'react' import {ErrorHandling} from 'src/shared/decorators/errors' +import TagList from 'src/ifql/components/TagList' interface Props { + db: string selected: string measurement: string - onChooseTag: () => void onChooseMeasurement: (measurement: string) => void } @@ -17,11 +18,11 @@ class MeasurementListItem extends PureComponent { constructor(props) { super(props) - this.state = {isOpen: this.isCurrentMeasurement} + this.state = {isOpen: false} } public render() { - const {measurement} = this.props + const {measurement, db} = this.props return (
@@ -31,25 +32,18 @@ class MeasurementListItem extends PureComponent { {measurement}
+ {this.shouldShow && }
) } private handleClick = () => { const {measurement, onChooseMeasurement} = this.props - - if (!this.isCurrentMeasurement) { - this.setState({isOpen: true}, () => { - onChooseMeasurement(measurement) - }) - } else { - this.setState({isOpen: !this.state.isOpen}) - } + onChooseMeasurement(measurement) } - private get isCurrentMeasurement(): boolean { - const {selected, measurement} = this.props - return selected === measurement + private get shouldShow(): boolean { + return this.state.isOpen } } diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx index ec187ccf8a..600ae85142 100644 --- a/ui/src/ifql/components/SchemaExplorer.tsx +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -1,11 +1,6 @@ import React, {PureComponent} from 'react' import PropTypes from 'prop-types' import DatabaseList from 'src/ifql/components/DatabaseList' -import {Source} from 'src/types' - -interface Props { - source: Source -} interface State { db: string @@ -13,7 +8,7 @@ interface State { const {shape} = PropTypes -class SchemaExplorer extends PureComponent { +class SchemaExplorer extends PureComponent<{}, State> { public static contextTypes = { source: shape({ links: shape({}).isRequired, diff --git a/ui/src/ifql/components/TagList.tsx b/ui/src/ifql/components/TagList.tsx new file mode 100644 index 0000000000..c996b0c93b --- /dev/null +++ b/ui/src/ifql/components/TagList.tsx @@ -0,0 +1,103 @@ +import PropTypes from 'prop-types' +import React, {PureComponent} from 'react' + +import _ from 'lodash' + +import TagListItem from 'src/ifql/components/TagListItem' + +import {showTagKeys, showTagValues} from 'src/shared/apis/metaQuery' +import showTagKeysParser from 'src/shared/parsing/showTagKeys' +import showTagValuesParser from 'src/shared/parsing/showTagValues' +import {ErrorHandling} from 'src/shared/decorators/errors' + +const {shape} = PropTypes + +interface Props { + db: string + measurement: string +} + +interface State { + tags: {} + selectedTag: string +} + +@ErrorHandling +class TagList extends PureComponent { + public static contextTypes = { + source: shape({ + links: shape({}).isRequired, + }).isRequired, + } + + constructor(props) { + super(props) + this.state = { + tags: {}, + selectedTag: '', + } + } + + public componentDidMount() { + const {db, measurement} = this.props + if (!db || !measurement) { + return + } + + this.getTags() + } + + public componentDidUpdate(prevProps) { + const {db, measurement} = this.props + + const {db: prevDB, measurement: prevMeas} = prevProps + + if (!db || !measurement) { + return + } + + if (db === prevDB && measurement === prevMeas) { + return + } + + this.getTags() + } + + public async getTags() { + const {db, measurement} = this.props + const {source} = this.context + + const {data} = await showTagKeys({ + database: db, + measurement, + retentionPolicy: 'autogen', + source: source.links.proxy, + }) + const {tagKeys} = showTagKeysParser(data) + + const response = await showTagValues({ + database: db, + measurement, + retentionPolicy: 'autogen', + source: source.links.proxy, + tagKeys, + }) + + const {tags} = showTagValuesParser(response.data) + + const selected = Object.keys(tags) + this.setState({tags, selectedTag: selected[0]}) + } + + public render() { + return ( +
+ {_.map(this.state.tags, (tagValues: string[], tagKey: string) => ( + + ))} +
+ ) + } +} + +export default TagList diff --git a/ui/src/ifql/components/TagListItem.tsx b/ui/src/ifql/components/TagListItem.tsx new file mode 100644 index 0000000000..7a235c7567 --- /dev/null +++ b/ui/src/ifql/components/TagListItem.tsx @@ -0,0 +1,119 @@ +import classnames from 'classnames' +import React, {PureComponent, MouseEvent} from 'react' +import {ErrorHandling} from 'src/shared/decorators/errors' + +interface Props { + tagKey: string + tagValues: string[] +} + +interface State { + isOpen: boolean + filterText: string +} + +@ErrorHandling +class TagListItem extends PureComponent { + constructor(props) { + super(props) + this.state = { + filterText: '', + isOpen: false, + } + + this.handleEscape = this.handleEscape.bind(this) + this.handleClickKey = this.handleClickKey.bind(this) + this.handleFilterText = this.handleFilterText.bind(this) + } + + public handleClickKey(e: MouseEvent) { + e.stopPropagation() + this.setState({isOpen: !this.state.isOpen}) + } + + public handleFilterText(e) { + e.stopPropagation() + this.setState({ + filterText: e.target.value, + }) + } + + public handleEscape(e) { + if (e.key !== 'Escape') { + return + } + + e.stopPropagation() + this.setState({ + filterText: '', + }) + } + + public handleInputClick(e: MouseEvent) { + e.stopPropagation() + } + + public renderTagValues() { + const {tagValues} = this.props + if (!tagValues || !tagValues.length) { + return
no tag values
+ } + + const filterText = this.state.filterText.toLowerCase() + const filtered = tagValues.filter(v => v.toLowerCase().includes(filterText)) + + return ( +
+
+ + +
+ {filtered.map(v => { + return ( +
+ {v} +
+ ) + })} +
+ ) + } + + public render() { + const {tagKey, tagValues} = this.props + const {isOpen} = this.state + const tagItemLabel = `${tagKey} — ${tagValues.length}` + + return ( +
+
+ +
+ {tagItemLabel} + +
+ {isOpen ? this.renderTagValues() : null} +
+ ) + } +} + +export default TagListItem diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 2ca53c887f..35494ed52d 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -38,7 +38,10 @@ class TimeMachine extends PureComponent { private get renderRightSide() { return ( - + ) } @@ -52,8 +55,8 @@ class TimeMachine extends PureComponent { { name: 'Visualize', render: () => , - }, + ] } private get renderEditor() { @@ -73,7 +76,7 @@ class TimeMachine extends PureComponent { }, { name: 'Explore', - render: () => (), + render: () => , }, ] } From 13b5aeb79e2da30729831e0fbbad1af99e925259 Mon Sep 17 00:00:00 2001 From: Alex P Date: Wed, 2 May 2018 20:17:08 -0700 Subject: [PATCH 040/104] Add "Fn" icon --- ui/src/ifql/components/FuncSelector.tsx | 2 +- ui/src/style/fonts/icomoon.eot | Bin 13400 -> 13720 bytes ui/src/style/fonts/icomoon.svg | 3 ++- ui/src/style/fonts/icomoon.ttf | Bin 13236 -> 13556 bytes ui/src/style/fonts/icomoon.woff | Bin 13312 -> 13632 bytes ui/src/style/fonts/icomoon.woff2 | Bin 6588 -> 6808 bytes ui/src/style/fonts/icon-font.scss | 3 ++- 7 files changed, 5 insertions(+), 3 deletions(-) diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index e0986d77c8..35dfe9ea9f 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -55,7 +55,7 @@ export class FuncSelector extends PureComponent { onClick={this.handleOpenList} tabIndex={0} > - + )}
diff --git a/ui/src/style/fonts/icomoon.eot b/ui/src/style/fonts/icomoon.eot index 538f905f782689c6cdaf573f59f6ababe0eead95..233e332b9ca7f272ac033ab87b2d1a8ef7c8d60e 100755 GIT binary patch delta 676 zcmXw%OGs2<6vxl^-I@ECJ2Us*nYrIp@8n~gxuec7Id5@9MJPndLdK2sU{GObGOY}R z7DkJ(2w}V%bJc?ufs`;1LQm~!Kp+{E~eL7_tV z5CFO|HF*^dB#vDcS$Zc+;LWt2Y`9pTeZ}NkDBVWlE@|k=jTcoV6s8?lK zc1l~)JLv&6KxPq#&6`*#J;7~u-$2D+p5qP3K!K*jQpto7Lg{XlP7$rX+5}C`9-OsC zP_vKaA}DCE66!!vjwKpN^`K-oipQ#}5~4~c1uCWt%mYM6>Z|6 zc-vufWF2#kva`>*Dvii~c~^O%?6_K7%kDP!Wly_jNhRu>`ktI4^W+)X(#EtUpl<;^ SPyt&t4=}xEt^ zO-vEA|0K)6U?9Q3z+jdE6ku0ijsx-ofP9sV+>(k{9|Ssq{1PDlM^1inqLZmv5(9%_ z4v=4$n^;l6;K|4c$}kvD0m(BkGfz%t z>|+#~{E0DJn@OBe?1$KQv9Dt9#NLR#5PL56Ozer+W3l_9>qR>R_yu_RAM-!pUk5aR zaq>haoyiNBI5+QQ(&E{yEO1v*rt9*7(s+KGuMFHQAPX3-3#i1x=*coh=8SfeLyVFq z?>0ISTgP^Q?G5`14hfD9jxU^BxRkhZxHfRJ@Z|7p<9WsF#Jhw~i*FBqn?RJHnqZh< zmk^&&kI)n0Ny2|bDnwq1x`<`cepl=!M82&SGfox-70y<52^Jn9w Fi~z}KZ5{vs diff --git a/ui/src/style/fonts/icomoon.svg b/ui/src/style/fonts/icomoon.svg index b8e1c4b5d8..7c65529bfe 100755 --- a/ui/src/style/fonts/icomoon.svg +++ b/ui/src/style/fonts/icomoon.svg @@ -23,10 +23,11 @@ - + + diff --git a/ui/src/style/fonts/icomoon.ttf b/ui/src/style/fonts/icomoon.ttf index dead09541e9cbd67ded91d0fb458ed1e17f75ff4..889175549ab9b43746c65f7237865fe62c84d381 100755 GIT binary patch delta 661 zcmXw%OGs2v9LE3W%*=gyUo+lwNAKigoVnv;nC3m=h)Ph%P*aF*q>^DYEj1+#j21?V zu!ty!7P%~11X6+#MVqJ=fi1)YH!Y^J*P<6#7>LuEMXT@pzW?F-{v6K3;jNR`>t{oK zPXLey0Ct9RQrUs!hupWae2E$d=@OSF zC#I&sMn@1|zw!7cauu>@G+;9@`AfFRAM%5IBVWlE@|k=jo1~~O>6cYim6c89o$_G0 zi1HQQOv$pnz-y3(QJNS{CgN5LN_C)Ak{C7RL!gis1GA10YVa~_2n8%gL9HmlGQ=Rs zPL$|Cv1s|Kf~eC*ftpDRbEe;KdL_XtFmA~i({gg0Tf_YV2%Uw1Au&$jNFonS04mAf&3C6|3^-Ka-x%|SrP+-VGfXA zmz!8oz~ITq2;{#3@)hzDb5kW|`f>heU@+nVI-;#0zqo{f87ReIJZ0jJsf= znZy~zeu#Y+`zrQM?2Xt9vFBpX#GZ&f7P~LHUbI7iUx1hYG5-Vpb(l meh^a;a}zrz_6KMK&}$5K4F4IpK(;Y3P5#IzyxHG)DI)+?Yi8L1 diff --git a/ui/src/style/fonts/icomoon.woff b/ui/src/style/fonts/icomoon.woff index 6deea0a9790b25b65e8a995f0bea13d7f34a65fa..17d2da624931a8b5f102f318b2bc8af227185f6d 100755 GIT binary patch delta 705 zcmXw%Pe@cj9LHy7-@d=Tf4lqUsn2v>_q|nj+1&r)$|^#OETmz2kw_Lb7yqQBk7N6RmD zXR`nTG7{}Tuxosmc&Zpdp2d(k7w&sHlAoRe01c5f_MeoI@yq838wcQc+}5#+0$Ui) zlT73;$s}#5?chOyK*HV-@3+Nx9Rr1lnOVa2i9gc!pUUg;$w88f?h!U`>%_BD2NU_( zsSp4$5(eHQ92^TxjK$6m*fQ2Xtk!(Vim5rOUCS4SGDOW;Y=>XMEaOBLU2RZKO0T0EoE{i64Z}<)BM8YZ(nyVK;ea5G_Vw8O;B6v` zkfM^k)tDA&K$JMv3)Ag2wL%#yAkzB76(dBE`pRLpa2#JLZ;(x^^o{x+fI#Tb>G*`U zfCV~NO44R+Re$UD`heW}3kVc>834AF;Qjx#xX_e>ZN=55L6|FgBkQLhF;&c2<_4Q( zpKunp#P{&40xxt4_e5Uo6&J*9sY7}x_bYy7Pko{8y6RmI-Hq<6o+eL8!`g!O9$&yW T@fzOIhxF3XXKh=_MAzV77zUQ& delta 408 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 u$a2IN(?xC#1 Date: Wed, 2 May 2018 21:04:15 -0700 Subject: [PATCH 041/104] Introduce double click to resize --- ui/src/shared/components/ResizeDivision.tsx | 19 +++++++++++-------- ui/src/shared/components/Threesizer.tsx | 11 ++++++----- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index b6ccdd199f..861cd0cb83 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -29,8 +29,9 @@ class Division extends PureComponent { <>
{name} @@ -58,18 +59,20 @@ class Division extends PureComponent { }) } - private handleDoubleClick = () => { - const {id, onDoubleClick} = this.props - return onDoubleClick(id) - } - - private dragCallback = e => { + private drag = e => { const {draggable, id} = this.props + if (!draggable) { return NOOP } - return this.props.onHandleStartDrag(id, e) + this.props.onHandleStartDrag(id, e) + } + + private handleDoubleClick = () => { + const {onDoubleClick, id} = this.props + + onDoubleClick(id) } } diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 7bb791612b..b7a4cf0fd3 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -163,10 +163,10 @@ class Threesizer extends Component { return } - const isFullSized = clickedDiv.size === 1 + const isMaxed = clickedDiv.size === 1 - if (isFullSized) { - return this.expandAll() + if (isMaxed) { + this.equalize() } const divisions = this.state.divisions.map(d => { @@ -180,9 +180,10 @@ class Threesizer extends Component { this.setState({divisions}) } - private expandAll = () => { + private equalize = () => { const divisions = this.state.divisions.map(d => { - return {...d, size: 1 / this.state.divisions.length} + const denominator = this.state.divisions.length + return {...d, size: 1 / denominator} }) this.setState({divisions}) From de71d6e73f8b98229edae5eea07badb9ffe6aed5 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Wed, 2 May 2018 23:42:42 -0700 Subject: [PATCH 042/104] Add transitions for doubleclick to resize --- ui/src/shared/components/ResizeDivision.tsx | 26 ++++++++++++++++++--- ui/src/shared/components/Threesizer.tsx | 8 ++----- ui/src/style/components/threesizer.scss | 4 ++++ 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 861cd0cb83..caacd50fbd 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -27,7 +27,11 @@ class Division extends PureComponent { const {name, render} = this.props return ( <> -
+
{ ) } + private get title() { + return 'Drag to resize.\nDouble click to expand.' + } + private get containerStyle() { return { height: `calc((100% - 90px) * ${this.props.size} + 30px)`, } } + private get containerClass(): string { + const isAnyHandleBeingDragged = !!this.props.activeHandleID + return classnames('threesizer--division', { + dragging: isAnyHandleBeingDragged, + }) + } + private get className(): string { - const {draggable, id, activeHandleID} = this.props + const {draggable} = this.props return classnames('threesizer--handle', { disabled: !draggable, - dragging: id === activeHandleID, + dragging: this.isDragging, }) } + private get isDragging(): boolean { + const {id, activeHandleID} = this.props + return id === activeHandleID + } + private drag = e => { const {draggable, id} = this.props diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index b7a4cf0fd3..cd3694fb99 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -77,10 +77,6 @@ class Threesizer extends Component { dragEvent.mouseY ) - if (!this.state.activeHandleID) { - return - } - if (orientation === HANDLE_VERTICAL) { const left = dragEvent.percentX < prevState.dragEvent.percentX @@ -166,7 +162,7 @@ class Threesizer extends Component { const isMaxed = clickedDiv.size === 1 if (isMaxed) { - this.equalize() + return this.equalize() } const divisions = this.state.divisions.map(d => { @@ -181,8 +177,8 @@ class Threesizer extends Component { } private equalize = () => { + const denominator = this.state.divisions.length const divisions = this.state.divisions.map(d => { - const denominator = this.state.divisions.length return {...d, size: 1 / denominator} }) diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 1dc9bac23f..35e5719676 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -24,6 +24,10 @@ $threesizer-contents-z: 1; .threesizer--division { position: relative; overflow: hidden; + transition: all 0.25s ease-in-out; + &.dragging { + transition: none; + } } /* Draggable Handle With Title */ From 6bd58d41a6fcce4a4ff7a4c43f0572bd4b656d8d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 09:51:19 -0700 Subject: [PATCH 043/104] Fix title --- ui/src/shared/components/ResizeDivision.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index caacd50fbd..c8c60563fe 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -27,16 +27,13 @@ class Division extends PureComponent { const {name, render} = this.props return ( <> -
+
{name}
From b376f755b1890976969992e531df518fad17ac54 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 09:51:38 -0700 Subject: [PATCH 044/104] Narrow transition animation scope to height and width --- ui/src/style/components/threesizer.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 35e5719676..2100b251ae 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -24,7 +24,7 @@ $threesizer-contents-z: 1; .threesizer--division { position: relative; overflow: hidden; - transition: all 0.25s ease-in-out; + transition: height 0.25s ease-in-out, width 0.25s ease-in-out; &.dragging { transition: none; } From 5ce2cd9623013c096574f3e9dd1841f24de182f1 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 10:39:16 -0700 Subject: [PATCH 045/104] Calc offset based on number of handles --- ui/src/ifql/components/TimeMachine.tsx | 4 ++++ ui/src/shared/components/ResizeDivision.tsx | 5 ++++- ui/src/shared/components/Threesizer.tsx | 11 ++++++++++- ui/src/shared/constants/index.tsx | 2 ++ 4 files changed, 20 insertions(+), 2 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 35494ed52d..9babac677e 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -78,6 +78,10 @@ class TimeMachine extends PureComponent { name: 'Explore', render: () => , }, + { + name: 'Test', + render: () =>
im a test
, + }, ] } } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index c8c60563fe..8935ba8916 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -10,6 +10,7 @@ interface Props { name?: string minPixels: number size: number + offset: number activeHandleID: string draggable: boolean orientation: string @@ -50,8 +51,10 @@ class Division extends PureComponent { } private get containerStyle() { + const {size, offset} = this.props + return { - height: `calc((100% - 90px) * ${this.props.size} + 30px)`, + height: `calc((100% - ${offset}px) * ${size} + 30px)`, } } diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index cd3694fb99..e67ab79986 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -5,7 +5,11 @@ import _ from 'lodash' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' -import {HANDLE_HORIZONTAL, HANDLE_VERTICAL} from 'src/shared/constants/' +import { + HANDLE_PIXELS, + HANDLE_HORIZONTAL, + HANDLE_VERTICAL, +} from 'src/shared/constants/' const initialDragEvent = { percentX: 0, @@ -115,6 +119,7 @@ class Threesizer extends Component { id={d.id} name={d.name} size={d.size} + offset={this.offset} draggable={i > 0} minPixels={d.minPixels} orientation={orientation} @@ -128,6 +133,10 @@ class Threesizer extends Component { ) } + private get offset(): number { + return HANDLE_PIXELS * this.state.divisions.length + } + private get className(): string { const {orientation} = this.props const {activeHandleID} = this.state diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index 46fc9322b9..a681f1c0d6 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -478,7 +478,9 @@ export const FIVE_SECONDS = 5000 export const TEN_SECONDS = 10000 export const INFINITE = -1 +// Resizer && Threesizer export const HUNDRED = 100 export const REQUIRED_HALVES = 2 export const HANDLE_VERTICAL = 'vertical' export const HANDLE_HORIZONTAL = 'horizontal' +export const HANDLE_PIXELS = 30 From 84fba91b10555f1e5c10121c32ded387c4fa6def Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 3 May 2018 10:57:27 -0700 Subject: [PATCH 046/104] Organize time machine styles --- ui/src/style/chronograf.scss | 6 +-- .../add-func-button.scss} | 0 .../ifql-builder.scss} | 0 .../ifql-editor.scss} | 5 ++ .../time-machine/visualization.scss | 48 +++++++++++++++++++ ui/src/style/pages/time-machine.scss | 9 ++++ 6 files changed, 63 insertions(+), 5 deletions(-) rename ui/src/style/components/{funcs-button.scss => time-machine/add-func-button.scss} (100%) rename ui/src/style/components/{func-node.scss => time-machine/ifql-builder.scss} (100%) rename ui/src/style/components/{time-machine.scss => time-machine/ifql-editor.scss} (67%) create mode 100644 ui/src/style/components/time-machine/visualization.scss create mode 100644 ui/src/style/pages/time-machine.scss diff --git a/ui/src/style/chronograf.scss b/ui/src/style/chronograf.scss index d41eb59e87..1e8206a350 100644 --- a/ui/src/style/chronograf.scss +++ b/ui/src/style/chronograf.scss @@ -71,7 +71,6 @@ @import 'components/threesizer'; @import 'components/threshold-controls'; @import 'components/kapacitor-logs-table'; -@import 'components/func-node.scss'; // Pages @import 'pages/config-endpoints'; @@ -82,11 +81,8 @@ @import 'pages/admin'; @import 'pages/users'; @import 'pages/tickscript-editor'; +@import 'pages/time-machine'; @import 'pages/manage-providers'; // TODO @import 'unsorted'; - -// IFQL - Time Machine -@import 'components/funcs-button'; -@import 'components/time-machine'; diff --git a/ui/src/style/components/funcs-button.scss b/ui/src/style/components/time-machine/add-func-button.scss similarity index 100% rename from ui/src/style/components/funcs-button.scss rename to ui/src/style/components/time-machine/add-func-button.scss diff --git a/ui/src/style/components/func-node.scss b/ui/src/style/components/time-machine/ifql-builder.scss similarity index 100% rename from ui/src/style/components/func-node.scss rename to ui/src/style/components/time-machine/ifql-builder.scss diff --git a/ui/src/style/components/time-machine.scss b/ui/src/style/components/time-machine/ifql-editor.scss similarity index 67% rename from ui/src/style/components/time-machine.scss rename to ui/src/style/components/time-machine/ifql-editor.scss index b3acd83576..843646300f 100644 --- a/ui/src/style/components/time-machine.scss +++ b/ui/src/style/components/time-machine/ifql-editor.scss @@ -1,3 +1,8 @@ +/* + IFQL Code Mirror Editor + ---------------------------------------------------------------------------- +*/ + .time-machine-container { display: flex; height: 90%; diff --git a/ui/src/style/components/time-machine/visualization.scss b/ui/src/style/components/time-machine/visualization.scss new file mode 100644 index 0000000000..122d54e10f --- /dev/null +++ b/ui/src/style/components/time-machine/visualization.scss @@ -0,0 +1,48 @@ +/* + Time Machine Visualization + ---------------------------------------------------------------------------- +*/ + +.time-machine-visualization { + display: flex; + align-content: center; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; + @include gradient-v($g2-kevlar, $g0-obsidian); +} + +.time-machine--graph { + width: calc(100% - 60px); + height: calc(100% - 60px); + background-color: $g3-castle; + border-radius: $radius; + display: flex; + flex-direction: column; + align-items: stretch; + flex-wrap: nowrap; +} + +.time-machine--graph-header { + height: 56px; + padding: 0 16px; + display: flex; + align-items: center; + justify-content: center +} + +.time-machine--graph-header .nav.nav-tablist { + width: 180px; + + li { + justify-content: center; + flex: 1 0 0; + white-space: nowrap; + } +} + +.time-machine--graph-body { + padding: 0 16px 8px 16px; + flex: 1 0 0; +} \ No newline at end of file diff --git a/ui/src/style/pages/time-machine.scss b/ui/src/style/pages/time-machine.scss new file mode 100644 index 0000000000..31447dca77 --- /dev/null +++ b/ui/src/style/pages/time-machine.scss @@ -0,0 +1,9 @@ +/* + Styles for IFQL Builder aka TIME MACHINE aka DELOREAN + ---------------------------------------------------------------------------- +*/ + +@import '../components/time-machine/ifql-editor'; +@import '../components/time-machine/ifql-builder'; +@import '../components/time-machine/visualization'; +@import '../components/time-machine/add-func-button'; From 586bab690195456d72e902920bb16890aef651e7 Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 3 May 2018 10:57:45 -0700 Subject: [PATCH 047/104] Add dummy visualizer --- ui/src/ifql/components/TimeMachine.tsx | 2 +- ui/src/ifql/components/TimeMachineVis.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 9babac677e..4da8e8d5d8 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -54,7 +54,7 @@ class TimeMachine extends PureComponent { }, { name: 'Visualize', - render: () => , + render: () => , }, ] } diff --git a/ui/src/ifql/components/TimeMachineVis.tsx b/ui/src/ifql/components/TimeMachineVis.tsx index fe73eeeba8..f5bcc985a2 100644 --- a/ui/src/ifql/components/TimeMachineVis.tsx +++ b/ui/src/ifql/components/TimeMachineVis.tsx @@ -3,6 +3,18 @@ import React, {SFC} from 'react' interface Props { blob: string } -const TimeMachineVis: SFC = ({blob}) =>
{blob}
+const TimeMachineVis: SFC = ({blob}) => ( +
+
+
+
    +
  • Line Graph
  • +
  • Table
  • +
+
+
{`I AM A ${blob} GRAPH`}
+
+
+) export default TimeMachineVis From 162b26807dd19f727714d623e23adb984357c7f4 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 11:35:19 -0700 Subject: [PATCH 048/104] Style vertical position --- ui/src/ifql/components/TimeMachine.tsx | 6 +- ui/src/shared/components/ResizeDivision.tsx | 27 ++++++-- ui/src/style/components/threesizer.scss | 68 ++++++++++++++------- 3 files changed, 71 insertions(+), 30 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 4da8e8d5d8..80b77e0d67 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -27,7 +27,7 @@ class TimeMachine extends PureComponent { {this.renderEditor} @@ -40,7 +40,7 @@ class TimeMachine extends PureComponent { return ( ) } @@ -61,7 +61,7 @@ class TimeMachine extends PureComponent { private get renderEditor() { return ( - + ) } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 8935ba8916..6c24e972ee 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -2,6 +2,7 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react' import classnames from 'classnames' import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' const NOOP = () => {} @@ -25,7 +26,7 @@ class Division extends PureComponent { } public render() { - const {name, render} = this.props + const {name, render, orientation} = this.props return ( <>
@@ -36,9 +37,12 @@ class Division extends PureComponent { onDoubleClick={this.handleDoubleClick} title={this.title} > - {name} +
{name}
- + {render()}
@@ -51,13 +55,22 @@ class Division extends PureComponent { } private get containerStyle() { - const {size, offset} = this.props + if (this.props.orientation === HANDLE_HORIZONTAL) { + return { + height: this.size, + } + } return { - height: `calc((100% - ${offset}px) * ${size} + 30px)`, + width: this.size, } } + private get size(): string { + const {size, offset} = this.props + return `calc((100% - ${offset}px) * ${size} + 30px)` + } + private get containerClass(): string { const isAnyHandleBeingDragged = !!this.props.activeHandleID return classnames('threesizer--division', { @@ -66,11 +79,13 @@ class Division extends PureComponent { } private get className(): string { - const {draggable} = this.props + const {draggable, orientation} = this.props return classnames('threesizer--handle', { disabled: !draggable, dragging: this.isDragging, + vertical: orientation === HANDLE_VERTICAL, + horizonatl: orientation === HANDLE_HORIZONTAL, }) } diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 2100b251ae..cdb4a7f06a 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -4,49 +4,63 @@ */ $threesizer-handle: 30px; -$threesizer-handle-z: 2; -$threesizer-contents-z: 1; .threesizer { - position: relative; width: 100%; height: 100%; display: flex; - flex-direction: column; align-items: stretch; &.dragging .threesizer--division { @include no-user-select(); pointer-events: none; } + + &.vertical { + flex-direction: row; + } + + &.horizontal { + flex-direction: column; + } } .threesizer--division { - position: relative; overflow: hidden; + display: flex; + align-items: stretch; + transition: height 0.25s ease-in-out, width 0.25s ease-in-out; + &.dragging { transition: none; } + + .vertical & { + flex-direction: column; + } + + .horizontal & { + flex-direction: row; + } } /* Draggable Handle With Title */ .threesizer--handle { - position: relative; - z-index: $threesizer-handle-z; @include no-user-select(); - height: $threesizer-handle; background-color: $g4-onyx; - padding: 0 12px; - line-height: $threesizer-handle; - font-size: 13px; - font-weight: 500; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - color: $g11-sidewalk; transition: background-color 0.25s ease, color 0.25s ease; + &.vertical { + width: $threesizer-handle; + padding: 12px 0; + } + + &.horizontal { + height: $threesizer-handle; + padding: 0 12px; + } + &:hover { cursor: row-resize; color: $g16-pearl; @@ -60,11 +74,23 @@ $threesizer-contents-z: 1; } } +.threesizer--title { + font-size: 13px; + font-weight: 500; + white-space: nowrap; + color: $g11-sidewalk; + + .vertical & { + transform: rotate(90deg) translateX(8px); + } +} + /* Division Contents */ .threesizer--contents { - position: absolute !important; - top: $threesizer-handle; - z-index: $threesizer-contents-z; - width: 100%; - height: calc(100% - #{$threesizer-handle}) !important; + &.horizontal { + height: calc(100% - #{$threesizer-handle}) !important; + } + &.vertical { + width: calc(100% - #{$threesizer-handle}) !important; + } } From 019e5e643af234960b028d9254b8583cfe241022 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 11:56:37 -0700 Subject: [PATCH 049/104] One threesizer to rule them all --- ui/src/ifql/components/TimeMachine.tsx | 46 +++++++-------------- ui/src/shared/components/ResizeDivision.tsx | 5 ++- ui/src/shared/components/Threesizer.tsx | 4 +- ui/src/style/components/threesizer.scss | 8 ++-- 4 files changed, 24 insertions(+), 39 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 80b77e0d67..fc658dd309 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -3,7 +3,6 @@ import SchemaExplorer from 'src/ifql/components/SchemaExplorer' import BodyBuilder from 'src/ifql/components/BodyBuilder' import TimeMachineEditor from 'src/ifql/components/TimeMachineEditor' import TimeMachineVis from 'src/ifql/components/TimeMachineVis' -import Resizer from 'src/shared/components/ResizeContainer' import Threesizer from 'src/shared/components/Threesizer' import {Suggestion, OnChangeScript, FlatBody} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' @@ -23,50 +22,33 @@ interface Body extends FlatBody { @ErrorHandling class TimeMachine extends PureComponent { public render() { - return ( - - {this.renderEditor} - {this.renderRightSide} - - ) - } - - private get renderRightSide() { return ( ) } - private get visPlusBuilder() { - const {body, suggestions} = this.props + private get mainSplit() { return [ { - name: 'Build', - render: () => , + render: () => ( + + ), }, { - name: 'Visualize', - render: () => , + render: () => , }, ] } - private get renderEditor() { - return ( - - ) - } - private get divisions() { - const {script, onChangeScript} = this.props + const {body, suggestions, script, onChangeScript} = this.props return [ { name: 'Script', @@ -79,8 +61,8 @@ class TimeMachine extends PureComponent { render: () => , }, { - name: 'Test', - render: () =>
im a test
, + name: 'Build', + render: () => , }, ] } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 6c24e972ee..2efbfef5fd 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -72,9 +72,12 @@ class Division extends PureComponent { } private get containerClass(): string { + const {orientation} = this.props const isAnyHandleBeingDragged = !!this.props.activeHandleID return classnames('threesizer--division', { dragging: isAnyHandleBeingDragged, + vertical: orientation === HANDLE_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, }) } @@ -85,7 +88,7 @@ class Division extends PureComponent { disabled: !draggable, dragging: this.isDragging, vertical: orientation === HANDLE_VERTICAL, - horizonatl: orientation === HANDLE_HORIZONTAL, + horizontal: orientation === HANDLE_HORIZONTAL, }) } diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index e67ab79986..1eefaecab6 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -138,10 +138,10 @@ class Threesizer extends Component { } private get className(): string { - const {orientation} = this.props + const {orientation, containerClass} = this.props const {activeHandleID} = this.state - return classnames(`threesizer`, { + return classnames(`threesizer ${containerClass}`, { dragging: activeHandleID, horizontal: orientation === HANDLE_HORIZONTAL, vertical: orientation === HANDLE_VERTICAL, diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index cdb4a7f06a..364c13b96c 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -36,12 +36,12 @@ $threesizer-handle: 30px; transition: none; } - .vertical & { - flex-direction: column; + &.vertical { + flex-direction: row; } - .horizontal & { - flex-direction: row; + &.horizontal { + flex-direction: column; } } From 30eab8ccfb91c22aff07b5b754ffbd4c15b99b01 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 14:34:59 -0700 Subject: [PATCH 050/104] Refine horizontal dragging logic --- ui/src/ifql/components/TimeMachineVis.tsx | 8 +- ui/src/shared/components/Threesizer.tsx | 142 +++++++++++++++------- ui/src/shared/constants/index.tsx | 2 + ui/src/style/components/threesizer.scss | 14 ++- 4 files changed, 112 insertions(+), 54 deletions(-) diff --git a/ui/src/ifql/components/TimeMachineVis.tsx b/ui/src/ifql/components/TimeMachineVis.tsx index f5bcc985a2..53214b72a0 100644 --- a/ui/src/ifql/components/TimeMachineVis.tsx +++ b/ui/src/ifql/components/TimeMachineVis.tsx @@ -6,13 +6,7 @@ interface Props { const TimeMachineVis: SFC = ({blob}) => (
-
-
    -
  • Line Graph
  • -
  • Table
  • -
-
-
{`I AM A ${blob} GRAPH`}
+
{blob}
) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 1eefaecab6..576c693e81 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -9,6 +9,8 @@ import { HANDLE_PIXELS, HANDLE_HORIZONTAL, HANDLE_VERTICAL, + MIN_SIZE, + MAX_SIZE, } from 'src/shared/constants/' const initialDragEvent = { @@ -81,8 +83,11 @@ class Threesizer extends Component { dragEvent.mouseY ) + const {percentX, percentY} = dragEvent + const {dragEvent: prevDrag} = prevState + if (orientation === HANDLE_VERTICAL) { - const left = dragEvent.percentX < prevState.dragEvent.percentX + const left = percentX < prevDrag.percentX if (left) { return this.move.left() @@ -91,14 +96,13 @@ class Threesizer extends Component { return this.move.right() } - const up = dragEvent.percentY < prevState.dragEvent.percentY - const down = dragEvent.percentY > prevState.dragEvent.percentY + const up = percentY < prevDrag.percentY if (up) { return this.move.up() - } else if (down) { - return this.move.down() } + + return this.move.down() } public render() { @@ -226,18 +230,18 @@ class Threesizer extends Component { } private pixelsToPercentX = (startValue, endValue) => { - if (!startValue) { + if (!startValue || !endValue) { return 0 } - const delta = startValue - endValue + const delta = Math.abs(startValue - endValue) const {width} = this.containerRef.getBoundingClientRect() - return Math.abs(delta / width) + return delta / width } private pixelsToPercentY = (startValue, endValue) => { - if (!startValue) { + if (!startValue || !endValue) { return 0 } @@ -306,6 +310,70 @@ class Threesizer extends Component { this.setState({divisions}) } + private left = activePosition => () => { + const divisions = this.state.divisions.map((d, i) => { + if (!activePosition) { + return d + } + + const first = i === 0 + const before = i === activePosition - 1 + const active = i === activePosition + + if (first && !before) { + const second = this.state.divisions[1] + if (second.size === 0) { + return {...d, size: this.thinner(d.size)} + } + + return {...d} + } + + if (before) { + return {...d, size: this.thinner(d.size)} + } + + if (active) { + return {...d, size: this.fatter(d.size)} + } + + return {...d} + }) + + this.setState({divisions}) + } + + private right = activePosition => () => { + const divisions = this.state.divisions.map((d, i, divs) => { + const before = i === activePosition - 1 + const active = i === activePosition + const after = i === activePosition + 1 + + if (before) { + return {...d, size: this.fatter(d.size)} + } + + if (active) { + return {...d, size: this.thinner(d.size)} + } + + if (after) { + const leftIndex = i - 1 + const left = _.get(divs, leftIndex, {size: 'none'}) + + if (left.size === 0) { + return {...d, size: this.thinner(d.size)} + } + + return {...d} + } + + return {...d} + }) + + this.setState({divisions}) + } + private down = activePosition => () => { const divisions = this.state.divisions.map((d, i, divs) => { const before = i === activePosition - 1 @@ -335,48 +403,32 @@ class Threesizer extends Component { this.setState({divisions}) } - private left = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size - this.percentChangeX} - } else if (active) { - return {...d, size: d.size + this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - - private right = activePosition => () => { - const divisions = this.state.divisions.map((d, i) => { - const before = i === activePosition - 1 - const active = i === activePosition - - if (before) { - return {...d, size: d.size + this.percentChangeX} - } else if (active) { - return {...d, size: d.size - this.percentChangeX} - } - - return d - }) - - this.setState({divisions}) - } - private taller = (size: number): number => { const newSize = size + this.percentChangeY - return newSize > 1 ? 1 : newSize + return this.enforceMax(newSize) + } + + private fatter = (size: number): number => { + const newSize = size + this.percentChangeX + return this.enforceMax(newSize) } private shorter = (size: number): number => { const newSize = size - this.percentChangeY - return newSize < 0 ? 0 : newSize + return this.enforceMin(newSize) + } + + private thinner = (size: number): number => { + const newSize = size - this.percentChangeX + return this.enforceMin(newSize) + } + + private enforceMax = (size: number): number => { + return size > MAX_SIZE ? MAX_SIZE : size + } + + private enforceMin = (size: number): number => { + return size < MIN_SIZE ? MIN_SIZE : size } } diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index a681f1c0d6..523db3990b 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -484,3 +484,5 @@ export const REQUIRED_HALVES = 2 export const HANDLE_VERTICAL = 'vertical' export const HANDLE_HORIZONTAL = 'horizontal' export const HANDLE_PIXELS = 30 +export const MAX_SIZE = 1 +export const MIN_SIZE = 0 diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 364c13b96c..54ed70504e 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -62,13 +62,23 @@ $threesizer-handle: 30px; } &:hover { - cursor: row-resize; + &.vertical { + cursor: col-resize; + } + &.horizontal { + cursor: row-resize; + } color: $g16-pearl; background-color: $g5-pepper; } &.dragging { - cursor: row-resize; + &.vertical { + cursor: col-resize; + } + &.horizontal { + cursor: row-resize; + } color: $c-laser; background-color: $g5-pepper; } From 938adc39baf7545d9e16f0c90e68c946f4f5737f Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 14:49:13 -0700 Subject: [PATCH 051/104] Put stopDrag on document --- ui/src/shared/components/Threesizer.tsx | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 576c693e81..139b24074c 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -65,6 +65,16 @@ class Threesizer extends Component { } } + public componentDidMount() { + document.addEventListener('mouseup', this.handleStopDrag) + document.addEventListener('mouseleave', this.handleStopDrag) + } + + public componentWillUnmount() { + document.removeEventListener('mouseup', this.handleStopDrag) + document.removeEventListener('mouseleave', this.handleStopDrag) + } + public componentDidUpdate(__, prevState) { const {dragEvent} = this.state const {orientation} = this.props @@ -112,7 +122,6 @@ class Threesizer extends Component { return (
(this.containerRef = r)} @@ -207,10 +216,6 @@ class Threesizer extends Component { this.setState({activeHandleID: '', dragEvent: initialDragEvent}) } - private handleMouseLeave = () => { - this.setState({activeHandleID: '', dragEvent: initialDragEvent}) - } - private mousePosWithinContainer = (e: MouseEvent) => { const {pageY, pageX} = e const {top, left, width, height} = this.containerRef.getBoundingClientRect() From ca997ee960c32bf7d1496ce2581d43a7064b7d2d Mon Sep 17 00:00:00 2001 From: Alex P Date: Thu, 3 May 2018 14:57:24 -0700 Subject: [PATCH 052/104] Implement Threesizer in DE --- .../data_explorer/containers/DataExplorer.tsx | 80 ++++++++++--------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/ui/src/data_explorer/containers/DataExplorer.tsx b/ui/src/data_explorer/containers/DataExplorer.tsx index adb372dd31..099d0d6b26 100644 --- a/ui/src/data_explorer/containers/DataExplorer.tsx +++ b/ui/src/data_explorer/containers/DataExplorer.tsx @@ -13,12 +13,16 @@ import QueryMaker from 'src/data_explorer/components/QueryMaker' import Visualization from 'src/data_explorer/components/Visualization' import WriteDataForm from 'src/data_explorer/components/WriteDataForm' import Header from 'src/data_explorer/containers/Header' -import Resizer from 'src/shared/components/ResizeContainer' +import Threesizer from 'src/shared/components/Threesizer' import OverlayTechnologies from 'src/shared/components/OverlayTechnologies' import ManualRefresh from 'src/shared/components/ManualRefresh' -import {VIS_VIEWS, AUTO_GROUP_BY, TEMPLATES} from 'src/shared/constants' -import {MINIMUM_HEIGHTS} from 'src/data_explorer/constants' +import { + VIS_VIEWS, + AUTO_GROUP_BY, + TEMPLATES, + HANDLE_HORIZONTAL, +} from 'src/shared/constants' import {errorThrown} from 'src/shared/actions/errors' import {setAutoRefresh} from 'src/shared/actions/app' import * as dataExplorerActionCreators from 'src/data_explorer/actions/view' @@ -120,33 +124,18 @@ export class DataExplorer extends PureComponent { onChooseAutoRefresh={handleChooseAutoRefresh} onManualRefresh={onManualRefresh} /> - - {this.renderTop()} - {this.renderBottom()} - + divisions={this.divisions} + />
) } - private renderTop = () => { - const {source, queryConfigActions} = this.props - return ( - - ) - } - - private renderBottom = () => { + private get divisions() { const { + source, timeRange, autoRefresh, queryConfigs, @@ -155,19 +144,36 @@ export class DataExplorer extends PureComponent { queryConfigActions, } = this.props - return ( - - ) + return [ + { + name: 'Query Builder', + render: () => ( + + ), + }, + { + name: 'Visualization', + render: () => ( + + ), + }, + ] } private handleCloseWriteData = (): void => { From 2e2563f812b1d7028eb6c25406c0bd3edc32e042 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 14:59:22 -0700 Subject: [PATCH 053/104] Cleanup --- ui/src/shared/components/ResizeDivision.tsx | 18 +++++++----------- ui/src/shared/components/Threesizer.tsx | 4 ---- ui/src/style/components/threesizer.scss | 4 ++++ 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 2efbfef5fd..f8ab276ffd 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -9,7 +9,6 @@ const NOOP = () => {} interface Props { id: string name?: string - minPixels: number size: number offset: number activeHandleID: string @@ -26,25 +25,22 @@ class Division extends PureComponent { } public render() { - const {name, render, orientation} = this.props + const {name, render, orientation, draggable} = this.props return ( <>
{name}
- +
{render()} - +
) diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index 139b24074c..faf80e0f31 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -30,13 +30,11 @@ interface State { interface Division { name?: string render: () => ReactElement - minPixels?: number } interface DivisionState extends Division { id: string size: number - minPixels?: number } interface Props { @@ -134,7 +132,6 @@ class Threesizer extends Component { size={d.size} offset={this.offset} draggable={i > 0} - minPixels={d.minPixels} orientation={orientation} activeHandleID={activeHandleID} onDoubleClick={this.handleDoubleClick} @@ -170,7 +167,6 @@ class Threesizer extends Component { ...d, id: uuid.v4(), size, - minPixels: d.minPixels || 0, })) } diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 54ed70504e..28a7d4a38c 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -68,6 +68,10 @@ $threesizer-handle: 30px; &.horizontal { cursor: row-resize; } + &.disabled { + cursor: pointer; + } + color: $g16-pearl; background-color: $g5-pepper; } From b978288ab53d4653b5387b18db8cef9ac659b770 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 15:01:08 -0700 Subject: [PATCH 054/104] Remove unused import --- ui/src/shared/components/ResizeDivision.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index f8ab276ffd..cbca43a9cf 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,7 +1,6 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react' import classnames from 'classnames' -import FancyScrollbar from 'src/shared/components/FancyScrollbar' import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' const NOOP = () => {} From c4c509038072b0a4377c142d221083c31de7aa11 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 15:29:17 -0700 Subject: [PATCH 055/104] Add ability to not display a handle --- ui/src/ifql/components/TimeMachine.tsx | 1 + ui/src/shared/components/ResizeDivision.tsx | 52 ++++++++++++++++++--- ui/src/shared/components/Threesizer.tsx | 13 +++++- ui/src/shared/constants/index.tsx | 1 + ui/src/style/components/threesizer.scss | 10 ---- 5 files changed, 59 insertions(+), 18 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index fc658dd309..5e76d96195 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -34,6 +34,7 @@ class TimeMachine extends PureComponent { private get mainSplit() { return [ { + handleDisplay: 'none', render: () => ( {} interface Props { - id: string name?: string + handleDisplay?: string + id: string size: number offset: number - activeHandleID: string draggable: boolean orientation: string + activeHandleID: string render: () => ReactElement onHandleStartDrag: (id: string, e: MouseEvent) => void onDoubleClick: (id: string) => void @@ -21,6 +26,7 @@ interface Props { class Division extends PureComponent { public static defaultProps: Partial = { name: '', + handleDisplay: 'visible', } public render() { @@ -29,15 +35,19 @@ class Division extends PureComponent { <>
{name}
-
+
{render()}
@@ -49,6 +59,26 @@ class Division extends PureComponent { return 'Drag to resize.\nDouble click to expand.' } + private get contentStyle() { + if (this.props.orientation === HANDLE_HORIZONTAL) { + return { + height: `calc(100% - ${this.handlePixels}px)`, + } + } + + return { + width: `calc(100% - ${this.handlePixels}px)`, + } + } + + private get handleStyle() { + const {handleDisplay: display} = this.props + + return { + display, + } + } + private get containerStyle() { if (this.props.orientation === HANDLE_HORIZONTAL) { return { @@ -63,7 +93,15 @@ class Division extends PureComponent { private get size(): string { const {size, offset} = this.props - return `calc((100% - ${offset}px) * ${size} + 30px)` + return `calc((100% - ${offset}px) * ${size} + ${this.handlePixels}px)` + } + + private get handlePixels(): number { + if (this.props.handleDisplay === 'none') { + return 0 + } + + return HANDLE_PIXELS } private get containerClass(): string { @@ -76,7 +114,7 @@ class Division extends PureComponent { }) } - private get className(): string { + private get handleClass(): string { const {draggable, orientation} = this.props return classnames('threesizer--handle', { diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index faf80e0f31..e9c3eb5f40 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -6,6 +6,7 @@ import _ from 'lodash' import ResizeDivision from 'src/shared/components/ResizeDivision' import {ErrorHandling} from 'src/shared/decorators/errors' import { + HANDLE_NONE, HANDLE_PIXELS, HANDLE_HORIZONTAL, HANDLE_VERTICAL, @@ -29,6 +30,7 @@ interface State { interface Division { name?: string + handleDisplay?: string render: () => ReactElement } @@ -133,6 +135,7 @@ class Threesizer extends Component { offset={this.offset} draggable={i > 0} orientation={orientation} + handleDisplay={d.handleDisplay} activeHandleID={activeHandleID} onDoubleClick={this.handleDoubleClick} onHandleStartDrag={this.handleStartDrag} @@ -144,7 +147,15 @@ class Threesizer extends Component { } private get offset(): number { - return HANDLE_PIXELS * this.state.divisions.length + const numHandles = this.props.divisions.reduce((acc, d) => { + if (d.handleDisplay === HANDLE_NONE) { + return acc + } + + return acc + 1 + }, 0) + + return HANDLE_PIXELS * numHandles } private get className(): string { diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index 523db3990b..3e3d87a466 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -483,6 +483,7 @@ export const HUNDRED = 100 export const REQUIRED_HALVES = 2 export const HANDLE_VERTICAL = 'vertical' export const HANDLE_HORIZONTAL = 'horizontal' +export const HANDLE_NONE = 'none' export const HANDLE_PIXELS = 30 export const MAX_SIZE = 1 export const MIN_SIZE = 0 diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 28a7d4a38c..919dbda538 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -98,13 +98,3 @@ $threesizer-handle: 30px; transform: rotate(90deg) translateX(8px); } } - -/* Division Contents */ -.threesizer--contents { - &.horizontal { - height: calc(100% - #{$threesizer-handle}) !important; - } - &.vertical { - width: calc(100% - #{$threesizer-handle}) !important; - } -} From 3caceee8a45b37a68e379360d0f34b51ca05ada9 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 16:09:07 -0700 Subject: [PATCH 056/104] Add ability to specify handle pixel girth --- ui/src/ifql/components/TimeMachine.tsx | 9 +++++---- ui/src/shared/components/ResizeDivision.tsx | 19 ++++++++++++------- ui/src/shared/components/Threesizer.tsx | 9 ++++++--- ui/src/style/components/threesizer.scss | 2 -- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/ui/src/ifql/components/TimeMachine.tsx b/ui/src/ifql/components/TimeMachine.tsx index 5e76d96195..e26961f38c 100644 --- a/ui/src/ifql/components/TimeMachine.tsx +++ b/ui/src/ifql/components/TimeMachine.tsx @@ -43,6 +43,7 @@ class TimeMachine extends PureComponent { ), }, { + handlePixels: 8, render: () => , }, ] @@ -57,14 +58,14 @@ class TimeMachine extends PureComponent { ), }, - { - name: 'Explore', - render: () => , - }, { name: 'Build', render: () => , }, + { + name: 'Explore', + render: () => , + }, ] } } diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 2cec69aee5..6a9b376b46 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,17 +1,14 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react' import classnames from 'classnames' -import { - HANDLE_PIXELS, - HANDLE_VERTICAL, - HANDLE_HORIZONTAL, -} from 'src/shared/constants/index' +import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' const NOOP = () => {} interface Props { name?: string handleDisplay?: string + handlePixels: number id: string size: number offset: number @@ -72,10 +69,18 @@ class Division extends PureComponent { } private get handleStyle() { - const {handleDisplay: display} = this.props + const {handleDisplay: display, orientation, handlePixels} = this.props + + if (orientation === HANDLE_HORIZONTAL) { + return { + display, + height: `${handlePixels}px`, + } + } return { display, + width: `${handlePixels}px`, } } @@ -101,7 +106,7 @@ class Division extends PureComponent { return 0 } - return HANDLE_PIXELS + return this.props.handlePixels } private get containerClass(): string { diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index e9c3eb5f40..fa6245a876 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -31,6 +31,7 @@ interface State { interface Division { name?: string handleDisplay?: string + handlePixels?: number render: () => ReactElement } @@ -135,6 +136,7 @@ class Threesizer extends Component { offset={this.offset} draggable={i > 0} orientation={orientation} + handlePixels={d.handlePixels} handleDisplay={d.handleDisplay} activeHandleID={activeHandleID} onDoubleClick={this.handleDoubleClick} @@ -147,15 +149,15 @@ class Threesizer extends Component { } private get offset(): number { - const numHandles = this.props.divisions.reduce((acc, d) => { + const handlesPixelCount = this.state.divisions.reduce((acc, d) => { if (d.handleDisplay === HANDLE_NONE) { return acc } - return acc + 1 + return acc + d.handlePixels }, 0) - return HANDLE_PIXELS * numHandles + return handlesPixelCount } private get className(): string { @@ -178,6 +180,7 @@ class Threesizer extends Component { ...d, id: uuid.v4(), size, + handlePixels: d.handlePixels || HANDLE_PIXELS, })) } diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 919dbda538..6aac482bbc 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -52,12 +52,10 @@ $threesizer-handle: 30px; transition: background-color 0.25s ease, color 0.25s ease; &.vertical { - width: $threesizer-handle; padding: 12px 0; } &.horizontal { - height: $threesizer-handle; padding: 0 12px; } From 81fa610cd00532fecc76b1f6e0f2eefc306cba90 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 17:05:21 -0700 Subject: [PATCH 057/104] Add moar styles Co-authored-by: Andrew Watkins Co-authored-by: Alex Paxton --- ui/src/shared/components/ResizeDivision.tsx | 14 +++-- ui/src/shared/components/Threesizer.tsx | 1 + ui/src/style/components/threesizer.scss | 68 +++++++++++++++++---- 3 files changed, 66 insertions(+), 17 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 6a9b376b46..4a33807022 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -27,7 +27,7 @@ class Division extends PureComponent { } public render() { - const {name, render, orientation, draggable} = this.props + const {name, render, draggable} = this.props return ( <>
@@ -41,10 +41,7 @@ class Division extends PureComponent { >
{name}
-
+
{render()}
@@ -130,6 +127,13 @@ class Division extends PureComponent { }) } + private get contentsClass(): string { + const {orientation, size} = this.props + return classnames(`threesizer--contents ${orientation}`, { + 'no-shadows': !size, + }) + } + private get isDragging(): boolean { const {id, activeHandleID} = this.props return id === activeHandleID diff --git a/ui/src/shared/components/Threesizer.tsx b/ui/src/shared/components/Threesizer.tsx index fa6245a876..687c312e19 100644 --- a/ui/src/shared/components/Threesizer.tsx +++ b/ui/src/shared/components/Threesizer.tsx @@ -50,6 +50,7 @@ interface Props { class Threesizer extends Component { public static defaultProps: Partial = { orientation: HANDLE_HORIZONTAL, + containerClass: '', } private containerRef: HTMLElement diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 6aac482bbc..d5c3e3deb5 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -53,19 +53,25 @@ $threesizer-handle: 30px; &.vertical { padding: 12px 0; + border-right: solid 2px $g3-castle; + + &:hover, + &.dragging { + cursor: col-resize; + } } &.horizontal { padding: 0 12px; + border-bottom: solid 2px $g3-castle; + + &:hover, + &.dragging { + cursor: row-resize; + } } &:hover { - &.vertical { - cursor: col-resize; - } - &.horizontal { - cursor: row-resize; - } &.disabled { cursor: pointer; } @@ -75,16 +81,15 @@ $threesizer-handle: 30px; } &.dragging { - &.vertical { - cursor: col-resize; - } - &.horizontal { - cursor: row-resize; - } color: $c-laser; background-color: $g5-pepper; } } +// First Handle should not have a outside facing border +// .threesizer:first-child .threesizer--division .threesizer--handle { +// border-top: 0; +// border-left: 0; +// } .threesizer--title { font-size: 13px; @@ -96,3 +101,42 @@ $threesizer-handle: 30px; transform: rotate(90deg) translateX(8px); } } + +$threesizer-shadow-size: 9px; +$threesizer-z-index: 2; +$threesizer-shadow-start: fade-out($g0-obsidian, 0.82); +$threesizer-shadow-stop: fade-out($g0-obsidian, 1); + +.threesizer--contents { + position: relative; + + // Bottom Shadow + &.horizontal:after, + &.vertical:after { + content: ''; + position: absolute; + bottom: 0; + right: 0; + z-index: $threesizer-z-index; + } + + &.horizontal:after { + width: 100%; + height: $threesizer-shadow-size; + @include gradient-v($threesizer-shadow-stop, $threesizer-shadow-start); + } + + &.vertical:after { + height: 100%; + width: $threesizer-shadow-size; + @include gradient-h($threesizer-shadow-stop, $threesizer-shadow-start); + } +} + +// Hide bottom shadow on last division +.threesizer--contents.no-shadows:before, +.threesizer--contents.no-shadows:after, +.threesizer--division:last-child .threesizer--contents:after { + content: none; + display: none; +} From 2f8d04e61ae30b8a6774b02e1f959efd128df92e Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Thu, 3 May 2018 17:15:51 -0700 Subject: [PATCH 058/104] Return DataExplorer to original status --- ui/src/data_explorer/constants/index.js | 5 + ui/src/shared/components/ResizeContainer.js | 168 ++++++++++++++++++ ui/src/shared/components/ResizeContainer.tsx | 173 ------------------- ui/src/shared/components/ResizeHalf.tsx | 62 ------- ui/src/shared/components/ResizeHandle.tsx | 63 ++----- ui/src/style/components/resizer.scss | 139 +++++---------- 6 files changed, 235 insertions(+), 375 deletions(-) create mode 100644 ui/src/shared/components/ResizeContainer.js delete mode 100644 ui/src/shared/components/ResizeContainer.tsx delete mode 100644 ui/src/shared/components/ResizeHalf.tsx diff --git a/ui/src/data_explorer/constants/index.js b/ui/src/data_explorer/constants/index.js index 82d37421ce..85c08d8283 100644 --- a/ui/src/data_explorer/constants/index.js +++ b/ui/src/data_explorer/constants/index.js @@ -16,6 +16,11 @@ export const MINIMUM_HEIGHTS = { visualization: 200, } +export const INITIAL_HEIGHTS = { + queryMaker: '66.666%', + visualization: '33.334%', +} + const SEPARATOR = 'SEPARATOR' export const QUERY_TEMPLATES = [ diff --git a/ui/src/shared/components/ResizeContainer.js b/ui/src/shared/components/ResizeContainer.js new file mode 100644 index 0000000000..d9aaf53c9b --- /dev/null +++ b/ui/src/shared/components/ResizeContainer.js @@ -0,0 +1,168 @@ +import React, {Component} from 'react' +import PropTypes from 'prop-types' +import classnames from 'classnames' + +import ResizeHandle from 'shared/components/ResizeHandle' +import {ErrorHandling} from 'src/shared/decorators/errors' + +const maximumNumChildren = 2 +const defaultMinTopHeight = 200 +const defaultMinBottomHeight = 200 +const defaultInitialTopHeight = '50%' +const defaultInitialBottomHeight = '50%' + +@ErrorHandling +class ResizeContainer extends Component { + constructor(props) { + super(props) + this.state = { + isDragging: false, + topHeight: props.initialTopHeight, + bottomHeight: props.initialBottomHeight, + } + } + + static defaultProps = { + minTopHeight: defaultMinTopHeight, + minBottomHeight: defaultMinBottomHeight, + initialTopHeight: defaultInitialTopHeight, + initialBottomHeight: defaultInitialBottomHeight, + } + + componentDidMount() { + this.setState({ + bottomHeightPixels: this.bottom.getBoundingClientRect().height, + topHeightPixels: this.top.getBoundingClientRect().height, + }) + } + + handleStartDrag = () => { + this.setState({isDragging: true}) + } + + handleStopDrag = () => { + this.setState({isDragging: false}) + } + + handleMouseLeave = () => { + this.setState({isDragging: false}) + } + + handleDrag = e => { + if (!this.state.isDragging) { + return + } + + const {minTopHeight, minBottomHeight} = this.props + const oneHundred = 100 + const containerHeight = parseInt( + getComputedStyle(this.resizeContainer).height, + 10 + ) + // verticalOffset moves the resize handle as many pixels as the page-heading is taking up. + const verticalOffset = window.innerHeight - containerHeight + const newTopPanelPercent = Math.ceil( + (e.pageY - verticalOffset) / containerHeight * oneHundred + ) + const newBottomPanelPercent = oneHundred - newTopPanelPercent + + // Don't trigger a resize unless the change in size is greater than minResizePercentage + const minResizePercentage = 0.5 + if ( + Math.abs(newTopPanelPercent - parseFloat(this.state.topHeight)) < + minResizePercentage + ) { + return + } + + const topHeightPixels = newTopPanelPercent / oneHundred * containerHeight + const bottomHeightPixels = + newBottomPanelPercent / oneHundred * containerHeight + + // Don't trigger a resize if the new sizes are too small + if ( + topHeightPixels < minTopHeight || + bottomHeightPixels < minBottomHeight + ) { + return + } + + this.setState({ + topHeight: `${newTopPanelPercent}%`, + bottomHeight: `${newBottomPanelPercent}%`, + bottomHeightPixels, + topHeightPixels, + }) + } + + render() { + const { + topHeightPixels, + bottomHeightPixels, + topHeight, + bottomHeight, + isDragging, + } = this.state + const {containerClass, children, theme} = this.props + + if (React.Children.count(children) > maximumNumChildren) { + console.error( + `There cannot be more than ${maximumNumChildren}' children in ResizeContainer` + ) + return + } + + return ( +
(this.resizeContainer = r)} + > +
(this.top = r)} + > + {React.cloneElement(children[0], { + resizerBottomHeight: bottomHeightPixels, + resizerTopHeight: topHeightPixels, + })} +
+ +
(this.bottom = r)} + > + {React.cloneElement(children[1], { + resizerBottomHeight: bottomHeightPixels, + resizerTopHeight: topHeightPixels, + })} +
+
+ ) + } +} + +const {node, number, string} = PropTypes + +ResizeContainer.propTypes = { + children: node.isRequired, + containerClass: string.isRequired, + minTopHeight: number, + minBottomHeight: number, + initialTopHeight: string, + initialBottomHeight: string, + theme: string, +} + +export default ResizeContainer diff --git a/ui/src/shared/components/ResizeContainer.tsx b/ui/src/shared/components/ResizeContainer.tsx deleted file mode 100644 index 2b5e098997..0000000000 --- a/ui/src/shared/components/ResizeContainer.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import React, {Component, ReactNode, MouseEvent} from 'react' -import classnames from 'classnames' - -import ResizeHalf from 'src/shared/components/ResizeHalf' -import ResizeHandle from 'src/shared/components/ResizeHandle' -import {ErrorHandling} from 'src/shared/decorators/errors' -import { - HANDLE_HORIZONTAL, - HANDLE_VERTICAL, - REQUIRED_HALVES, -} from 'src/shared/constants/index' - -interface State { - topPercent: number - bottomPercent: number - isDragging: boolean -} - -interface Props { - children: ReactNode - topMinPixels: number - bottomMinPixels: number - orientation?: string - containerClass: string -} - -@ErrorHandling -class Resizer extends Component { - public static defaultProps: Partial = { - orientation: HANDLE_HORIZONTAL, - } - - private containerRef: HTMLElement - - constructor(props) { - super(props) - this.state = { - topPercent: 0.5, - bottomPercent: 0.5, - isDragging: false, - } - } - - public render() { - const {isDragging, topPercent, bottomPercent} = this.state - const {children, topMinPixels, bottomMinPixels, orientation} = this.props - - if (React.Children.count(children) !== REQUIRED_HALVES) { - console.error('ResizeContainer requires exactly 2 children') - return null - } - - return ( -
(this.containerRef = r)} - > - - - -
- ) - } - - private get className(): string { - const {orientation, containerClass} = this.props - const {isDragging} = this.state - - return classnames(`resize--container ${containerClass}`, { - dragging: isDragging, - horizontal: orientation === HANDLE_HORIZONTAL, - vertical: orientation === HANDLE_VERTICAL, - }) - } - - private handleStartDrag = () => { - this.setState({isDragging: true}) - } - - private handleStopDrag = () => { - this.setState({isDragging: false}) - } - - private handleMouseLeave = () => { - this.setState({isDragging: false}) - } - - private handleDrag = (e: MouseEvent) => { - const {isDragging} = this.state - const {orientation} = this.props - if (!isDragging) { - return - } - - const {percentX, percentY} = this.mousePosWithinContainer(e) - - if (orientation === HANDLE_HORIZONTAL && this.dragIsWithinBounds(e)) { - this.setState({ - topPercent: percentY, - bottomPercent: this.invertPercent(percentY), - }) - } - - if (orientation === HANDLE_VERTICAL && this.dragIsWithinBounds(e)) { - this.setState({ - topPercent: percentX, - bottomPercent: this.invertPercent(percentX), - }) - } - } - - private dragIsWithinBounds = (e: MouseEvent): boolean => { - const {orientation, topMinPixels, bottomMinPixels} = this.props - const {mouseX, mouseY} = this.mousePosWithinContainer(e) - const {width, height} = this.containerRef.getBoundingClientRect() - - if (orientation === HANDLE_HORIZONTAL) { - const doesNotExceedTop = mouseY > topMinPixels - const doesNotExceedBottom = Math.abs(mouseY - height) > bottomMinPixels - - return doesNotExceedTop && doesNotExceedBottom - } - - const doesNotExceedLeft = mouseX > topMinPixels - const doesNotExceedRight = Math.abs(mouseX - width) > bottomMinPixels - - return doesNotExceedLeft && doesNotExceedRight - } - - private mousePosWithinContainer = (e: MouseEvent) => { - const {pageY, pageX} = e - const {top, left, width, height} = this.containerRef.getBoundingClientRect() - - const mouseX = pageX - left - const mouseY = pageY - top - - const percentX = mouseX / width - const percentY = mouseY / height - - return { - mouseX, - mouseY, - percentX, - percentY, - } - } - - private invertPercent = percent => { - return 1 - percent - } -} - -export default Resizer diff --git a/ui/src/shared/components/ResizeHalf.tsx b/ui/src/shared/components/ResizeHalf.tsx deleted file mode 100644 index 4f7920b199..0000000000 --- a/ui/src/shared/components/ResizeHalf.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import React, {PureComponent, ReactElement} from 'react' -import classnames from 'classnames' - -import { - HANDLE_VERTICAL, - HANDLE_HORIZONTAL, - HUNDRED, -} from 'src/shared/constants/' - -interface Props { - percent: number - minPixels: number - component: ReactElement - orientation: string - offset: number -} - -class ResizeHalf extends PureComponent { - public render() { - const {component} = this.props - - return ( -
- {component} -
- ) - } - - private get style() { - const {orientation, minPixels, percent, offset} = this.props - - const size = `${percent * HUNDRED}%` - const gap = `${offset * HUNDRED}%` - - if (orientation === HANDLE_VERTICAL) { - return { - top: '0', - width: size, - left: gap, - minWidth: minPixels, - } - } - - return { - left: '0', - height: size, - top: gap, - minHeight: minPixels, - } - } - - private get className(): string { - const {orientation} = this.props - - return classnames('resize--half', { - vertical: orientation === HANDLE_VERTICAL, - horizontal: orientation === HANDLE_HORIZONTAL, - }) - } -} - -export default ResizeHalf diff --git a/ui/src/shared/components/ResizeHandle.tsx b/ui/src/shared/components/ResizeHandle.tsx index f05528c8ca..09112d3b1c 100644 --- a/ui/src/shared/components/ResizeHandle.tsx +++ b/ui/src/shared/components/ResizeHandle.tsx @@ -1,54 +1,27 @@ -import React, {PureComponent, MouseEvent} from 'react' +import React, {SFC} from 'react' import classnames from 'classnames' -import { - HANDLE_VERTICAL, - HANDLE_HORIZONTAL, - HUNDRED, -} from 'src/shared/constants/' - interface Props { - onStartDrag: (e: MouseEvent) => void + onHandleStartDrag: () => void isDragging: boolean - orientation: string - percent: number + theme?: string + top?: string } -class ResizeHandle extends PureComponent { - public render() { - return ( -
- ) - } - - private get style() { - const {percent, orientation} = this.props - const size = `${percent * HUNDRED}%` - - if (orientation === HANDLE_VERTICAL) { - return {left: size} - } - - return {top: size} - } - - private get className(): string { - const {isDragging, orientation} = this.props - - return classnames('resizer--handle', { +const ResizeHandle: SFC = ({ + onHandleStartDrag, + isDragging, + theme, + top, +}) => ( +
): void => { - this.props.onStartDrag(e) - } -} + 'resizer--malachite': theme === 'kapacitor', + })} + onMouseDown={onHandleStartDrag} + style={{top}} + /> +) export default ResizeHandle diff --git a/ui/src/style/components/resizer.scss b/ui/src/style/components/resizer.scss index 246a8c4525..967d1299cd 100644 --- a/ui/src/style/components/resizer.scss +++ b/ui/src/style/components/resizer.scss @@ -1,15 +1,12 @@ /* - Resizable Container - ------------------------------------------------------------------------------ + Resizable Container + ---------------------------------------------- */ -$resizer-division-z: 1; -$resizer-clickable-z: 2; -$resizer-line-z: 3; -$resizer-handle-z: 4; - $resizer-line-width: 2px; +$resizer-line-z: 2; $resizer-handle-width: 10px; +$resizer-handle-z: 3; $resizer-click-area: 28px; $resizer-glow: 14px; $resizer-dots: $g3-castle; @@ -17,45 +14,44 @@ $resizer-color: $g5-pepper; $resizer-color-hover: $g8-storm; $resizer-color-active: $c-pool; $resizer-color-kapacitor: $c-rainforest; - .resize--container { - position: relative; - - &.dragging * { + overflow: hidden !important; + &.resize--dragging * { @include no-user-select(); } } -.resize--half { - z-index: $resizer-division-z; +.resize--top, +.resize--bottom { position: absolute; + width: 100%; + left: 0; +} - &.horizontal { - width: 100%; - left: 0; - } - - &.vertical { - height: 100%; - top: 0; - } - - .dragging & { - pointer-events: none; - } +.resizer--full-size { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; } /* - Resizable Container Handle - ------------------------------------------------------------------------------ + Resizable Container Handle + ---------------------------------------------- */ + .resizer--handle { - z-index: $resizer-clickable-z; + top: 60%; + left: 0; + height: $resizer-click-area; + margin-top: -$resizer-click-area/2; + margin-bottom: -$resizer-click-area/2; + width: 100%; + z-index: 1; user-select: none; -webkit-user-select: none; - position: absolute; - - // Psuedo element for handle + position: absolute; // Psuedo element for handle &:before { z-index: $resizer-handle-z; color: $resizer-dots; @@ -65,7 +61,7 @@ $resizer-color-kapacitor: $c-rainforest; position: absolute; top: 50%; left: 50%; - transform: translate(-50%,-50%); + transform: translate(-50%, -50%); width: 160px; height: $resizer-handle-width; line-height: $resizer-handle-width; @@ -73,21 +69,24 @@ $resizer-color-kapacitor: $c-rainforest; border-radius: 3px; white-space: nowrap; text-align: center; - transition: - background-color 0.25s ease; - } - // Psuedo element for line + transition: background-color 0.25s ease; + } // Psuedo element for line &:after { z-index: $resizer-line-z; content: ''; display: block; position: absolute; + top: 50%; + left: 0; + transform: translateY(-50%); + width: 100%; + height: $resizer-line-width; background-color: $resizer-color; box-shadow: 0 0 0 transparent; - transition: - background-color 0.19s ease; + transition: background-color 0.19s ease; } &:hover { + cursor: ns-resize; &:before { background-color: $resizer-color-hover; } @@ -98,69 +97,19 @@ $resizer-color-kapacitor: $c-rainforest; &.dragging { &:before, &:after { - transition: - box-shadow 0.3s ease, - background-color 0.3s ease; + transition: box-shadow 0.3s ease, background-color 0.3s ease; background-color: $resizer-color-active; box-shadow: 0 0 $resizer-glow $resizer-color-active; } } } -/* - Horizontal Handle - ------------------------------------------------------------------------------ -*/ -.resizer--handle.horizontal { - top: 0; - height: $resizer-click-area; - margin-top: -$resizer-click-area/2; - margin-bottom: -$resizer-click-area/2; - width: 100%; +/* Kapacitor Theme */ - &:hover { - cursor: row-resize; - } - - // Psuedo element for handle - &:before { - transform: translate(-50%,-50%); - } - // Psuedo element for line +.resizer--handle.resizer--malachite.dragging { + &:before, &:after { - top: 50%; - left: 0; - transform: translateY(-50%); - width: 100%; - height: $resizer-line-width; - } -} - -/* - Vertical Handle - ------------------------------------------------------------------------------ -*/ -.resizer--handle.vertical { - left: 0; - width: $resizer-click-area; - margin-left: -$resizer-click-area/2; - margin-right: -$resizer-click-area/2; - height: 100%; - - &:hover { - cursor: col-resize; - } - - // Psuedo element for handle - &:before { - transform: translate(-50%,-50%) rotate(90deg); - } - // Psuedo element for line - &:after { - left: 50%; - top: 0; - transform: translateX(-50%); - height: 100%; - width: $resizer-line-width; + background-color: $resizer-color-kapacitor; + box-shadow: 0 0 $resizer-glow $resizer-color-kapacitor; } } From e40383ad2abf78050de35770c83bef5f2781c7a2 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 4 May 2018 10:08:21 -0700 Subject: [PATCH 059/104] Revert back to old resizer for CEO --- .../components/CellEditorOverlay.tsx | 137 ++++++++---------- 1 file changed, 64 insertions(+), 73 deletions(-) diff --git a/ui/src/dashboards/components/CellEditorOverlay.tsx b/ui/src/dashboards/components/CellEditorOverlay.tsx index 723a7f834a..c22680c912 100644 --- a/ui/src/dashboards/components/CellEditorOverlay.tsx +++ b/ui/src/dashboards/components/CellEditorOverlay.tsx @@ -166,46 +166,13 @@ class CellEditorOverlay extends Component { } public render() { - return ( -
- - {this.renderVisualization()} - {this.renderControls()} - -
- ) - } - - private renderVisualization = () => { - const {templates, timeRange, autoRefresh, editQueryStatus} = this.props - - const {queriesWorkingDraft, isStaticLegend} = this.state - - return ( - - ) - } - - private renderControls = () => { - const {onCancel, templates, timeRange} = this.props + const { + onCancel, + templates, + timeRange, + autoRefresh, + editQueryStatus, + } = this.props const { activeQueryIndex, @@ -215,41 +182,65 @@ class CellEditorOverlay extends Component { } = this.state return ( - - - {isDisplayOptionsTabActive ? ( - - ) : ( - + + - )} - + + + {isDisplayOptionsTabActive ? ( + + ) : ( + + )} + + +
) } From b5eff08f8d6f2265be3613b1d7bd0a1e1b6f3992 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 4 May 2018 10:26:39 -0700 Subject: [PATCH 060/104] Add back old resizer to data explorer --- .../data_explorer/containers/DataExplorer.tsx | 53 ++++++------------- 1 file changed, 15 insertions(+), 38 deletions(-) diff --git a/ui/src/data_explorer/containers/DataExplorer.tsx b/ui/src/data_explorer/containers/DataExplorer.tsx index 099d0d6b26..e3581e86a8 100644 --- a/ui/src/data_explorer/containers/DataExplorer.tsx +++ b/ui/src/data_explorer/containers/DataExplorer.tsx @@ -13,16 +13,12 @@ import QueryMaker from 'src/data_explorer/components/QueryMaker' import Visualization from 'src/data_explorer/components/Visualization' import WriteDataForm from 'src/data_explorer/components/WriteDataForm' import Header from 'src/data_explorer/containers/Header' -import Threesizer from 'src/shared/components/Threesizer' +import ResizeContainer from 'src/shared/components/ResizeContainer' import OverlayTechnologies from 'src/shared/components/OverlayTechnologies' import ManualRefresh from 'src/shared/components/ManualRefresh' -import { - VIS_VIEWS, - AUTO_GROUP_BY, - TEMPLATES, - HANDLE_HORIZONTAL, -} from 'src/shared/constants' +import {VIS_VIEWS, AUTO_GROUP_BY, TEMPLATES} from 'src/shared/constants' +import {MINIMUM_HEIGHTS, INITIAL_HEIGHTS} from 'src/data_explorer/constants' import {errorThrown} from 'src/shared/actions/errors' import {setAutoRefresh} from 'src/shared/actions/app' import * as dataExplorerActionCreators from 'src/data_explorer/actions/view' @@ -95,9 +91,12 @@ export class DataExplorer extends PureComponent { source, timeRange, autoRefresh, + queryConfigs, + manualRefresh, onManualRefresh, errorThrownAction, writeLineProtocol, + queryConfigActions, handleChooseAutoRefresh, } = this.props @@ -124,30 +123,13 @@ export class DataExplorer extends PureComponent { onChooseAutoRefresh={handleChooseAutoRefresh} onManualRefresh={onManualRefresh} /> - -
- ) - } - - private get divisions() { - const { - source, - timeRange, - autoRefresh, - queryConfigs, - manualRefresh, - errorThrownAction, - queryConfigActions, - } = this.props - - return [ - { - name: 'Query Builder', - render: () => ( + minTopHeight={MINIMUM_HEIGHTS.queryMaker} + minBottomHeight={MINIMUM_HEIGHTS.visualization} + initialTopHeight={INITIAL_HEIGHTS.queryMaker} + initialBottomHeight={INITIAL_HEIGHTS.visualization} + > { activeQuery={this.activeQuery} initialGroupByTime={AUTO_GROUP_BY} /> - ), - }, - { - name: 'Visualization', - render: () => ( { errorThrown={errorThrownAction} editQueryStatus={queryConfigActions.editQueryStatus} /> - ), - }, - ] + +
+ ) } private handleCloseWriteData = (): void => { From 7128a6f0f25767c828abe7194a5e9cba80c3848c Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 4 May 2018 10:49:28 -0700 Subject: [PATCH 061/104] Fix css --- ui/src/style/pages/dashboards.scss | 62 +++++++++++++++--------------- 1 file changed, 30 insertions(+), 32 deletions(-) diff --git a/ui/src/style/pages/dashboards.scss b/ui/src/style/pages/dashboards.scss index 50fd0db458..3d3e4d080b 100644 --- a/ui/src/style/pages/dashboards.scss +++ b/ui/src/style/pages/dashboards.scss @@ -2,14 +2,15 @@ Variables ------------------------------------------------------ */ + $dash-graph-heading: 30px; $dash-graph-heading-context: $dash-graph-heading - 8px; $dash-graph-options-arrow: 8px; - /* Animations ------------------------------------------------------ */ + @keyframes refreshingSpinnerA { 0% { transform: translate(-50%, -50%) scale(1.75); @@ -25,6 +26,7 @@ $dash-graph-options-arrow: 8px; transform: translate(-50%, -50%) scale(1, 1); } } + @keyframes refreshingSpinnerB { 0% { transform: translate(-50%, -50%) scale(1, 1); @@ -40,6 +42,7 @@ $dash-graph-options-arrow: 8px; transform: translate(-50%, -50%) scale(1, 1); } } + @keyframes refreshingSpinnerC { 0% { transform: translate(-50%, -50%) scale(1, 1); @@ -60,6 +63,7 @@ $dash-graph-options-arrow: 8px; Dashboard Index Page ------------------------------------------------------ */ + .dashboards-page--actions { display: flex; align-items: center; @@ -69,6 +73,7 @@ $dash-graph-options-arrow: 8px; Default Dashboard Mode ------------------------------------------------------ */ + .cell-shell { background-color: $g3-castle; border-radius: $radius; @@ -89,6 +94,7 @@ $dash-graph-options-arrow: 8px; left: 0; } } + .dash-graph { position: absolute; width: 100%; @@ -96,6 +102,7 @@ $dash-graph-options-arrow: 8px; top: 0; left: 0; } + .dash-graph--container { user-select: none !important; -o-user-select: none !important; @@ -108,7 +115,6 @@ $dash-graph-options-arrow: 8px; top: $dash-graph-heading; left: 0; padding: 0; - .dygraph { position: absolute; left: 0; @@ -126,6 +132,7 @@ $dash-graph-options-arrow: 8px; top: (-$dash-graph-heading + 5px) !important; } } + .dash-graph--heading { user-select: none !important; -o-user-select: none !important; @@ -151,6 +158,7 @@ $dash-graph-options-arrow: 8px; background-color: $g5-pepper; } } + .dash-graph--name { font-size: 12px; font-weight: 600; @@ -165,23 +173,24 @@ $dash-graph-options-arrow: 8px; padding-left: 10px; transition: color 0.25s ease, background-color 0.25s ease, border-color 0.25s ease; - &:only-child { width: 100%; } } + .dash-graph--name.dash-graph--name__default { font-style: italic; } + .dash-graph--draggable { cursor: move !important; } + .dash-graph--custom-indicators { height: 24px; border-radius: 3px; display: flex; cursor: default; - > .custom-indicator, > .source-indicator { font-size: 10px; @@ -196,13 +205,13 @@ $dash-graph-options-arrow: 8px; } > .source-indicator { height: 24px; - > .icon { font-size: 12px; margin: 0; } } } + .dash-graph-context { z-index: 2; position: absolute; @@ -213,12 +222,15 @@ $dash-graph-options-arrow: 8px; align-items: center; flex-wrap: nowrap; } + .dash-graph-context.dash-graph-context__open { z-index: 20; } + .dash-graph-context--buttons { display: flex; } + .dash-graph-context--button { width: 24px; height: 24px; @@ -228,7 +240,6 @@ $dash-graph-options-arrow: 8px; color: $g11-sidewalk; margin-right: 2px; transition: color 0.25s ease, background-color 0.25s ease; - &:hover, &.active { cursor: pointer; @@ -238,7 +249,6 @@ $dash-graph-options-arrow: 8px; &:last-child { margin-right: 0; } - > .icon { position: absolute; top: 50%; @@ -250,6 +260,7 @@ $dash-graph-options-arrow: 8px; z-index: 20; } } + .dash-graph-context--menu, .dash-graph-context--menu.default { z-index: 3; @@ -263,7 +274,6 @@ $dash-graph-options-arrow: 8px; flex-direction: column; align-items: stretch; justify-content: center; - &:before { position: absolute; content: ''; @@ -274,7 +284,6 @@ $dash-graph-options-arrow: 8px; transform: translate(-50%, -100%); transition: border-color 0.25s ease; } - .dash-graph-context--menu-item { @include no-user-select(); white-space: nowrap; @@ -285,7 +294,6 @@ $dash-graph-options-arrow: 8px; padding: 0 10px; color: $g20-white; transition: background-color 0.25s ease; - &:first-child { border-top-left-radius: 3px; border-top-right-radius: 3px; @@ -298,7 +306,6 @@ $dash-graph-options-arrow: 8px; background-color: $g8-storm; cursor: pointer; } - &.disabled, &.disabled:hover { cursor: default; @@ -318,6 +325,7 @@ $dash-graph-options-arrow: 8px; background-color: $c-pool; } } + .dash-graph-context--menu.warning { background-color: $c-star; &:before { @@ -327,6 +335,7 @@ $dash-graph-options-arrow: 8px; background-color: $c-comet; } } + .dash-graph-context--menu.success { background-color: $c-rainforest; &:before { @@ -336,6 +345,7 @@ $dash-graph-options-arrow: 8px; background-color: $c-honeydew; } } + .dash-graph-context--menu.danger { background-color: $c-curacao; &:before { @@ -347,6 +357,7 @@ $dash-graph-options-arrow: 8px; } /* Presentation Mode */ + .presentation-mode { .dash-graph-context { display: none; @@ -364,7 +375,6 @@ $dash-graph-options-arrow: 8px; transform: translateX(50%); width: 16px; height: 18px; - > div { width: 4px; height: 4px; @@ -374,7 +384,6 @@ $dash-graph-options-arrow: 8px; top: 50%; transform: translate(-50%, -50%); } - div:nth-child(1) { left: 0; animation: refreshingSpinnerA 0.8s cubic-bezier(0.645, 0.045, 0.355, 1) @@ -396,13 +405,15 @@ $dash-graph-options-arrow: 8px; Dashboard Edit Mode ------------------------------------------------------ */ + .react-grid-placeholder { - @include gradient-diag-down($c-pool,$c-comet); + @include gradient-diag-down($c-pool, $c-comet); border: 0 !important; opacity: 0.3; z-index: 2; border-radius: $radius !important; } + .react-grid-item { &.resizing { background-color: fade-out($g3-castle, 0.09); @@ -413,7 +424,6 @@ $dash-graph-options-arrow: 8px; border-image-width: 2px; border-image-source: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDY3IDc5LjE1Nzc0NywgMjAxNS8wMy8zMC0yMzo0MDo0MiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6MTg0NjVDRkVGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MTg0NjVDRkZGMEVFMTFFNkE0QjVFRTJGNEI1ODc0RDMiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDoxODQ2NUNGQ0YwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDoxODQ2NUNGREYwRUUxMUU2QTRCNUVFMkY0QjU4NzREMyIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PpeetfIAAAMnSURBVHja7N1PatwwFMfxJ5NlKT1DIfQKWZfSA/Q0hexDL9McoOQAPUKglwhp6dZ9Ho/HfyTZs6l+b/E1GDm27IH5oH9Pyji9//7XfLtNZt88/eT722TzlvrFseXHaXFmypuO8vd5nmW6nyeNefrKfZv7i9f75blU/NzafXvns2dV7tl8zqsnT55+9f3Xjf/xwQ9+evou+xLB+N8Ydi4AX3z/6PnvOj94AEOGMV/rB4P00J2rKTC0GNOTPne0GWEwhv1NB0YYjNPWgREHI00gYMTAOIGAEQdjuKcDIw7GXGWBEQJjrLLACIORrFBlgaHDsG2VBYYWY1VlgaHHSH3WqIOhxLB1ow6GGmPRqIMRAeMMAkYUDFuGTsDQYwxP6MCIg1Hp9oKhwih0e8FQYthuLAuM5hj1WBYYEoxUjGWBIcOwrFEHQ4qxLiFgyDFOvSww4mCM8yFghMEoDgzB0GGk2owhGBoMq5UQMDQYxRIChg4ja0PA0GLYMrgIhh7jUkLAiIExV1lghMA4GBiC0RrjNIULRhyMysAQDBVGYWAIhhJjM6cOhhpjUULAiIAxr1wEIwTGPDAEIwTGWGWBEQajHu0FQ4JRjvaCIcPIo71gSDHW0V4w5Bj5SB0MKUZxoRwYOoxsPgQMLcZqPgQMPUaxUQdDh2HVcQgYEoxUHIeAIcPIqywwpBjrKgsMOcb8f+pghMDIwu9gaDFWI3Uw9Bg2N+pgRMA497LAiIJRXf0OhgajuPodDB3G1dFeMNpgXBXtBaMdxmG0F4y2GLvRXjDaY2wGhmCoMawU7QVDh5G20V4wtBjzwBCMEBiXVx6BEQPjsJcFRluM3V4WGO0xqr0sMDQYVuplgaHDWL1YEgw9hi17WWDoMVJ1ChcMCYYVp3DBkGFUl5KCocGw6deAwIiBYUfBRTDaYmTdXjC0GFYLLoKhwSj+cAAYOgzbBhfB0GKsgotg6DGuWrkIRjuMudsLRgiMsQ0BIwzG5ZVHYMTAmKqsVzBiYPj2Z+j2PoERAmM4/2MoIfe+v4Ahx3jx5H4AefYLd37q0Y9/g9EcY/jOHz11A3v+J8AA9wisahRCWTQAAAAASUVORK5CYII=); z-index: 3; - & > .react-resizable-handle { &:before, &:after { @@ -433,7 +443,6 @@ $dash-graph-options-arrow: 8px; &:hover { cursor: move; } - .dash-graph--heading { background-color: $g5-pepper; cursor: move; @@ -442,7 +451,6 @@ $dash-graph-options-arrow: 8px; & > .react-resizable-handle { background-image: none; cursor: nwse-resize; - &:before, &:after { content: ''; @@ -477,39 +485,29 @@ $dash-graph-options-arrow: 8px; Dashboard Empty State ------------------------------------------------------ */ -@import '../components/dashboard-empty'; +@import '../components/dashboard-empty'; /* Template Control Bar ------------------------------------------------------ */ -@import '../components/template-control-bar'; +@import '../components/template-control-bar'; /* Cell Editor Overlay ------------------------------------------------------ */ -<<<<<<< HEAD -@import 'cell-editor-overlay'; -======= -.ceo-resizer { - position: absolute; - width: 100%; - height: 100%; - top: 0; - left: 0; -} -@import 'overlay-technology'; ->>>>>>> Rebuild resizer component +@import 'cell-editor-overlay'; /* Template Variables Manager ------------------------------------------------------ */ -@import '../components/template-variables-manager'; +@import '../components/template-variables-manager'; /* Write Data Form ------------------------------------------------------ */ + @import '../components/write-data-form'; From ccd060f179cdeb981748ab58187b3f4cbc72b0aa Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 4 May 2018 11:25:26 -0700 Subject: [PATCH 062/104] Introduce stylesheet for IFQL schema explorer --- .../time-machine/ifql-explorer.scss | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 ui/src/style/components/time-machine/ifql-explorer.scss diff --git a/ui/src/style/components/time-machine/ifql-explorer.scss b/ui/src/style/components/time-machine/ifql-explorer.scss new file mode 100644 index 0000000000..05ae155ef8 --- /dev/null +++ b/ui/src/style/components/time-machine/ifql-explorer.scss @@ -0,0 +1,49 @@ +/* + IFQL Schema Explorer -- Tree View + ---------------------------------------------------------------------------- +*/ + +$ifql-tree-indent: 30px; + +.ifql-schema-explorer { + width: 100%; + height: 100%; +} + +.ifql-schema-tree { + display: flex; + flex-direction: column; + align-items: stretch; + + & > .ifql-schema-tree { + padding-left: $ifql-tree-indent; + } + + .ifql-schema-item + & { + display: none; + } + + .expanded .ifql-schema-item + & { + display: flex; + } +} + +.ifql-schema-item { + position: relative; + height: 30px; + display: flex; + align-items: center; + padding: 0 11px; + padding-left: 32px; + + > span { + position: absolute; + top: 50%; + left: 14px; + transform: translate(-50%, -50%); + transition: transform 0.25s ease; + } + +} + + From 716f7e8b4d42e94c9255a3613f23d01ee2c49c5d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Fri, 4 May 2018 11:31:27 -0700 Subject: [PATCH 063/104] Simplify schema explorer --- ui/src/ifql/apis/index.ts | 22 +++ ui/src/ifql/components/DatabaseList.tsx | 57 ++++---- ui/src/ifql/components/DatabaseListItem.tsx | 27 ++-- ui/src/ifql/components/MeasurementList.tsx | 125 ------------------ .../ifql/components/MeasurementListItem.tsx | 50 ------- ui/src/ifql/components/SchemaExplorer.tsx | 32 +---- ui/src/ifql/components/TagList.tsx | 50 ++----- ui/src/style/pages/time-machine.scss | 3 +- 8 files changed, 78 insertions(+), 288 deletions(-) delete mode 100644 ui/src/ifql/components/MeasurementList.tsx delete mode 100644 ui/src/ifql/components/MeasurementListItem.tsx diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts index 5b84069672..ba9c44f1d7 100644 --- a/ui/src/ifql/apis/index.ts +++ b/ui/src/ifql/apis/index.ts @@ -46,3 +46,25 @@ export const getDatabases = async () => { throw error } } + +export const getTags = async () => { + try { + const response = {data: {tags: ['tk1', 'tk2', 'tk3']}} + const {data} = await Promise.resolve(response) + return data.tags + } catch (error) { + console.error('Could not get tagKeys', error) + throw error + } +} + +export const getTagValues = async () => { + try { + const response = {data: {values: ['tv1', 'tv2', 'tv3']}} + const {data} = await Promise.resolve(response) + return data.values + } catch (error) { + console.error('Could not get tagKeys', error) + throw error + } +} diff --git a/ui/src/ifql/components/DatabaseList.tsx b/ui/src/ifql/components/DatabaseList.tsx index bff9a884c9..515b0109ef 100644 --- a/ui/src/ifql/components/DatabaseList.tsx +++ b/ui/src/ifql/components/DatabaseList.tsx @@ -1,34 +1,36 @@ import React, {PureComponent} from 'react' +import PropTypes from 'prop-types' import _ from 'lodash' import DatabaseListItem from 'src/ifql/components/DatabaseListItem' -import MeasurementList from 'src/ifql/components/MeasurementList' - -import {Source} from 'src/types' import {showDatabases} from 'src/shared/apis/metaQuery' import showDatabasesParser from 'src/shared/parsing/showDatabases' import {ErrorHandling} from 'src/shared/decorators/errors' -interface DatabaseListProps { - db: string - source: Source - onChooseDatabase: (database: string) => void -} - interface DatabaseListState { databases: string[] measurement: string + db: string } +const {shape} = PropTypes + @ErrorHandling -class DatabaseList extends PureComponent { +class DatabaseList extends PureComponent<{}, DatabaseListState> { + public static contextTypes = { + source: shape({ + links: shape({}).isRequired, + }).isRequired, + } + constructor(props) { super(props) this.state = { databases: [], measurement: '', + db: '', } } @@ -37,7 +39,7 @@ class DatabaseList extends PureComponent { } public async getDatabases() { - const {source} = this.props + const {source} = this.context try { const {data} = await showDatabases(source.links.proxy) @@ -46,34 +48,31 @@ class DatabaseList extends PureComponent { this.setState({databases: sorted}) const db = _.get(sorted, '0', '') - this.props.onChooseDatabase(db) + this.handleChooseDatabase(db) } catch (err) { console.error(err) } } public render() { - const {onChooseDatabase} = this.props - return ( -
-
- {this.state.databases.map(db => { - return ( - - - {this.props.db === db && } - - ) - })} -
+
+ {this.state.databases.map(db => { + return ( + + ) + })}
) } + + private handleChooseDatabase = (db: string): void => { + this.setState({db}) + } } export default DatabaseList diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx index e7e553ef8b..697d28618a 100644 --- a/ui/src/ifql/components/DatabaseListItem.tsx +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -2,17 +2,22 @@ import React, {PureComponent} from 'react' import classnames from 'classnames' -export interface Props { - isActive: boolean +import TagList from 'src/ifql/components/TagList' + +interface Props { db: string onChooseDatabase: (db: string) => void } -class DatabaseListItem extends PureComponent { +interface State { + isOpen: boolean +} + +class DatabaseListItem extends PureComponent { constructor(props) { super(props) this.state = { - measurement: '', + isOpen: false, } } @@ -21,23 +26,23 @@ class DatabaseListItem extends PureComponent { return (
- -
+
+
{db} - +
+ {this.state.isOpen && }
) } private get className(): string { - return classnames('query-builder--list-item', { - active: this.props.isActive, + return classnames('ifql-schema-tree', { + expanded: this.state.isOpen, }) } private handleChooseDatabase = () => { - const {onChooseDatabase, db} = this.props - onChooseDatabase(db) + this.setState({isOpen: !this.state.isOpen}) } } diff --git a/ui/src/ifql/components/MeasurementList.tsx b/ui/src/ifql/components/MeasurementList.tsx deleted file mode 100644 index 14c1a5fec5..0000000000 --- a/ui/src/ifql/components/MeasurementList.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import React, {PureComponent} from 'react' -import PropTypes from 'prop-types' - -import {showMeasurements} from 'src/shared/apis/metaQuery' -import showMeasurementsParser from 'src/shared/parsing/showMeasurements' - -import MeasurementListFilter from 'src/shared/components/MeasurementListFilter' -import MeasurementListItem from 'src/ifql/components/MeasurementListItem' -import {ErrorHandling} from 'src/shared/decorators/errors' - -interface Props { - db: string -} - -interface State { - measurements: string[] - filterText: string - filtered: string[] - selected: string -} - -const {shape} = PropTypes - -@ErrorHandling -class MeasurementList extends PureComponent { - public static contextTypes = { - source: shape({ - links: shape({}).isRequired, - }).isRequired, - } - - constructor(props) { - super(props) - this.state = { - filterText: '', - filtered: [], - measurements: [], - selected: '', - } - } - - public componentDidMount() { - if (!this.props.db) { - return - } - - this.getMeasurements() - } - - public render() { - const {filtered} = this.state - - return ( -
-
- Measurements & Tags - -
-
- {filtered.map(measurement => ( - - ))} -
-
- ) - } - - private async getMeasurements() { - const {source} = this.context - const {db} = this.props - - try { - const {data} = await showMeasurements(source.links.proxy, db) - const {measurementSets} = showMeasurementsParser(data) - const measurements = measurementSets[0].measurements - - const selected = measurements[0] - this.setState({measurements, filtered: measurements, selected}) - } catch (err) { - console.error(err) - } - } - - private handleChooseMeasurement = (selected: string): void => { - this.setState({selected}) - } - - private handleFilterText = e => { - e.stopPropagation() - const filterText = e.target.value - this.setState({ - filterText, - filtered: this.handleFilterMeasuremet(filterText), - }) - } - - private handleFilterMeasuremet = filter => { - return this.state.measurements.filter(m => - m.toLowerCase().includes(filter.toLowerCase()) - ) - } - - private handleEscape = e => { - if (e.key !== 'Escape') { - return - } - - e.stopPropagation() - this.setState({ - filterText: '', - }) - } -} - -export default MeasurementList diff --git a/ui/src/ifql/components/MeasurementListItem.tsx b/ui/src/ifql/components/MeasurementListItem.tsx deleted file mode 100644 index a2f87f5ca0..0000000000 --- a/ui/src/ifql/components/MeasurementListItem.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import React, {PureComponent} from 'react' -import {ErrorHandling} from 'src/shared/decorators/errors' -import TagList from 'src/ifql/components/TagList' - -interface Props { - db: string - selected: string - measurement: string - onChooseMeasurement: (measurement: string) => void -} - -interface State { - isOpen: boolean -} - -@ErrorHandling -class MeasurementListItem extends PureComponent { - constructor(props) { - super(props) - - this.state = {isOpen: false} - } - - public render() { - const {measurement, db} = this.props - - return ( -
-
- -
- {measurement} - -
- {this.shouldShow && } -
- ) - } - - private handleClick = () => { - const {measurement, onChooseMeasurement} = this.props - onChooseMeasurement(measurement) - } - - private get shouldShow(): boolean { - return this.state.isOpen - } -} - -export default MeasurementListItem diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx index 600ae85142..702634bff2 100644 --- a/ui/src/ifql/components/SchemaExplorer.tsx +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -1,42 +1,14 @@ import React, {PureComponent} from 'react' -import PropTypes from 'prop-types' import DatabaseList from 'src/ifql/components/DatabaseList' -interface State { - db: string -} - -const {shape} = PropTypes - -class SchemaExplorer extends PureComponent<{}, State> { - public static contextTypes = { - source: shape({ - links: shape({}).isRequired, - }).isRequired, - } - - constructor(props) { - super(props) - this.state = { - db: '', - } - } - +class SchemaExplorer extends PureComponent { public render() { return (
- +
) } - - private handleChooseDatabase = (db: string): void => { - this.setState({db}) - } } export default SchemaExplorer diff --git a/ui/src/ifql/components/TagList.tsx b/ui/src/ifql/components/TagList.tsx index c996b0c93b..3e4e3e2f3d 100644 --- a/ui/src/ifql/components/TagList.tsx +++ b/ui/src/ifql/components/TagList.tsx @@ -5,16 +5,13 @@ import _ from 'lodash' import TagListItem from 'src/ifql/components/TagListItem' -import {showTagKeys, showTagValues} from 'src/shared/apis/metaQuery' -import showTagKeysParser from 'src/shared/parsing/showTagKeys' -import showTagValuesParser from 'src/shared/parsing/showTagValues' +import {getTags, getTagValues} from 'src/ifql/apis' import {ErrorHandling} from 'src/shared/decorators/errors' const {shape} = PropTypes interface Props { db: string - measurement: string } interface State { @@ -39,24 +36,8 @@ class TagList extends PureComponent { } public componentDidMount() { - const {db, measurement} = this.props - if (!db || !measurement) { - return - } - - this.getTags() - } - - public componentDidUpdate(prevProps) { - const {db, measurement} = this.props - - const {db: prevDB, measurement: prevMeas} = prevProps - - if (!db || !measurement) { - return - } - - if (db === prevDB && measurement === prevMeas) { + const {db} = this.props + if (!db) { return } @@ -64,29 +45,14 @@ class TagList extends PureComponent { } public async getTags() { - const {db, measurement} = this.props - const {source} = this.context + const keys = await getTags() + const values = await getTagValues() - const {data} = await showTagKeys({ - database: db, - measurement, - retentionPolicy: 'autogen', - source: source.links.proxy, - }) - const {tagKeys} = showTagKeysParser(data) - - const response = await showTagValues({ - database: db, - measurement, - retentionPolicy: 'autogen', - source: source.links.proxy, - tagKeys, + const tags = keys.map(k => { + return (this.state.tags[k] = values) }) - const {tags} = showTagValuesParser(response.data) - - const selected = Object.keys(tags) - this.setState({tags, selectedTag: selected[0]}) + this.setState({tags}) } public render() { diff --git a/ui/src/style/pages/time-machine.scss b/ui/src/style/pages/time-machine.scss index 31447dca77..308c8128ed 100644 --- a/ui/src/style/pages/time-machine.scss +++ b/ui/src/style/pages/time-machine.scss @@ -5,5 +5,6 @@ @import '../components/time-machine/ifql-editor'; @import '../components/time-machine/ifql-builder'; +// @import '../components/time-machine/ifql-explorer'; @import '../components/time-machine/visualization'; -@import '../components/time-machine/add-func-button'; +@import '../components/time-machine/add-func-button'; \ No newline at end of file From a70d5811c98d7bb216dcef2de0e98764c099f2b7 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 4 May 2018 13:48:49 -0700 Subject: [PATCH 064/104] Implement styles for IFQL schema explorer --- ui/src/ifql/components/DatabaseList.tsx | 23 +--- ui/src/ifql/components/DatabaseListItem.tsx | 3 +- ui/src/ifql/components/SchemaExplorer.tsx | 21 ++++ ui/src/ifql/components/TagList.tsx | 16 +-- ui/src/ifql/components/TagListItem.tsx | 107 +++++------------- .../time-machine/ifql-explorer.scss | 79 ++++++++++--- ui/src/style/pages/time-machine.scss | 2 +- 7 files changed, 122 insertions(+), 129 deletions(-) diff --git a/ui/src/ifql/components/DatabaseList.tsx b/ui/src/ifql/components/DatabaseList.tsx index 515b0109ef..71b15db130 100644 --- a/ui/src/ifql/components/DatabaseList.tsx +++ b/ui/src/ifql/components/DatabaseList.tsx @@ -1,6 +1,5 @@ import React, {PureComponent} from 'react' import PropTypes from 'prop-types' -import _ from 'lodash' import DatabaseListItem from 'src/ifql/components/DatabaseListItem' @@ -47,31 +46,15 @@ class DatabaseList extends PureComponent<{}, DatabaseListState> { const sorted = databases.sort() this.setState({databases: sorted}) - const db = _.get(sorted, '0', '') - this.handleChooseDatabase(db) } catch (err) { console.error(err) } } public render() { - return ( -
- {this.state.databases.map(db => { - return ( - - ) - })} -
- ) - } - - private handleChooseDatabase = (db: string): void => { - this.setState({db}) + return this.state.databases.map(db => { + return + }) } } diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx index 697d28618a..17ac4c948f 100644 --- a/ui/src/ifql/components/DatabaseListItem.tsx +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -6,7 +6,6 @@ import TagList from 'src/ifql/components/TagList' interface Props { db: string - onChooseDatabase: (db: string) => void } interface State { @@ -27,7 +26,7 @@ class DatabaseListItem extends PureComponent { return (
-
+ {db}
{this.state.isOpen && } diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx index 702634bff2..62db36dffe 100644 --- a/ui/src/ifql/components/SchemaExplorer.tsx +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -5,6 +5,27 @@ class SchemaExplorer extends PureComponent { public render() { return (
+
+
+ +
+ +
) diff --git a/ui/src/ifql/components/TagList.tsx b/ui/src/ifql/components/TagList.tsx index 3e4e3e2f3d..abf6c3079c 100644 --- a/ui/src/ifql/components/TagList.tsx +++ b/ui/src/ifql/components/TagList.tsx @@ -48,21 +48,17 @@ class TagList extends PureComponent { const keys = await getTags() const values = await getTagValues() - const tags = keys.map(k => { - return (this.state.tags[k] = values) - }) + const tags = keys.reduce((acc, k) => { + return {...acc, [k]: values} + }, {}) this.setState({tags}) } public render() { - return ( -
- {_.map(this.state.tags, (tagValues: string[], tagKey: string) => ( - - ))} -
- ) + return _.map(this.state.tags, (tagValues: string[], tagKey: string) => ( + + )) } } diff --git a/ui/src/ifql/components/TagListItem.tsx b/ui/src/ifql/components/TagListItem.tsx index 7a235c7567..76599367b7 100644 --- a/ui/src/ifql/components/TagListItem.tsx +++ b/ui/src/ifql/components/TagListItem.tsx @@ -9,7 +9,6 @@ interface Props { interface State { isOpen: boolean - filterText: string } @ErrorHandling @@ -17,102 +16,52 @@ class TagListItem extends PureComponent { constructor(props) { super(props) this.state = { - filterText: '', isOpen: false, } - - this.handleEscape = this.handleEscape.bind(this) - this.handleClickKey = this.handleClickKey.bind(this) - this.handleFilterText = this.handleFilterText.bind(this) } - public handleClickKey(e: MouseEvent) { + public render() { + const {isOpen} = this.state + + return ( +
+
+ + {this.tagItemLabel} +
+ {isOpen && this.renderTagValues} +
+ ) + } + + private handleClick = (e: MouseEvent): void => { e.stopPropagation() this.setState({isOpen: !this.state.isOpen}) } - public handleFilterText(e) { - e.stopPropagation() - this.setState({ - filterText: e.target.value, - }) + private get tagItemLabel(): string { + const {tagKey, tagValues} = this.props + return `${tagKey} — ${tagValues.length}` } - public handleEscape(e) { - if (e.key !== 'Escape') { - return - } - - e.stopPropagation() - this.setState({ - filterText: '', - }) - } - - public handleInputClick(e: MouseEvent) { - e.stopPropagation() - } - - public renderTagValues() { + private get renderTagValues(): JSX.Element[] | JSX.Element { const {tagValues} = this.props if (!tagValues || !tagValues.length) { - return
no tag values
+ return
No tag values
} - const filterText = this.state.filterText.toLowerCase() - const filtered = tagValues.filter(v => v.toLowerCase().includes(filterText)) - - return ( -
-
- - + return tagValues.map(v => { + return ( +
+ {v}
- {filtered.map(v => { - return ( -
- {v} -
- ) - })} -
- ) + ) + }) } - public render() { - const {tagKey, tagValues} = this.props + private get className(): string { const {isOpen} = this.state - const tagItemLabel = `${tagKey} — ${tagValues.length}` - - return ( -
-
- -
- {tagItemLabel} - -
- {isOpen ? this.renderTagValues() : null} -
- ) + return classnames('ifql-schema-tree', {expanded: isOpen}) } } diff --git a/ui/src/style/components/time-machine/ifql-explorer.scss b/ui/src/style/components/time-machine/ifql-explorer.scss index 05ae155ef8..0f5067032b 100644 --- a/ui/src/style/components/time-machine/ifql-explorer.scss +++ b/ui/src/style/components/time-machine/ifql-explorer.scss @@ -3,47 +3,92 @@ ---------------------------------------------------------------------------- */ -$ifql-tree-indent: 30px; +$ifql-tree-indent: 28px; .ifql-schema-explorer { width: 100%; height: 100%; + background-color: $g2-kevlar; } .ifql-schema-tree { display: flex; flex-direction: column; align-items: stretch; + padding-left: 0; - & > .ifql-schema-tree { + > .ifql-schema-tree { padding-left: $ifql-tree-indent; } - - .ifql-schema-item + & { - display: none; - } - - .expanded .ifql-schema-item + & { - display: flex; - } } -.ifql-schema-item { - position: relative; - height: 30px; +.ifql-schema-tree__empty { + height: $ifql-tree-indent; display: flex; align-items: center; padding: 0 11px; - padding-left: 32px; + font-size: 12px; + font-weight: 600; + color: $g8-storm; + font-style: italic; +} - > span { +.ifql-schema-item { + @include no-user-select(); + position: relative; + height: $ifql-tree-indent; + display: flex; + align-items: center; + padding: 0 11px; + padding-left: $ifql-tree-indent; + font-size: 12px; + font-weight: 600; + color: $g11-sidewalk; + transition: color 0.25s ease, background-color 0.25s ease; + + > span.icon { position: absolute; top: 50%; - left: 14px; + left: $ifql-tree-indent / 2; transform: translate(-50%, -50%); transition: transform 0.25s ease; } - + + &:hover { + color: $g15-platinum; + cursor: pointer; + background-color: $g4-onyx; + } + + .expanded > & { + color: $c-pool; + background-color: $g3-castle; + + > span.icon { + transform: translate(-50%, -50%) rotate(90deg); + } + } + + &.readonly, + &.readonly:hover { + background-color: transparent; + color: $g11-sidewalk; + cursor: default; + } } +/* + Controls + ---------------------------------------------------------------------------- +*/ +.ifql-schema--controls { + padding: 11px; + display: flex; + align-items: center; + justify-content: space-between; +} +.ifql-schema--filter { + flex: 1 0 0; + margin-right: 4px; +} diff --git a/ui/src/style/pages/time-machine.scss b/ui/src/style/pages/time-machine.scss index 308c8128ed..2b6c49f5b7 100644 --- a/ui/src/style/pages/time-machine.scss +++ b/ui/src/style/pages/time-machine.scss @@ -5,6 +5,6 @@ @import '../components/time-machine/ifql-editor'; @import '../components/time-machine/ifql-builder'; -// @import '../components/time-machine/ifql-explorer'; +@import '../components/time-machine/ifql-explorer'; @import '../components/time-machine/visualization'; @import '../components/time-machine/add-func-button'; \ No newline at end of file From a5fe99ab52e2bf3f717a342c4fcf57fe66f42680 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 14:35:08 -0700 Subject: [PATCH 065/104] Flatten groupbys when parsing data for csv download in dashboard --- ui/src/shared/components/AutoRefresh.tsx | 4 ++- ui/src/shared/components/Layout.js | 6 ++-- ui/src/shared/components/LayoutCell.js | 8 ++--- ui/src/shared/parsing/resultsToCSV.js | 37 ++++++++++++------------ 4 files changed, 28 insertions(+), 27 deletions(-) diff --git a/ui/src/shared/components/AutoRefresh.tsx b/ui/src/shared/components/AutoRefresh.tsx index cfbb2f5a53..53ee8381af 100644 --- a/ui/src/shared/components/AutoRefresh.tsx +++ b/ui/src/shared/components/AutoRefresh.tsx @@ -4,6 +4,7 @@ import _ from 'lodash' import {fetchTimeSeries} from 'src/shared/apis/query' import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series' import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series' +import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' interface Axes { bounds: { @@ -129,8 +130,9 @@ const AutoRefresh = ( isFetching: false, }) + const {data} = timeSeriesToTableGraph(newSeries) if (grabDataForDownload) { - grabDataForDownload(newSeries) + grabDataForDownload(data) } } catch (err) { console.error(err) diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index b38adbdef2..aa441602b2 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -23,7 +23,7 @@ const getSource = (cell, source, sources, defaultSource) => { @ErrorHandling class LayoutState extends Component { state = { - celldata: [], + celldata: [[]], } grabDataForDownload = celldata => { @@ -122,7 +122,7 @@ const Layout = ( ) -const {arrayOf, bool, func, number, shape, string} = PropTypes +const {array, arrayOf, bool, func, number, shape, string} = PropTypes Layout.contextTypes = { source: shape(), @@ -200,7 +200,7 @@ LayoutState.propTypes = {...propTypes} Layout.propTypes = { ...propTypes, grabDataForDownload: func, - celldata: arrayOf(shape()), + celldata: arrayOf(array), } export default LayoutState diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 7bbb05cba0..6f66844b6a 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -8,9 +8,9 @@ import LayoutCellMenu from 'shared/components/LayoutCellMenu' import LayoutCellHeader from 'shared/components/LayoutCellHeader' import {notify} from 'src/shared/actions/notifications' import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications' -import {dashboardtoCSV} from 'shared/parsing/resultsToCSV' import download from 'src/external/download.js' import {ErrorHandling} from 'src/shared/decorators/errors' +import {dataToCSV} from 'src/shared/parsing/resultsToCSV' @ErrorHandling class LayoutCell extends Component { @@ -26,7 +26,7 @@ class LayoutCell extends Component { const joinedName = cell.name.split(' ').join('_') const {celldata} = this.props try { - download(dashboardtoCSV(celldata), `${joinedName}.csv`, 'text/plain') + download(dataToCSV(celldata), `${joinedName}.csv`, 'text/plain') } catch (error) { notify(notifyCSVDownloadFailed()) console.error(error) @@ -79,7 +79,7 @@ class LayoutCell extends Component { } } -const {arrayOf, bool, func, node, number, shape, string} = PropTypes +const {array, arrayOf, bool, func, node, number, shape, string} = PropTypes LayoutCell.propTypes = { cell: shape({ @@ -96,7 +96,7 @@ LayoutCell.propTypes = { onSummonOverlayTechnologies: func, isEditable: bool, onCancelEditCell: func, - celldata: arrayOf(shape()), + celldata: arrayOf(array), } export default LayoutCell diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/resultsToCSV.js index c3d3fdd020..c73698fd14 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/resultsToCSV.js @@ -1,5 +1,6 @@ import _ from 'lodash' import moment from 'moment' +import {map} from 'fast.js' export const formatDate = timestamp => moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A') @@ -30,24 +31,22 @@ export const resultsToCSV = results => { return {flag: 'ok', name, CSVString} } -export const dashboardtoCSV = data => { - const columnNames = _.flatten( - data.map(r => _.get(r, 'results[0].series[0].columns', [])) - ) - const timeIndices = columnNames - .map((e, i) => (e === 'time' ? i : -1)) - .filter(e => e >= 0) - - let values = data.map(r => _.get(r, 'results[0].series[0].values', [])) - values = _.unzip(values).map(v => _.flatten(v)) - if (timeIndices) { - values.map(v => { - timeIndices.forEach(i => (v[i] = formatDate(v[i]))) - return v - }) +export const dataToCSV = ([titleRow, ...valueRows]) => { + if (_.isEmpty(titleRow)) { + return '' } - const CSVString = [columnNames.join(',')] - .concat(values.map(v => v.join(','))) - .join('\n') - return CSVString + if (_.isEmpty(valueRows)) { + return ['date', titleRow.slice(1)].join(',') + } + if (titleRow[0] === 'time') { + const titlesString = ['date', titleRow.slice(1)].join(',') + + const valuesString = map(valueRows, ([timestamp, ...values]) => [ + [formatDate(timestamp), ...values].join(','), + ]).join('\n') + return `${titlesString}\n${valuesString}` + } + const allRows = [titleRow, ...valueRows] + const allRowsStringArray = map(allRows, r => r.join(',')) + return allRowsStringArray.join('\n') } From dad56782f9fb5bdf7573c0e40b9e00256da1c0b2 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 14:35:29 -0700 Subject: [PATCH 066/104] Bestow csv download ability on table graphs --- ui/src/shared/components/RefreshingGraph.js | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/shared/components/RefreshingGraph.js b/ui/src/shared/components/RefreshingGraph.js index 2e4d1586a2..c8369e960e 100644 --- a/ui/src/shared/components/RefreshingGraph.js +++ b/ui/src/shared/components/RefreshingGraph.js @@ -118,6 +118,7 @@ const RefreshingGraph = ({ decimalPlaces={decimalPlaces} editQueryStatus={editQueryStatus} resizerTopHeight={resizerTopHeight} + grabDataForDownload={grabDataForDownload} handleSetHoverTime={handleSetHoverTime} isInCEO={isInCEO} /> From d4dea24876d474284b72f01a555fc298c8e5fefd Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 4 May 2018 14:40:05 -0700 Subject: [PATCH 067/104] Add "tree" structure design to IFQL schema explorer --- ui/src/ifql/components/DatabaseListItem.tsx | 3 +- ui/src/ifql/components/TagListItem.tsx | 11 +- .../time-machine/ifql-explorer.scss | 105 ++++++++++++++++-- 3 files changed, 106 insertions(+), 13 deletions(-) diff --git a/ui/src/ifql/components/DatabaseListItem.tsx b/ui/src/ifql/components/DatabaseListItem.tsx index 17ac4c948f..7c81291533 100644 --- a/ui/src/ifql/components/DatabaseListItem.tsx +++ b/ui/src/ifql/components/DatabaseListItem.tsx @@ -26,8 +26,9 @@ class DatabaseListItem extends PureComponent { return (
- +
{db} + Bucket
{this.state.isOpen && }
diff --git a/ui/src/ifql/components/TagListItem.tsx b/ui/src/ifql/components/TagListItem.tsx index 76599367b7..2b224bc474 100644 --- a/ui/src/ifql/components/TagListItem.tsx +++ b/ui/src/ifql/components/TagListItem.tsx @@ -26,8 +26,9 @@ class TagListItem extends PureComponent { return (
- +
{this.tagItemLabel} + Tag Key
{isOpen && this.renderTagValues}
@@ -40,8 +41,8 @@ class TagListItem extends PureComponent { } private get tagItemLabel(): string { - const {tagKey, tagValues} = this.props - return `${tagKey} — ${tagValues.length}` + const {tagKey} = this.props + return `${tagKey}` } private get renderTagValues(): JSX.Element[] | JSX.Element { @@ -52,7 +53,7 @@ class TagListItem extends PureComponent { return tagValues.map(v => { return ( -
+
{v}
) @@ -61,7 +62,7 @@ class TagListItem extends PureComponent { private get className(): string { const {isOpen} = this.state - return classnames('ifql-schema-tree', {expanded: isOpen}) + return classnames('ifql-schema-tree ifql-tree-node', {expanded: isOpen}) } } diff --git a/ui/src/style/components/time-machine/ifql-explorer.scss b/ui/src/style/components/time-machine/ifql-explorer.scss index 0f5067032b..502621aa32 100644 --- a/ui/src/style/components/time-machine/ifql-explorer.scss +++ b/ui/src/style/components/time-machine/ifql-explorer.scss @@ -3,15 +3,18 @@ ---------------------------------------------------------------------------- */ -$ifql-tree-indent: 28px; +$ifql-tree-indent: 26px; +$ifql-tree-line: 2px; .ifql-schema-explorer { width: 100%; height: 100%; background-color: $g2-kevlar; + min-width: 200px; } .ifql-schema-tree { + position: relative; display: flex; flex-direction: column; align-items: stretch; @@ -33,6 +36,30 @@ $ifql-tree-indent: 28px; font-style: italic; } +.ifql-schema-item-toggle { + width: $ifql-tree-indent; + height: $ifql-tree-indent; + position: relative; + + // Plus Sign + &:before, + &:after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background-color: $g11-sidewalk; + width: $ifql-tree-indent / 3; + height: $ifql-tree-line; + transition: transform 0.25s ease, background-color 0.25s ease; + } + // Vertical Line + &:after { + transform: translate(-50%, -50%) rotate(90deg); + } +} + .ifql-schema-item { @include no-user-select(); position: relative; @@ -40,10 +67,11 @@ $ifql-tree-indent: 28px; display: flex; align-items: center; padding: 0 11px; - padding-left: $ifql-tree-indent; + padding-left: 0; font-size: 12px; font-weight: 600; color: $g11-sidewalk; + white-space: nowrap; transition: color 0.25s ease, background-color 0.25s ease; > span.icon { @@ -51,32 +79,80 @@ $ifql-tree-indent: 28px; top: 50%; left: $ifql-tree-indent / 2; transform: translate(-50%, -50%); - transition: transform 0.25s ease; } &:hover { - color: $g15-platinum; + color: $g17-whisper; cursor: pointer; background-color: $g4-onyx; + + .ifql-schema-item-toggle:before, + .ifql-schema-item-toggle:after { + background-color: $g17-whisper; + } } .expanded > & { color: $c-pool; - background-color: $g3-castle; - > span.icon { - transform: translate(-50%, -50%) rotate(90deg); + .ifql-schema-item-toggle:before, + .ifql-schema-item-toggle:after { + background-color: $c-pool; + } + .ifql-schema-item-toggle:before { + transform: translate(-50%, -50%) rotate(-90deg); + width: $ifql-tree-line; + } + .ifql-schema-item-toggle:after { + transform: translate(-50%, -50%) rotate(0deg); + } + + &:hover { + color: $c-laser; + + .ifql-schema-item-toggle:before, + .ifql-schema-item-toggle:after { + background-color: $c-laser; + } } } &.readonly, &.readonly:hover { + padding-left: $ifql-tree-indent + 8px; background-color: transparent; color: $g11-sidewalk; cursor: default; } } +/* Tree Node Lines */ +.ifql-tree-node:before, +.ifql-tree-node:after { + content: ''; + background-color: $g4-onyx; + position: absolute; +} + +// Vertical Line +.ifql-tree-node:before { + top: 0; + left: $ifql-tree-indent / 2; + width: $ifql-tree-line; + height: 100%; +} +.ifql-tree-node:last-child:before { + height: $ifql-tree-indent / 2; +} + +// Horizontal Line +.ifql-tree-node:after { + top: $ifql-tree-indent / 2; + left: $ifql-tree-indent / 2; + width: $ifql-tree-indent / 2; + height: $ifql-tree-line; +} + /* Controls ---------------------------------------------------------------------------- @@ -92,3 +168,18 @@ $ifql-tree-indent: 28px; flex: 1 0 0; margin-right: 4px; } + + + +// Hints +.ifql-schema-type { + color: $g11-sidewalk; + display: inline-block; + margin-left: 8px; + opacity: 0; + transition: opacity 0.25s ease; + + .ifql-schema-item:hover & { + opacity: 1; + } +} From 5b55cc4fa735de3814cf6cec015f6bfa4e547598 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 4 May 2018 15:26:45 -0700 Subject: [PATCH 068/104] First pass at highlighting variable syntax --- ui/src/ifql/components/BodyBuilder.tsx | 30 +++++++++++++++++-- .../components/time-machine/ifql-builder.scss | 13 ++++++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index dc327f0975..3904c9a2e3 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -22,7 +22,9 @@ class BodyBuilder extends PureComponent { if (d.funcs) { return (
-
{d.name}
+
+ {d.name} +
{ return (
-
{b.source}
+
+ {this.colorVariableSyntax(b.source)} +
) }) @@ -55,6 +59,28 @@ class BodyBuilder extends PureComponent { return
{_.flatten(bodybuilder)}
} + private colorVariableSyntax = (varString: string) => { + const split = varString.split('=') + const varName = split[0].substring(0, split[0].length - 1) + const varValue = split[1].substring(1) + + const valueIsString = varValue.endsWith('"') + + return ( + <> + {varName} + {' = '} + + {varValue} + + + ) + } + private get funcNames() { return this.props.suggestions.map(f => f.name) } diff --git a/ui/src/style/components/time-machine/ifql-builder.scss b/ui/src/style/components/time-machine/ifql-builder.scss index bdb0ba17d1..7731e73cd9 100644 --- a/ui/src/style/components/time-machine/ifql-builder.scss +++ b/ui/src/style/components/time-machine/ifql-builder.scss @@ -37,14 +37,23 @@ $ifql-arg-min-width: 120px; } } -.variable-name { +.variable-string { @extend %ifql-node; - color: $c-laser; + color: $g11-sidewalk; line-height: $ifql-node-height; white-space: nowrap; background-color: $g3-castle; @include no-user-select(); } +.variable-name { + color: $c-pool; +} +.variable-value--string { + color: $c-honeydew +} +.variable-value--number { + color: $c-neutrino; +} .func-node { @extend %ifql-node; From 87cd3fa9e30157ac088d9390e077ec46ee628f34 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 15:54:57 -0700 Subject: [PATCH 069/104] Prevent data transformation if no csv download option --- ui/src/shared/components/AutoRefresh.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/AutoRefresh.tsx b/ui/src/shared/components/AutoRefresh.tsx index 53ee8381af..32c09ccb5f 100644 --- a/ui/src/shared/components/AutoRefresh.tsx +++ b/ui/src/shared/components/AutoRefresh.tsx @@ -130,8 +130,8 @@ const AutoRefresh = ( isFetching: false, }) - const {data} = timeSeriesToTableGraph(newSeries) if (grabDataForDownload) { + const {data} = timeSeriesToTableGraph(newSeries) grabDataForDownload(data) } } catch (err) { From 9ead3ee1a9a0b3c31ffc2c4a16e28201d2eae9e4 Mon Sep 17 00:00:00 2001 From: Alex P Date: Fri, 4 May 2018 16:04:00 -0700 Subject: [PATCH 070/104] Color builder arguments based on type --- ui/src/ifql/components/FuncNode.tsx | 46 ++++++++++++++++--- .../components/time-machine/ifql-builder.scss | 8 +++- 2 files changed, 46 insertions(+), 8 deletions(-) diff --git a/ui/src/ifql/components/FuncNode.tsx b/ui/src/ifql/components/FuncNode.tsx index 747d630d40..36e8b695e7 100644 --- a/ui/src/ifql/components/FuncNode.tsx +++ b/ui/src/ifql/components/FuncNode.tsx @@ -1,4 +1,5 @@ import React, {PureComponent, MouseEvent} from 'react' +import uuid from 'uuid' import FuncArgs from 'src/ifql/components/FuncArgs' import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' @@ -46,7 +47,7 @@ export default class FuncNode extends PureComponent { onMouseLeave={this.handleMouseLeave} >
{func.name}
-
{this.stringifyArgs}
+ {this.coloredSyntaxArgs} {isExpanded && ( { ) } - private get stringifyArgs(): string { + private get coloredSyntaxArgs(): JSX.Element { const { func: {args}, } = this.props @@ -70,15 +71,46 @@ export default class FuncNode extends PureComponent { return } - return args.reduce((acc, arg, i) => { + const coloredSyntax = args.map((arg, i): JSX.Element => { if (!arg.value) { - return acc + return } - const separator = i === 0 ? '' : ', ' + const separator = i === 0 ? null : ', ' - return `${acc}${separator}${arg.key}: ${arg.value}` - }, '') + return ( + + {separator} + {arg.key}: {this.colorArgType(`${arg.value}`, arg.type)} + + ) + }) + + return
{coloredSyntax}
+ } + + private colorArgType = (argument: string, type: string): JSX.Element => { + switch (type) { + case 'time': + case 'number': + case 'period': + case 'duration': + case 'array': { + return {argument} + } + case 'bool': { + return {argument} + } + case 'string': { + return "{argument}" + } + case 'invalid': { + return {argument} + } + default: { + return {argument} + } + } } private handleDelete = (): void => { diff --git a/ui/src/style/components/time-machine/ifql-builder.scss b/ui/src/style/components/time-machine/ifql-builder.scss index 7731e73cd9..b9b187deb9 100644 --- a/ui/src/style/components/time-machine/ifql-builder.scss +++ b/ui/src/style/components/time-machine/ifql-builder.scss @@ -51,9 +51,15 @@ $ifql-arg-min-width: 120px; .variable-value--string { color: $c-honeydew } +.variable-value--boolean { + color: $c-viridian +} .variable-value--number { color: $c-neutrino; } +.variable-value--invalid { + color: $c-dreamsicle; +} .func-node { @extend %ifql-node; @@ -97,7 +103,7 @@ $ifql-arg-min-width: 120px; } .func-node--preview { - color: $g13-mist; + color: $g11-sidewalk; margin-left: 4px; .func-node:hover & { From 2629a2638bf38d584065e5fddf9e53777de4f5cc Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 16:32:28 -0700 Subject: [PATCH 071/104] Use Flatten groupbys when parsing data for csv download in data explorer --- ui/src/data_explorer/components/VisHeader.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 79638222a3..13f0a39565 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -4,23 +4,21 @@ import classnames from 'classnames' import _ from 'lodash' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' -import {resultsToCSV} from 'src/shared/parsing/resultsToCSV.js' +import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' +import {dataToCSV} from 'src/shared/parsing/resultsToCSV' import download from 'src/external/download.js' import {TEMPLATES} from 'src/shared/constants' -const getCSV = (query, errorThrown) => async () => { +const getDataForCSV = (query, errorThrown) => async () => { try { - const {results} = await fetchTimeSeriesAsync({ + const response = await fetchTimeSeriesAsync({ source: query.host, query, tempVars: TEMPLATES, }) - const {flag, name, CSVString} = resultsToCSV(results) - if (flag === 'no_data') { - errorThrown('no data', 'There are no data to download.') - return - } - download(CSVString, `${name}.csv`, 'text/plain') + const {data} = timeSeriesToTableGraph([{response}]) + + download(dataToCSV(data), `${''}.csv`, 'text/plain') } catch (error) { errorThrown(error, 'Unable to download .csv file') console.error(error) @@ -46,7 +44,7 @@ const VisHeader = ({views, view, onToggleView, query, errorThrown}) => ( {query ? (
.csv From 4ab0a73491adc4b26e5e0df65174f7ee44efec02 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 16:50:34 -0700 Subject: [PATCH 072/104] Removed unused functions, rename files, and add tests --- ui/src/data_explorer/components/VisHeader.js | 2 +- ui/src/shared/components/LayoutCell.js | 2 +- .../parsing/{resultsToCSV.js => dataToCSV.js} | 26 ----- ui/test/shared/parsing/dataToCSV.test.js | 46 ++++++++ ui/test/shared/parsing/resultsToCSV.test.js | 105 ------------------ 5 files changed, 48 insertions(+), 133 deletions(-) rename ui/src/shared/parsing/{resultsToCSV.js => dataToCSV.js} (52%) create mode 100644 ui/test/shared/parsing/dataToCSV.test.js delete mode 100644 ui/test/shared/parsing/resultsToCSV.test.js diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 13f0a39565..7c05417e8c 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -5,7 +5,7 @@ import _ from 'lodash' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' -import {dataToCSV} from 'src/shared/parsing/resultsToCSV' +import {dataToCSV} from 'src/shared/parsing/dataToCSV' import download from 'src/external/download.js' import {TEMPLATES} from 'src/shared/constants' diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index 6f66844b6a..f1658e8ac7 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -10,7 +10,7 @@ import {notify} from 'src/shared/actions/notifications' import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications' import download from 'src/external/download.js' import {ErrorHandling} from 'src/shared/decorators/errors' -import {dataToCSV} from 'src/shared/parsing/resultsToCSV' +import {dataToCSV} from 'src/shared/parsing/dataToCSV' @ErrorHandling class LayoutCell extends Component { diff --git a/ui/src/shared/parsing/resultsToCSV.js b/ui/src/shared/parsing/dataToCSV.js similarity index 52% rename from ui/src/shared/parsing/resultsToCSV.js rename to ui/src/shared/parsing/dataToCSV.js index c73698fd14..8edbd4afca 100644 --- a/ui/src/shared/parsing/resultsToCSV.js +++ b/ui/src/shared/parsing/dataToCSV.js @@ -5,32 +5,6 @@ import {map} from 'fast.js' export const formatDate = timestamp => moment(timestamp).format('M/D/YYYY h:mm:ss.SSSSSSSSS A') -export const resultsToCSV = results => { - if (!_.get(results, ['0', 'series', '0'])) { - return {flag: 'no_data', name: '', CSVString: ''} - } - - const {name, columns, values} = _.get(results, ['0', 'series', '0']) - - if (columns[0] === 'time') { - const [, ...cols] = columns - const CSVString = [['date', ...cols].join(',')] - .concat( - values.map(([timestamp, ...measurements]) => - // MS Excel format - [formatDate(timestamp), ...measurements].join(',') - ) - ) - .join('\n') - return {flag: 'ok', name, CSVString} - } - - const CSVString = [columns.join(',')] - .concat(values.map(row => row.join(','))) - .join('\n') - return {flag: 'ok', name, CSVString} -} - export const dataToCSV = ([titleRow, ...valueRows]) => { if (_.isEmpty(titleRow)) { return '' diff --git a/ui/test/shared/parsing/dataToCSV.test.js b/ui/test/shared/parsing/dataToCSV.test.js new file mode 100644 index 0000000000..d46e620cf1 --- /dev/null +++ b/ui/test/shared/parsing/dataToCSV.test.js @@ -0,0 +1,46 @@ +import {dataToCSV, formatDate} from 'shared/parsing/dataToCSV' +import moment from 'moment' + +describe('formatDate', () => { + it('converts timestamp to an excel compatible date string', () => { + const timestamp = 1000000000000 + const result = formatDate(timestamp) + expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe( + timestamp + ) + }) +}) + +describe('dataToCSV', () => { + it('parses data, an array of arrays, to a csv string', () => { + const data = [[1, 2], [3, 4], [5, 6], [7, 8]] + const returned = dataToCSV(data) + const expected = `1,2\n3,4\n5,6\n7,8` + + expect(returned).toEqual(expected) + }) + + it('converts values to dates if title of first column is time.', () => { + const data = [ + ['time', 'something'], + [1505262600000, 0.06163066773148772], + [1505264400000, 2.616484718180463], + [1505266200000, 1.6174323943535571], + ] + const returned = dataToCSV(data) + const expected = `date,something\n${formatDate( + 1505262600000 + )},0.06163066773148772\n${formatDate( + 1505264400000 + )},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571` + + expect(returned).toEqual(expected) + }) + + it('returns an empty string if data is empty', () => { + const data = [[]] + const returned = dataToCSV(data) + const expected = '' + expect(returned).toEqual(expected) + }) +}) diff --git a/ui/test/shared/parsing/resultsToCSV.test.js b/ui/test/shared/parsing/resultsToCSV.test.js deleted file mode 100644 index fdc84ace22..0000000000 --- a/ui/test/shared/parsing/resultsToCSV.test.js +++ /dev/null @@ -1,105 +0,0 @@ -import { - resultsToCSV, - formatDate, - dashboardtoCSV, -} from 'shared/parsing/resultsToCSV' -import moment from 'moment' - -describe('formatDate', () => { - it('converts timestamp to an excel compatible date string', () => { - const timestamp = 1000000000000 - const result = formatDate(timestamp) - expect(moment(result, 'M/D/YYYY h:mm:ss.SSSSSSSSS A').valueOf()).toBe( - timestamp - ) - }) -}) - -describe('resultsToCSV', () => { - it('parses results, a time series data structure, to an object with name and CSVString keys', () => { - const results = [ - { - statement_id: 0, - series: [ - { - name: 'procstat', - columns: ['time', 'mean_cpu_usage'], - values: [ - [1505262600000, 0.06163066773148772], - [1505264400000, 2.616484718180463], - [1505266200000, 1.6174323943535571], - ], - }, - ], - }, - ] - const response = resultsToCSV(results) - const expected = { - flag: 'ok', - name: 'procstat', - CSVString: `date,mean_cpu_usage\n${formatDate( - 1505262600000 - )},0.06163066773148772\n${formatDate( - 1505264400000 - )},2.616484718180463\n${formatDate(1505266200000)},1.6174323943535571`, - } - expect(Object.keys(response).sort()).toEqual( - ['flag', 'name', 'CSVString'].sort() - ) - expect(response.flag).toBe(expected.flag) - expect(response.name).toBe(expected.name) - expect(response.CSVString).toBe(expected.CSVString) - }) -}) - -describe('dashboardtoCSV', () => { - it('parses the array of timeseries data displayed by the dashboard cell to a CSVstring for download', () => { - const data = [ - { - results: [ - { - statement_id: 0, - series: [ - { - name: 'procstat', - columns: ['time', 'mean_cpu_usage'], - values: [ - [1505262600000, 0.06163066773148772], - [1505264400000, 2.616484718180463], - [1505266200000, 1.6174323943535571], - ], - }, - ], - }, - ], - }, - { - results: [ - { - statement_id: 0, - series: [ - { - name: 'procstat', - columns: ['not-time', 'mean_cpu_usage'], - values: [ - [1505262600000, 0.06163066773148772], - [1505264400000, 2.616484718180463], - [1505266200000, 1.6174323943535571], - ], - }, - ], - }, - ], - }, - ] - const result = dashboardtoCSV(data) - const expected = `time,mean_cpu_usage,not-time,mean_cpu_usage\n${formatDate( - 1505262600000 - )},0.06163066773148772,1505262600000,0.06163066773148772\n${formatDate( - 1505264400000 - )},2.616484718180463,1505264400000,2.616484718180463\n${formatDate( - 1505266200000 - )},1.6174323943535571,1505266200000,1.6174323943535571` - expect(result).toBe(expected) - }) -}) From 50cb45857fe807a773528082d5c88dac22def3c8 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Fri, 4 May 2018 17:26:02 -0700 Subject: [PATCH 073/104] Add a filename with db.rp.measurement... to csvs from data explorer --- ui/src/data_explorer/components/VisHeader.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ui/src/data_explorer/components/VisHeader.js b/ui/src/data_explorer/components/VisHeader.js index 7c05417e8c..615608d6a7 100644 --- a/ui/src/data_explorer/components/VisHeader.js +++ b/ui/src/data_explorer/components/VisHeader.js @@ -2,6 +2,7 @@ import React from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' import _ from 'lodash' +import moment from 'moment' import {fetchTimeSeriesAsync} from 'shared/actions/timeSeries' import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' @@ -17,8 +18,13 @@ const getDataForCSV = (query, errorThrown) => async () => { tempVars: TEMPLATES, }) const {data} = timeSeriesToTableGraph([{response}]) + const db = _.get(query, ['queryConfig', 'database'], '') + const rp = _.get(query, ['queryConfig', 'retentionPolicy'], '') + const measurement = _.get(query, ['queryConfig', 'measurement'], '') - download(dataToCSV(data), `${''}.csv`, 'text/plain') + const timestring = moment().format('YYYY-MM-DD-HH-mm') + const name = `${db}.${rp}.${measurement}.${timestring}` + download(dataToCSV(data), `${name}.csv`, 'text/plain') } catch (error) { errorThrown(error, 'Unable to download .csv file') console.error(error) From 478a68f5245362b172c208866d49ac8995317685 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 7 May 2018 09:36:41 -0700 Subject: [PATCH 074/104] Remove function icons, add "Collapse" icon Also replace function icon with plus --- ui/src/ifql/components/FuncSelector.tsx | 2 +- ui/src/ifql/components/SchemaExplorer.tsx | 2 +- ui/src/style/fonts/icomoon.eot | Bin 13720 -> 13548 bytes ui/src/style/fonts/icomoon.svg | 3 +-- ui/src/style/fonts/icomoon.ttf | Bin 13556 -> 13384 bytes ui/src/style/fonts/icomoon.woff | Bin 13632 -> 13460 bytes ui/src/style/fonts/icomoon.woff2 | Bin 6808 -> 6632 bytes ui/src/style/fonts/icon-font.scss | 3 +-- 8 files changed, 4 insertions(+), 6 deletions(-) diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 35dfe9ea9f..7ed896d792 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -55,7 +55,7 @@ export class FuncSelector extends PureComponent { onClick={this.handleOpenList} tabIndex={0} > - + )}
diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx index 62db36dffe..8bbc32ba74 100644 --- a/ui/src/ifql/components/SchemaExplorer.tsx +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -23,7 +23,7 @@ class SchemaExplorer extends PureComponent { disabled={true} title="Collapse YO tree" > - +
diff --git a/ui/src/style/fonts/icomoon.eot b/ui/src/style/fonts/icomoon.eot index 233e332b9ca7f272ac033ab87b2d1a8ef7c8d60e..6a466d0f50adc72382b1d36a04b327f8336b326a 100755 GIT binary patch delta 741 zcmY*XO-NKx6h7z9%zHCW$NOfScOQs7Z6;^3!sh4sCd|e*nL)V-gK$wyW*GiPAI(6 z0(2BSX}7+zoUlKqLWQmyuE>R%!b0tMvF_0*0ILURn(XV%UJaa88UW%?0l?~`f?V&q zM0}0-fxf}q*!Ld+H$cKmd~0C1C);tlH3yI+3bzKcVhH_otTo2nFi3hn?jG+`_rO@yP7a5Q!Z36W!%Akd)=R7&B0hQ`Ldqf`n8VY3e7fRNQLLU>jOyEIJ=e_rGz^g3N_( z_L6hnsE*b-Hu9_Lxy8A?VKh=Afm0n_*9=1|<-M}(CH>OGKh4giocWCul`rk+%Bf2L z#pji2R*a}0_SYtsSMyWC--p^m^O~tm2}87qA#qPv^>)gpj|Can2F1ArF!}}6#h8ZS5-Mm3me2~-+6P*p;GrTY zDk8MGs24>~LeWA&5D}z$5cE(~5X3{&iVD7vS}I8EMDXUp!|>1deZ%+v-!Sv(;Kc#% zVhVtT2`R7GfT0R#d0x>0!<2gA1sEuOhXYZ@M))UQ!i)GT{)|835BNQPhv#wGnzqgw zhN0>6`fL3bHGriF6f0dt(#$=SmBt~svSKquG!H!5B@P7*>ad}nx2muQ84U`&867BLg`9#00r)^s_)@*kic0s*T33WYi&L zHdgMS9i|;5l5m)e*Q;{9%&$`YNy`%ap+Y>f%hYou9112qsX^jbX?hA7eWV1lObktiFpxf-%M#yaArkcLHpLk?dtR0-A{eY z7Ax*5+Mzx93cu{VN;T6fF==lzX}OJvRR_AD*Ri%v@reqe)^CrK7)$FDCcIISE^;9*&l~(rp<384oD;W-wGfl}^_!~Muu|@y@ diff --git a/ui/src/style/fonts/icomoon.svg b/ui/src/style/fonts/icomoon.svg index 7c65529bfe..7673d95bd5 100755 --- a/ui/src/style/fonts/icomoon.svg +++ b/ui/src/style/fonts/icomoon.svg @@ -23,11 +23,10 @@ - + - diff --git a/ui/src/style/fonts/icomoon.ttf b/ui/src/style/fonts/icomoon.ttf index 889175549ab9b43746c65f7237865fe62c84d381..6baf37bbdb2c4114974e48db587276c6644d4569 100755 GIT binary patch delta 727 zcmY*XPe>GT6n^iw>&)!v_M2UIej{R|?&fZGu$|p?Hq6#8nL!x@L3k*(QaAF?g1d`d z+UOu1B#fm)5aFS2k->s=>EcPmD55`?B0;GLk%}aV$lk0PG<@*p``-7x_h9D7rTNTM zb1`&!1^^oX;P+s9EK5}bV0BcL!J#Vy<;Ht?;!OZ;lbQbXMc;9?1t9v6`gVo{xyf~w z_zLkonc>{{m#;oIfTa-M7#h8h?m60-1BelYtHbH>EF425evNq3NP4)x?oO{~lZ0tH zVj?>_mID{HAhGw>liTPo_2T6SGf-@ZpJH9CiB<7gd=ekUidYsO^cnqz#x*Zr=1crh zc?t8LBE$Mk8bL#jz;%F@HjG5WRxlh2h9mos5ITSsfnF+*gHoshj#9u7u`bjDA|A63 zptO^n`v> hn4&|Bh$%xex@k83MUa9`P@OA)mA+wZ`DFY7{sjQIj++1g delta 898 zcma)4O=uHA7@gT{_BYv|q&wRmY6vmqfI@Ho@k47zWdV2}bfVqtzm8n+gNc-Dumg5AL)?39kd$`tbP*+cBIJLt}G z8h4wo;1BR;g>AxJ(Gn-6tn^Tpvi!!HnXhH3V{#ak~g?&^0djF}i q4P{phVoVrs$Wd~h%#ua3+q{C{!=qNTilovch$(!-{+SyMH}P)~c(7;y diff --git a/ui/src/style/fonts/icomoon.woff b/ui/src/style/fonts/icomoon.woff index 17d2da624931a8b5f102f318b2bc8af227185f6d..b69d8bf5ccdbbadeb49c1978995a356279f4eb11 100755 GIT binary patch delta 771 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 Date: Mon, 7 May 2018 10:04:56 -0700 Subject: [PATCH 075/104] Refine autocomplete to operate as user types --- ui/package.json | 2 +- ui/src/external/codemirror.js | 622 +++++++++++++++++- ui/src/ifql/components/TimeMachineEditor.tsx | 18 + ui/src/ifql/constants/ast.ts | 155 +++++ ui/src/ifql/constants/editor.ts | 55 ++ ui/src/ifql/constants/index.ts | 160 +---- .../style/components/code-mirror-theme.scss | 181 ++++- 7 files changed, 976 insertions(+), 217 deletions(-) create mode 100644 ui/src/ifql/constants/ast.ts create mode 100644 ui/src/ifql/constants/editor.ts diff --git a/ui/package.json b/ui/package.json index 62c4e0d5ca..0fa63f482a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -38,6 +38,7 @@ }, "devDependencies": { "@types/chai": "^4.1.2", + "@types/codemirror": "^0.0.56", "@types/dygraphs": "^1.1.6", "@types/enzyme": "^3.1.9", "@types/jest": "^22.1.4", @@ -49,7 +50,6 @@ "@types/react-dnd-html5-backend": "^2.1.9", "@types/react-router": "^3.0.15", "@types/text-encoding": "^0.0.32", - "@types/codemirror": "^0.0.56", "autoprefixer": "^6.3.1", "babel-core": "^6.5.1", "babel-eslint": "6.1.2", diff --git a/ui/src/external/codemirror.js b/ui/src/external/codemirror.js index 3129a886d6..927219b32b 100644 --- a/ui/src/external/codemirror.js +++ b/ui/src/external/codemirror.js @@ -1,13 +1,13 @@ /* eslint-disable */ const CodeMirror = require('codemirror') -CodeMirror.defineSimpleMode = function(name, states) { - CodeMirror.defineMode(name, function(config) { +CodeMirror.defineSimpleMode = function (name, states) { + CodeMirror.defineMode(name, function (config) { return CodeMirror.simpleMode(config, states) }) } -CodeMirror.simpleMode = function(config, states) { +CodeMirror.simpleMode = function (config, states) { ensureState(states, 'start') const states_ = {}, meta = states.meta || {} @@ -53,10 +53,8 @@ CodeMirror.simpleMode = function(config, states) { s.persistentStates = { mode: pers.mode, spec: pers.spec, - state: - pers.state === state.localState - ? s.localState - : CodeMirror.copyState(pers.mode, pers.state), + state: pers.state === state.localState ? + s.localState : CodeMirror.copyState(pers.mode, pers.state), next: s.persistentStates, } } @@ -64,7 +62,10 @@ CodeMirror.simpleMode = function(config, states) { }, token: tokenFunction(states_, config), innerMode(state) { - return state.local && {mode: state.local.mode, state: state.localState} + return state.local && { + mode: state.local.mode, + state: state.localState + } }, indent: indentFunction(states_, meta), } @@ -127,7 +128,7 @@ function Rule(data, states) { } function tokenFunction(states, config) { - return function(stream, state) { + return function (stream, state) { if (state.pending) { const pend = state.pending.shift() if (state.pending.length === 0) { @@ -163,8 +164,8 @@ function tokenFunction(states, config) { if (matches) { if (rule.data.next) { state.state = rule.data.next - } else if (rule.data.push) { - ;(state.stack || (state.stack = [])).push(state.state) + } else if (rule.data.push) {; + (state.stack || (state.stack = [])).push(state.state) state.state = rule.data.push } else if (rule.data.pop && state.stack && state.stack.length) { state.state = state.stack.pop() @@ -187,7 +188,10 @@ function tokenFunction(states, config) { state.pending = [] for (let j = 2; j < matches.length; j++) { if (matches[j]) { - state.pending.push({text: matches[j], token: rule.token[j - 1]}) + state.pending.push({ + text: matches[j], + token: rule.token[j - 1] + }) } } stream.backUp( @@ -238,9 +242,9 @@ function enterLocalMode(config, state, spec, token) { } } } - const mode = pers - ? pers.mode - : spec.mode || CodeMirror.getMode(config, spec.spec) + const mode = pers ? + pers.mode : + spec.mode || CodeMirror.getMode(config, spec.spec) const lState = pers ? pers.state : CodeMirror.startState(mode) if (spec.persistent && !pers) { state.persistentStates = { @@ -269,7 +273,7 @@ function indexOf(val, arr) { } function indentFunction(states, meta) { - return function(state, textAfter, line) { + return function (state, textAfter, line) { if (state.local && state.local.mode.indent) { return state.local.mode.indent(state.localState, textAfter, line) } @@ -309,8 +313,14 @@ CodeMirror.defineSimpleMode('tickscript', { // The start state contains the rules that are intially used start: [ // The regex matches the token, the token property contains the type - {regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string.double'}, - {regex: /'(?:[^\\]|\\.)*?(?:'|$)/, token: 'string.single'}, + { + regex: /"(?:[^\\]|\\.)*?(?:"|$)/, + token: 'string.double' + }, + { + regex: /'(?:[^\\]|\\.)*?(?:'|$)/, + token: 'string.single' + }, { regex: /(function)(\s+)([a-z$][\w$]*)/, token: ['keyword', null, 'variable-2'], @@ -321,22 +331,47 @@ CodeMirror.defineSimpleMode('tickscript', { regex: /(?:var|return|if|for|while|else|do|this|stream|batch|influxql|lambda)/, token: 'keyword', }, - {regex: /true|false|null|undefined|TRUE|FALSE/, token: 'atom'}, + { + regex: /true|false|null|undefined|TRUE|FALSE/, + token: 'atom' + }, { regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i, token: 'number', }, - {regex: /\/\/.*/, token: 'comment'}, - {regex: /\/(?:[^\\]|\\.)*?\//, token: 'variable-3'}, + { + regex: /\/\/.*/, + token: 'comment' + }, + { + regex: /\/(?:[^\\]|\\.)*?\//, + token: 'variable-3' + }, // A next property will cause the mode to move to a different state - {regex: /\/\*/, token: 'comment', next: 'comment'}, - {regex: /[-+\/*=<>!]+/, token: 'operator'}, - {regex: /[a-z$][\w$]*/, token: 'variable'}, + { + regex: /\/\*/, + token: 'comment', + next: 'comment' + }, + { + regex: /[-+\/*=<>!]+/, + token: 'operator' + }, + { + regex: /[a-z$][\w$]*/, + token: 'variable' + }, ], // The multi-line comment state. - comment: [ - {regex: /.*?\*\//, token: 'comment', next: 'start'}, - {regex: /.*/, token: 'comment'}, + comment: [{ + regex: /.*?\*\//, + token: 'comment', + next: 'start' + }, + { + regex: /.*/, + token: 'comment' + }, ], // The meta property contains global information about the mode. It // can contain properties like lineComment, which are supported by @@ -347,3 +382,536 @@ CodeMirror.defineSimpleMode('tickscript', { lineComment: '//', }, }) + +// CodeMirror Hints + +var HINT_ELEMENT_CLASS = "CodeMirror-hint"; +var ACTIVE_HINT_ELEMENT_CLASS = "CodeMirror-hint-active"; + +// This is the old interface, kept around for now to stay backwards-compatible. +CodeMirror.showHint = function (cm, getHints, options) { + if (!getHints) return cm.showHint(options); + if (options && options.async) getHints.async = true; + var newOpts = { + hint: getHints + }; + if (options) + for (var prop in options) newOpts[prop] = options[prop]; + return cm.showHint(newOpts); +}; + +CodeMirror.defineExtension("showHint", function (options) { + options = parseOptions(this, this.getCursor("start"), options); + var selections = this.listSelections() + if (selections.length > 1) return; + // By default, don't allow completion when something is selected. + // A hint function can have a `supportsSelection` property to + // indicate that it can handle selections. + if (this.somethingSelected()) { + if (!options.hint.supportsSelection) return; + // Don't try with cross-line selections + for (var i = 0; i < selections.length; i++) + if (selections[i].head.line != selections[i].anchor.line) return; + } + + if (this.state.completionActive) this.state.completionActive.close(); + var completion = this.state.completionActive = new Completion(this, options); + if (!completion.options.hint) return; + + CodeMirror.signal(this, "startCompletion", this); + completion.update(true); +}); + +function Completion(cm, options) { + this.cm = cm; + this.options = options; + this.widget = null; + this.debounce = 0; + this.tick = 0; + this.startPos = this.cm.getCursor("start"); + this.startLen = this.cm.getLine(this.startPos.line).length - this.cm.getSelection().length; + + var self = this; + cm.on("cursorActivity", this.activityFunc = function () { + self.cursorActivity(); + }); +} + +var requestAnimationFrame = window.requestAnimationFrame || function (fn) { + return setTimeout(fn, 1000 / 60); +}; +var cancelAnimationFrame = window.cancelAnimationFrame || clearTimeout; + +Completion.prototype = { + close: function () { + if (!this.active()) return; + this.cm.state.completionActive = null; + this.tick = null; + this.cm.off("cursorActivity", this.activityFunc); + + if (this.widget && this.data) CodeMirror.signal(this.data, "close"); + if (this.widget) this.widget.close(); + CodeMirror.signal(this.cm, "endCompletion", this.cm); + }, + + active: function () { + return this.cm.state.completionActive == this; + }, + + pick: function (data, i) { + var completion = data.list[i]; + if (completion.hint) completion.hint(this.cm, data, completion); + else this.cm.replaceRange(getText(completion), completion.from || data.from, + completion.to || data.to, "complete"); + CodeMirror.signal(data, "pick", completion); + this.close(); + }, + + cursorActivity: function () { + if (this.debounce) { + cancelAnimationFrame(this.debounce); + this.debounce = 0; + } + + var pos = this.cm.getCursor(), + line = this.cm.getLine(pos.line); + if (pos.line != this.startPos.line || line.length - pos.ch != this.startLen - this.startPos.ch || + pos.ch < this.startPos.ch || this.cm.somethingSelected() || + (pos.ch && this.options.closeCharacters.test(line.charAt(pos.ch - 1)))) { + this.close(); + } else { + var self = this; + this.debounce = requestAnimationFrame(function () { + self.update(); + }); + if (this.widget) this.widget.disable(); + } + }, + + update: function (first) { + if (this.tick == null) return + var self = this, + myTick = ++this.tick + fetchHints(this.options.hint, this.cm, this.options, function (data) { + if (self.tick == myTick) self.finishUpdate(data, first) + }) + }, + + finishUpdate: function (data, first) { + if (this.data) CodeMirror.signal(this.data, "update"); + + var picked = (this.widget && this.widget.picked) || (first && this.options.completeSingle); + if (this.widget) this.widget.close(); + + this.data = data; + + if (data && data.list.length) { + if (picked && data.list.length == 1) { + this.pick(data, 0); + } else { + this.widget = new Widget(this, data); + CodeMirror.signal(data, "shown"); + } + } + } +}; + +function parseOptions(cm, pos, options) { + var editor = cm.options.hintOptions; + var out = {}; + for (var prop in defaultOptions) out[prop] = defaultOptions[prop]; + if (editor) + for (var prop in editor) + if (editor[prop] !== undefined) out[prop] = editor[prop]; + if (options) + for (var prop in options) + if (options[prop] !== undefined) out[prop] = options[prop]; + if (out.hint.resolve) out.hint = out.hint.resolve(cm, pos) + return out; +} + +function getText(completion) { + if (typeof completion == "string") return completion; + else return completion.text; +} + +function buildKeyMap(completion, handle) { + var baseMap = { + Up: function () { + handle.moveFocus(-1); + }, + Down: function () { + handle.moveFocus(1); + }, + PageUp: function () { + handle.moveFocus(-handle.menuSize() + 1, true); + }, + PageDown: function () { + handle.moveFocus(handle.menuSize() - 1, true); + }, + Home: function () { + handle.setFocus(0); + }, + End: function () { + handle.setFocus(handle.length - 1); + }, + Enter: handle.pick, + Tab: handle.pick, + Esc: handle.close + }; + var custom = completion.options.customKeys; + var ourMap = custom ? {} : baseMap; + + function addBinding(key, val) { + var bound; + if (typeof val != "string") + bound = function (cm) { + return val(cm, handle); + }; + // This mechanism is deprecated + else if (baseMap.hasOwnProperty(val)) + bound = baseMap[val]; + else + bound = val; + ourMap[key] = bound; + } + if (custom) + for (var key in custom) + if (custom.hasOwnProperty(key)) + addBinding(key, custom[key]); + var extra = completion.options.extraKeys; + if (extra) + for (var key in extra) + if (extra.hasOwnProperty(key)) + addBinding(key, extra[key]); + return ourMap; +} + +function getHintElement(hintsElement, el) { + while (el && el != hintsElement) { + if (el.nodeName.toUpperCase() === "LI" && el.parentNode == hintsElement) return el; + el = el.parentNode; + } +} + +function Widget(completion, data) { + this.completion = completion; + this.data = data; + this.picked = false; + var widget = this, + cm = completion.cm; + + var hints = this.hints = document.createElement("ul"); + hints.className = "CodeMirror-hints"; + this.selectedHint = data.selectedHint || 0; + + var completions = data.list; + for (var i = 0; i < completions.length; ++i) { + var elt = hints.appendChild(document.createElement("li")), + cur = completions[i]; + var className = HINT_ELEMENT_CLASS + (i != this.selectedHint ? "" : " " + ACTIVE_HINT_ELEMENT_CLASS); + if (cur.className != null) className = cur.className + " " + className; + elt.className = className; + if (cur.render) cur.render(elt, data, cur); + else elt.appendChild(document.createTextNode(cur.displayText || getText(cur))); + elt.hintId = i; + } + + var pos = cm.cursorCoords(completion.options.alignWithWord ? data.from : null); + var left = pos.left, + top = pos.bottom, + below = true; + hints.style.left = left + "px"; + hints.style.top = top + "px"; + // If we're at the edge of the screen, then we want the menu to appear on the left of the cursor. + var winW = window.innerWidth || Math.max(document.body.offsetWidth, document.documentElement.offsetWidth); + var winH = window.innerHeight || Math.max(document.body.offsetHeight, document.documentElement.offsetHeight); + (completion.options.container || document.body).appendChild(hints); + var box = hints.getBoundingClientRect(), + overlapY = box.bottom - winH; + var scrolls = hints.scrollHeight > hints.clientHeight + 1 + var startScroll = cm.getScrollInfo(); + + if (overlapY > 0) { + var height = box.bottom - box.top, + curTop = pos.top - (pos.bottom - box.top); + if (curTop - height > 0) { // Fits above cursor + hints.style.top = (top = pos.top - height) + "px"; + below = false; + } else if (height > winH) { + hints.style.height = (winH - 5) + "px"; + hints.style.top = (top = pos.bottom - box.top) + "px"; + var cursor = cm.getCursor(); + if (data.from.ch != cursor.ch) { + pos = cm.cursorCoords(cursor); + hints.style.left = (left = pos.left) + "px"; + box = hints.getBoundingClientRect(); + } + } + } + var overlapX = box.right - winW; + if (overlapX > 0) { + if (box.right - box.left > winW) { + hints.style.width = (winW - 5) + "px"; + overlapX -= (box.right - box.left) - winW; + } + hints.style.left = (left = pos.left - overlapX) + "px"; + } + if (scrolls) + for (var node = hints.firstChild; node; node = node.nextSibling) + node.style.paddingRight = cm.display.nativeBarWidth + "px" + + cm.addKeyMap(this.keyMap = buildKeyMap(completion, { + moveFocus: function (n, avoidWrap) { + widget.changeActive(widget.selectedHint + n, avoidWrap); + }, + setFocus: function (n) { + widget.changeActive(n); + }, + menuSize: function () { + return widget.screenAmount(); + }, + length: completions.length, + close: function () { + completion.close(); + }, + pick: function () { + widget.pick(); + }, + data: data + })); + + if (completion.options.closeOnUnfocus) { + var closingOnBlur; + cm.on("blur", this.onBlur = function () { + closingOnBlur = setTimeout(function () { + completion.close(); + }, 100); + }); + cm.on("focus", this.onFocus = function () { + clearTimeout(closingOnBlur); + }); + } + + cm.on("scroll", this.onScroll = function () { + var curScroll = cm.getScrollInfo(), + editor = cm.getWrapperElement().getBoundingClientRect(); + var newTop = top + startScroll.top - curScroll.top; + var point = newTop - (window.pageYOffset || (document.documentElement || document.body).scrollTop); + if (!below) point += hints.offsetHeight; + if (point <= editor.top || point >= editor.bottom) return completion.close(); + hints.style.top = newTop + "px"; + hints.style.left = (left + startScroll.left - curScroll.left) + "px"; + }); + + CodeMirror.on(hints, "dblclick", function (e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + widget.pick(); + } + }); + + CodeMirror.on(hints, "click", function (e) { + var t = getHintElement(hints, e.target || e.srcElement); + if (t && t.hintId != null) { + widget.changeActive(t.hintId); + if (completion.options.completeOnSingleClick) widget.pick(); + } + }); + + CodeMirror.on(hints, "mousedown", function () { + setTimeout(function () { + cm.focus(); + }, 20); + }); + + CodeMirror.signal(data, "select", completions[this.selectedHint], hints.childNodes[this.selectedHint]); + return true; +} + +Widget.prototype = { + close: function () { + if (this.completion.widget != this) return; + this.completion.widget = null; + this.hints.parentNode.removeChild(this.hints); + this.completion.cm.removeKeyMap(this.keyMap); + + var cm = this.completion.cm; + if (this.completion.options.closeOnUnfocus) { + cm.off("blur", this.onBlur); + cm.off("focus", this.onFocus); + } + cm.off("scroll", this.onScroll); + }, + + disable: function () { + this.completion.cm.removeKeyMap(this.keyMap); + var widget = this; + this.keyMap = { + Enter: function () { + widget.picked = true; + } + }; + this.completion.cm.addKeyMap(this.keyMap); + }, + + pick: function () { + this.completion.pick(this.data, this.selectedHint); + }, + + changeActive: function (i, avoidWrap) { + if (i >= this.data.list.length) + i = avoidWrap ? this.data.list.length - 1 : 0; + else if (i < 0) + i = avoidWrap ? 0 : this.data.list.length - 1; + if (this.selectedHint == i) return; + var node = this.hints.childNodes[this.selectedHint]; + node.className = node.className.replace(" " + ACTIVE_HINT_ELEMENT_CLASS, ""); + node = this.hints.childNodes[this.selectedHint = i]; + node.className += " " + ACTIVE_HINT_ELEMENT_CLASS; + if (node.offsetTop < this.hints.scrollTop) + this.hints.scrollTop = node.offsetTop - 3; + else if (node.offsetTop + node.offsetHeight > this.hints.scrollTop + this.hints.clientHeight) + this.hints.scrollTop = node.offsetTop + node.offsetHeight - this.hints.clientHeight + 3; + CodeMirror.signal(this.data, "select", this.data.list[this.selectedHint], node); + }, + + screenAmount: function () { + return Math.floor(this.hints.clientHeight / this.hints.firstChild.offsetHeight) || 1; + } +}; + +function applicableHelpers(cm, helpers) { + if (!cm.somethingSelected()) return helpers + var result = [] + for (var i = 0; i < helpers.length; i++) + if (helpers[i].supportsSelection) result.push(helpers[i]) + return result +} + +function fetchHints(hint, cm, options, callback) { + if (hint.async) { + hint(cm, callback, options) + } else { + var result = hint(cm, options) + if (result && result.then) result.then(callback) + else callback(result) + } +} + +function resolveAutoHints(cm, pos) { + var helpers = cm.getHelpers(pos, "hint"), + words + if (helpers.length) { + var resolved = function (cm, callback, options) { + var app = applicableHelpers(cm, helpers); + + function run(i) { + if (i == app.length) return callback(null) + fetchHints(app[i], cm, options, function (result) { + if (result && result.list.length > 0) callback(result) + else run(i + 1) + }) + } + run(0) + } + resolved.async = true + resolved.supportsSelection = true + return resolved + } else if (words = cm.getHelper(cm.getCursor(), "hintWords")) { + return function (cm) { + return CodeMirror.hint.fromList(cm, { + words: words + }) + } + } else if (CodeMirror.hint.anyword) { + return function (cm, options) { + return CodeMirror.hint.anyword(cm, options) + } + } else { + return function () {} + } +} + +CodeMirror.registerHelper("hint", "auto", { + resolve: resolveAutoHints +}); + +CodeMirror.registerHelper("hint", "fromList", function (cm, options) { + var cur = cm.getCursor(), + token = cm.getTokenAt(cur) + var term, from = CodeMirror.Pos(cur.line, token.start), + to = cur + if (token.start < cur.ch && /\w/.test(token.string.charAt(cur.ch - token.start - 1))) { + term = token.string.substr(0, cur.ch - token.start) + } else { + term = "" + from = cur + } + var found = []; + for (var i = 0; i < options.words.length; i++) { + var word = options.words[i]; + if (word.slice(0, term.length) == term) + found.push(word); + } + + if (found.length) return { + list: found, + from: from, + to: to + }; +}); + +CodeMirror.commands.autocomplete = CodeMirror.showHint; + +var defaultOptions = { + hint: CodeMirror.hint.auto, + completeSingle: true, + alignWithWord: true, + closeCharacters: /[\s()\[\]{};:>,]/, + closeOnUnfocus: true, + completeOnSingleClick: true, + container: null, + customKeys: null, + extraKeys: null +}; + +CodeMirror.defineOption("hintOptions", null); +var WORD = /[\w$]+/, + RANGE = 500; + +CodeMirror.registerHelper("hint", "anyword", function (editor, options) { + var word = options && options.word || WORD; + var range = options && options.range || RANGE; + var cur = editor.getCursor(), + curLine = editor.getLine(cur.line); + var end = cur.ch, + start = end; + while (start && word.test(curLine.charAt(start - 1))) --start; + var curWord = start != end && curLine.slice(start, end); + + var list = options && options.list || [], + seen = {}; + var re = new RegExp(word.source, "g"); + for (var dir = -1; dir <= 1; dir += 2) { + var line = cur.line, + endLine = Math.min(Math.max(line + dir * range, editor.firstLine()), editor.lastLine()) + dir; + for (; line != endLine; line += dir) { + var text = editor.getLine(line), + m; + while (m = re.exec(text)) { + if (line == cur.line && m[0] === curWord) continue; + if ((!curWord || m[0].lastIndexOf(curWord, 0) == 0) && !Object.prototype.hasOwnProperty.call(seen, m[0])) { + seen[m[0]] = true; + list.push(m[0]); + } + } + } + } + return { + list: list, + from: CodeMirror.Pos(cur.line, start), + to: CodeMirror.Pos(cur.line, end) + }; +}); \ No newline at end of file diff --git a/ui/src/ifql/components/TimeMachineEditor.tsx b/ui/src/ifql/components/TimeMachineEditor.tsx index 1251f496a1..e2fcd7d903 100644 --- a/ui/src/ifql/components/TimeMachineEditor.tsx +++ b/ui/src/ifql/components/TimeMachineEditor.tsx @@ -4,12 +4,17 @@ import {EditorChange} from 'codemirror' import 'src/external/codemirror' import {ErrorHandling} from 'src/shared/decorators/errors' import {OnChangeScript} from 'src/types/ifql' +import {editor} from 'src/ifql/constants' interface Props { script: string onChangeScript: OnChangeScript } +interface EditorInstance extends IInstance { + showHint: (options?: any) => void +} + @ErrorHandling class TimeMachineEditor extends PureComponent { constructor(props) { @@ -24,6 +29,8 @@ class TimeMachineEditor extends PureComponent { theme: 'material', tabIndex: 1, readonly: false, + extraKeys: {'Ctrl-Space': 'autocomplete'}, + completeSingle: false, } return ( @@ -35,11 +42,22 @@ class TimeMachineEditor extends PureComponent { options={options} onBeforeChange={this.updateCode} onTouchStart={this.onTouchStart} + onKeyUp={this.handleKeyUp} />
) } + private handleKeyUp = (instance: EditorInstance, e: KeyboardEvent) => { + const {key} = e + + if (editor.EXCLUDED_KEYS.includes(key)) { + return + } + + instance.showHint({completeSingle: false}) + } + private onTouchStart = () => {} private updateCode = ( diff --git a/ui/src/ifql/constants/ast.ts b/ui/src/ifql/constants/ast.ts new file mode 100644 index 0000000000..e542d8b432 --- /dev/null +++ b/ui/src/ifql/constants/ast.ts @@ -0,0 +1,155 @@ +export const ast = { + type: 'File', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + program: { + type: 'Program', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + sourceType: 'module', + body: [ + { + type: 'ExpressionStatement', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + expression: { + type: 'CallExpression', + start: 0, + end: 22, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 22, + }, + }, + callee: { + type: 'Identifier', + start: 0, + end: 4, + loc: { + start: { + line: 1, + column: 0, + }, + end: { + line: 1, + column: 4, + }, + identifierName: 'from', + }, + name: 'from', + }, + arguments: [ + { + type: 'ObjectExpression', + start: 5, + end: 21, + loc: { + start: { + line: 1, + column: 5, + }, + end: { + line: 1, + column: 21, + }, + }, + properties: [ + { + type: 'ObjectProperty', + start: 6, + end: 20, + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 20, + }, + }, + method: false, + shorthand: false, + computed: false, + key: { + type: 'Identifier', + start: 6, + end: 8, + loc: { + start: { + line: 1, + column: 6, + }, + end: { + line: 1, + column: 8, + }, + identifierName: 'db', + }, + name: 'db', + }, + value: { + type: 'StringLiteral', + start: 10, + end: 20, + loc: { + start: { + line: 1, + column: 10, + }, + end: { + line: 1, + column: 20, + }, + }, + extra: { + rawValue: 'telegraf', + raw: 'telegraf', + }, + value: 'telegraf', + }, + }, + ], + }, + ], + }, + }, + ], + directives: [], + }, +} diff --git a/ui/src/ifql/constants/editor.ts b/ui/src/ifql/constants/editor.ts new file mode 100644 index 0000000000..78bc7709f3 --- /dev/null +++ b/ui/src/ifql/constants/editor.ts @@ -0,0 +1,55 @@ +export const EXCLUDED_KEYS = [ + 'ArrowRight', + 'ArrowLeft', + 'ArrowDown', + 'ArrowUp', + 'Backspace', + 'Tab', + 'Enter', + 'Shift', + 'Ctrl', + 'Alt', + 'Pause', + 'Capslock', + 'Escape', + 'Pageup', + 'Pagedown', + 'End', + 'Home', + 'Left', + 'Up', + 'Right', + 'Down', + 'Insert', + 'Delete', + 'Left window key', + 'Right window key', + 'Select', + 'Add', + 'Subtract', + 'Decimal point', + 'Divide', + 'F1', + 'F2', + 'F3', + 'F4', + 'F5', + 'F6', + 'F7', + 'F8', + 'F9', + 'F10', + 'F11', + 'F12', + 'Numlock', + 'Scrolllock', + 'Semicolon', + 'Equalsign', + 'Comma', + 'Dash', + 'Slash', + 'Graveaccent', + 'Backslash', + 'Quote', + ' ', +] diff --git a/ui/src/ifql/constants/index.ts b/ui/src/ifql/constants/index.ts index 0fd805dde3..d34a7234f5 100644 --- a/ui/src/ifql/constants/index.ts +++ b/ui/src/ifql/constants/index.ts @@ -1,160 +1,6 @@ import * as funcNames from 'src/ifql/constants/funcNames' import * as argTypes from 'src/ifql/constants/argumentTypes' +import {ast} from 'src/ifql/constants/ast' +import * as editor from 'src/ifql/constants/editor' -const ast = { - type: 'File', - start: 0, - end: 22, - loc: { - start: { - line: 1, - column: 0, - }, - end: { - line: 1, - column: 22, - }, - }, - program: { - type: 'Program', - start: 0, - end: 22, - loc: { - start: { - line: 1, - column: 0, - }, - end: { - line: 1, - column: 22, - }, - }, - sourceType: 'module', - body: [ - { - type: 'ExpressionStatement', - start: 0, - end: 22, - loc: { - start: { - line: 1, - column: 0, - }, - end: { - line: 1, - column: 22, - }, - }, - expression: { - type: 'CallExpression', - start: 0, - end: 22, - loc: { - start: { - line: 1, - column: 0, - }, - end: { - line: 1, - column: 22, - }, - }, - callee: { - type: 'Identifier', - start: 0, - end: 4, - loc: { - start: { - line: 1, - column: 0, - }, - end: { - line: 1, - column: 4, - }, - identifierName: 'from', - }, - name: 'from', - }, - arguments: [ - { - type: 'ObjectExpression', - start: 5, - end: 21, - loc: { - start: { - line: 1, - column: 5, - }, - end: { - line: 1, - column: 21, - }, - }, - properties: [ - { - type: 'ObjectProperty', - start: 6, - end: 20, - loc: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 20, - }, - }, - method: false, - shorthand: false, - computed: false, - key: { - type: 'Identifier', - start: 6, - end: 8, - loc: { - start: { - line: 1, - column: 6, - }, - end: { - line: 1, - column: 8, - }, - identifierName: 'db', - }, - name: 'db', - }, - value: { - type: 'StringLiteral', - start: 10, - end: 20, - loc: { - start: { - line: 1, - column: 10, - }, - end: { - line: 1, - column: 20, - }, - }, - extra: { - rawValue: 'telegraf', - raw: 'telegraf', - }, - value: 'telegraf', - }, - }, - ], - }, - ], - }, - }, - ], - directives: [], - }, -} - -export {ast, funcNames, argTypes} +export {ast, funcNames, argTypes, editor} diff --git a/ui/src/style/components/code-mirror-theme.scss b/ui/src/style/components/code-mirror-theme.scss index 242890443e..0545c00f30 100644 --- a/ui/src/style/components/code-mirror-theme.scss +++ b/ui/src/style/components/code-mirror-theme.scss @@ -20,73 +20,190 @@ font-weight: 600; height: 100%; } + .CodeMirror-vscrollbar { - @include custom-scrollbar-round($g2-kevlar,$g6-smoke); + @include custom-scrollbar-round($g2-kevlar, $g6-smoke); } + .CodeMirror-hscrollbar { - @include custom-scrollbar-round($g0-obsidian,$g6-smoke); + @include custom-scrollbar-round($g0-obsidian, $g6-smoke); } + .cm-s-material .CodeMirror-gutters { - @include gradient-v($g2-kevlar, $g0-obsidian) - border: none; + @include gradient-v($g2-kevlar, $g0-obsidian) border: none; } + .cm-s-material .CodeMirror-gutters .CodeMirror-gutter { background-color: fade-out($g4-onyx, 0.75); height: calc(100% + 30px); } + .CodeMirror-gutter.CodeMirror-linenumbers { width: 60px; } + .cm-s-material.CodeMirror .CodeMirror-sizer { margin-left: 60px; } + .cm-s-material.CodeMirror .CodeMirror-linenumber.CodeMirror-gutter-elt { padding-right: 9px; width: 46px; color: $g8-storm; } -.cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); } + +.cm-s-material .CodeMirror-guttermarker, +.cm-s-material .CodeMirror-guttermarker-subtle, +.cm-s-material .CodeMirror-linenumber { + color: rgb(83, 127, 126); +} + .cm-s-material .CodeMirror-cursor { width: 2px; border: 0; background-color: $g20-white; - box-shadow: - 0 0 3px $c-laser, - 0 0 6px $c-ocean, - 0 0 11px $c-amethyst; + box-shadow: 0 0 3px $c-laser, 0 0 6px $c-ocean, 0 0 11px $c-amethyst; } + .cm-s-material div.CodeMirror-selected, .cm-s-material.CodeMirror-focused div.CodeMirror-selected { - background-color: fade-out($g8-storm,0.7); + background-color: fade-out($g8-storm, 0.7); +} + +.cm-s-material .CodeMirror-line::selection, +.cm-s-material .CodeMirror-line>span::selection, +.cm-s-material .CodeMirror-line>span>span::selection { + background: rgba(255, 255, 255, 0.10); +} + +.cm-s-material .CodeMirror-line::-moz-selection, +.cm-s-material .CodeMirror-line>span::-moz-selection, +.cm-s-material .CodeMirror-line>span>span::-moz-selection { + background: rgba(255, 255, 255, 0.10); +} + +.cm-s-material .CodeMirror-activeline-background { + background: rgba(0, 0, 0, 0); +} + +.cm-s-material .cm-keyword { + color: $c-comet; +} + +.cm-s-material .cm-operator { + color: $c-dreamsicle; +} + +.cm-s-material .cm-variable-2 { + color: #80CBC4; +} + +.cm-s-material .cm-variable-3, +.cm-s-material .cm-type { + color: $c-laser; +} + +.cm-s-material .cm-builtin { + color: #DECB6B; +} + +.cm-s-material .cm-atom { + color: $c-viridian; +} + +.cm-s-material .cm-number { + color: $c-daisy; +} + +.cm-s-material .cm-def { + color: rgba(233, 237, 237, 1); +} + +.cm-s-material .cm-string { + color: $c-krypton; +} + +.cm-s-material .cm-string-2 { + color: #80CBC4; +} + +.cm-s-material .cm-comment { + color: $g10-wolf; +} + +.cm-s-material .cm-variable { + color: $c-laser; +} + +.cm-s-material .cm-tag { + color: #80CBC4; +} + +.cm-s-material .cm-meta { + color: #80CBC4; +} + +.cm-s-material .cm-attribute { + color: #FFCB6B; +} + +.cm-s-material .cm-property { + color: #80CBAE; +} + +.cm-s-material .cm-qualifier { + color: #DECB6B; +} + +.cm-s-material .cm-variable-3, +.cm-s-material .cm-type { + color: #DECB6B; +} + +.cm-s-material .cm-tag { + color: rgba(255, 83, 112, 1); } -.cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); } -.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); } -.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); } -.cm-s-material .cm-keyword { color: $c-comet; } -.cm-s-material .cm-operator { color: $c-dreamsicle; } -.cm-s-material .cm-variable-2 { color: #80CBC4; } -.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: $c-laser; } -.cm-s-material .cm-builtin { color: #DECB6B; } -.cm-s-material .cm-atom { color: $c-viridian; } -.cm-s-material .cm-number { color: $c-daisy; } -.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); } -.cm-s-material .cm-string { color: $c-krypton; } -.cm-s-material .cm-string-2 { color: #80CBC4; } -.cm-s-material .cm-comment { color: $g10-wolf; } -.cm-s-material .cm-variable { color: $c-laser; } -.cm-s-material .cm-tag { color: #80CBC4; } -.cm-s-material .cm-meta { color: #80CBC4; } -.cm-s-material .cm-attribute { color: #FFCB6B; } -.cm-s-material .cm-property { color: #80CBAE; } -.cm-s-material .cm-qualifier { color: #DECB6B; } -.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; } -.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); } .cm-s-material .cm-error { color: rgba(255, 255, 255, 1.0); background-color: #EC5F67; } + .cm-s-material .CodeMirror-matchingbracket { text-decoration: underline; color: white !important; } + +// CodeMirror hints +.CodeMirror-hints { + position: absolute; + z-index: 10; + overflow: hidden; + list-style: none; + margin: 0; + padding: 2px; + -webkit-box-shadow: 2px 3px 5px rgba(0, 0, 0, .2); + -moz-box-shadow: 2px 3px 5px rgba(0, 0, 0, .2); + box-shadow: 2px 3px 5px rgba(0, 0, 0, .2); + border-radius: 3px; + border: 1px solid silver; + background: white; + font-size: 90%; + font-family: monospace; + max-height: 20em; + overflow-y: auto; +} + +.CodeMirror-hint { + margin: 0; + padding: 0 4px; + border-radius: 2px; + white-space: pre; + color: black; + cursor: pointer; +} + +li.CodeMirror-hint-active { + background: #08f; + color: white; +} \ No newline at end of file From 8ba19187e43994dfa19b07ae1e4f3a77ee9d2ebf Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 7 May 2018 10:11:00 -0700 Subject: [PATCH 076/104] Update copy --- ui/src/ifql/apis/index.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/ifql/apis/index.ts b/ui/src/ifql/apis/index.ts index ba9c44f1d7..36467dcaca 100644 --- a/ui/src/ifql/apis/index.ts +++ b/ui/src/ifql/apis/index.ts @@ -53,7 +53,7 @@ export const getTags = async () => { const {data} = await Promise.resolve(response) return data.tags } catch (error) { - console.error('Could not get tagKeys', error) + console.error('Could not get tags', error) throw error } } @@ -64,7 +64,7 @@ export const getTagValues = async () => { const {data} = await Promise.resolve(response) return data.values } catch (error) { - console.error('Could not get tagKeys', error) + console.error('Could not get tag values', error) throw error } } From 6aa95018148ed802fbfebd1419e736520b171953 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 7 May 2018 10:48:27 -0700 Subject: [PATCH 077/104] Remove unused props --- ui/src/ifql/components/SchemaExplorer.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/ui/src/ifql/components/SchemaExplorer.tsx b/ui/src/ifql/components/SchemaExplorer.tsx index 8bbc32ba74..7524213e90 100644 --- a/ui/src/ifql/components/SchemaExplorer.tsx +++ b/ui/src/ifql/components/SchemaExplorer.tsx @@ -11,9 +11,6 @@ class SchemaExplorer extends PureComponent { className="form-control input-sm" placeholder="Filter YO schema dawg..." type="text" - // onChange={this.handleFilterText} - // onKeyUp={this.handleEscape} - // onClick={this.handleInputClick} spellCheck={false} autoComplete="off" /> From 5f8564ffa2f13e2519a541c9748bf12e40a69e11 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 7 May 2018 11:26:43 -0700 Subject: [PATCH 078/104] Add threesizer division header with collapsible title --- ui/src/shared/components/ResizeDivision.tsx | 82 +++++++++++++++++---- ui/src/style/components/threesizer.scss | 51 ++++++++++--- 2 files changed, 107 insertions(+), 26 deletions(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 4a33807022..11b153cbbb 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -1,5 +1,6 @@ import React, {PureComponent, ReactElement, MouseEvent} from 'react' import classnames from 'classnames' +import calculateSize from 'calculate-size' import {HANDLE_VERTICAL, HANDLE_HORIZONTAL} from 'src/shared/constants/index' @@ -26,26 +27,49 @@ class Division extends PureComponent { handleDisplay: 'visible', } + private collapseThreshold: number = 0 + private containerRef: HTMLElement + + public componentDidMount() { + const {name} = this.props + + if (!name) { + return 0 + } + + const {width} = calculateSize(name, { + font: '"Roboto", Helvetica, Arial, Tahoma, Verdana, sans-serif', + fontSize: '16px', + fontWeight: '500', + }) + const NAME_OFFSET = 66 + + this.collapseThreshold = width + NAME_OFFSET + } + public render() { const {name, render, draggable} = this.props return ( - <> -
-
-
{name}
-
-
- {render()} -
+
(this.containerRef = r)} + > +
+
{name}
- +
+ {name &&
} +
{render()}
+
+
) } @@ -134,6 +158,32 @@ class Division extends PureComponent { }) } + private get titleClass(): string { + const {orientation} = this.props + + const collapsed = orientation === HANDLE_VERTICAL && this.isTitleObscured + + return classnames('threesizer--title', { + 'threesizer--collapsed': collapsed, + vertical: orientation === HANDLE_VERTICAL, + horizontal: orientation === HANDLE_HORIZONTAL, + }) + } + + private get isTitleObscured(): boolean { + if (this.props.size === 0) { + return true + } + + if (!this.containerRef || this.props.size === 1) { + return false + } + + const {width} = this.containerRef.getBoundingClientRect() + + return width <= this.collapseThreshold + } + private get isDragging(): boolean { const {id, activeHandleID} = this.props return id === activeHandleID diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index d5c3e3deb5..42e18231a8 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -52,7 +52,6 @@ $threesizer-handle: 30px; transition: background-color 0.25s ease, color 0.25s ease; &.vertical { - padding: 12px 0; border-right: solid 2px $g3-castle; &:hover, @@ -62,7 +61,6 @@ $threesizer-handle: 30px; } &.horizontal { - padding: 0 12px; border-bottom: solid 2px $g3-castle; &:hover, @@ -85,20 +83,23 @@ $threesizer-handle: 30px; background-color: $g5-pepper; } } -// First Handle should not have a outside facing border -// .threesizer:first-child .threesizer--division .threesizer--handle { -// border-top: 0; -// border-left: 0; -// } .threesizer--title { - font-size: 13px; + padding-left: 14px; + position: relative; + font-size: 16px; font-weight: 500; white-space: nowrap; color: $g11-sidewalk; + z-index: 1; + transition: transform 0.25s ease; - .vertical & { - transform: rotate(90deg) translateX(8px); + &.vertical { + transform: translate(28px, 14px); + + &.threesizer--collapsed { + transform: translate(0, 3px) rotate(90deg); + } } } @@ -108,8 +109,19 @@ $threesizer-shadow-start: fade-out($g0-obsidian, 0.82); $threesizer-shadow-stop: fade-out($g0-obsidian, 1); .threesizer--contents { + display: flex; + align-items: stretch; + flex-wrap: nowrap; position: relative; + &.horizontal { + flex-direction: row; + } + + &.vertical { + flex-direction: column; + } + // Bottom Shadow &.horizontal:after, &.vertical:after { @@ -140,3 +152,22 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1); content: none; display: none; } + +// Header +.threesizer--header { + background-color: $g2-kevlar; + + .horizontal > & { + width: 50px; + border-right: 2px solid $g4-onyx; + } + + .vertical > & { + height: 50px; + border-bottom: 2px solid $g4-onyx; + } +} + +.threesizer--body { + flex: 1 0 0; +} From f1b9ee5c1b11c9df28526eaba1f447b0118c1535 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Mon, 7 May 2018 11:27:00 -0700 Subject: [PATCH 079/104] partial revert(43c5afe7) to fix GitHub Enterprise via Generic Oauth The above commit was over-applied in #3168 to Generic Oauth in addition to GitHub Oauth based on an assumption. It should only have been applied to GitHub-specific OAuth. This over-application introduced a bug where GitHub Enterprise did not work anymore. --- oauth2/generic.go | 24 +----------------------- oauth2/generic_test.go | 4 +--- 2 files changed, 2 insertions(+), 26 deletions(-) diff --git a/oauth2/generic.go b/oauth2/generic.go index 54b4392fcb..c53c98c87e 100644 --- a/oauth2/generic.go +++ b/oauth2/generic.go @@ -165,28 +165,6 @@ type UserEmail struct { Verified *bool `json:"verified,omitempty"` } -// GetPrimary returns if the email is the primary email. -// If primary is not present, all emails are considered the primary. -func (u *UserEmail) GetPrimary() bool { - if u == nil { - return false - } else if u.Primary == nil { - return true - } - return *u.Primary -} - -// GetVerified returns if the email has been verified. -// If verified is not present, all emails are considered verified. -func (u *UserEmail) GetVerified() bool { - if u == nil { - return false - } else if u.Verified == nil { - return true - } - return *u.Verified -} - // getPrimaryEmail gets the private email account for the authenticated user. func (g *Generic) getPrimaryEmail(client *http.Client) (string, error) { emailsEndpoint := g.APIURL + "/emails" @@ -211,7 +189,7 @@ func (g *Generic) getPrimaryEmail(client *http.Client) (string, error) { func (g *Generic) primaryEmail(emails []*UserEmail) (string, error) { for _, m := range emails { - if m != nil && m.GetPrimary() && m.GetVerified() && m.Email != nil { + if m != nil && m.Primary != nil && m.Verified != nil && m.Email != nil { return *m.Email, nil } } diff --git a/oauth2/generic_test.go b/oauth2/generic_test.go index 33e54c5de6..89bfc88184 100644 --- a/oauth2/generic_test.go +++ b/oauth2/generic_test.go @@ -155,9 +155,7 @@ func TestGenericPrincipalIDDomain(t *testing.T) { Primary bool `json:"primary"` Verified bool `json:"verified"` }{ - {"mcfly@example.com", false, true}, - {"martymcspelledwrong@example.com", false, false}, - {"martymcfly@pinheads.rok", true, true}, + {"martymcfly@pinheads.rok", true, false}, } mockAPI := httptest.NewServer(http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { if r.URL.Path == "/" { From 715e7410df63d8e166ea5d24c7849c19cb1b3ee8 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 7 May 2018 11:29:50 -0700 Subject: [PATCH 080/104] Fix logic --- ui/src/shared/components/ResizeDivision.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/components/ResizeDivision.tsx b/ui/src/shared/components/ResizeDivision.tsx index 11b153cbbb..4b94093acf 100644 --- a/ui/src/shared/components/ResizeDivision.tsx +++ b/ui/src/shared/components/ResizeDivision.tsx @@ -175,7 +175,7 @@ class Division extends PureComponent { return true } - if (!this.containerRef || this.props.size === 1) { + if (!this.containerRef || this.props.size >= 0.33) { return false } From 22bc3f867d0cad48697b31df35e37e7fa13e6354 Mon Sep 17 00:00:00 2001 From: Jared Scheib Date: Mon, 7 May 2018 11:40:02 -0700 Subject: [PATCH 081/104] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e71ae66615..41c90ff45e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ 1. [#3353](https://github.com/influxdata/chronograf/pull/3353): Display y-axis label on initial graph load 1. [#3352](https://github.com/influxdata/chronograf/pull/3352): Fix not being able to change the source in the CEO display 1. [#3357](https://github.com/influxdata/chronograf/pull/3357): Fix only the selected template variable value getting loaded +1. [#3389](https://github.com/influxdata/chronograf/pull/3389): Fix Generic OAuth bug for GitHub Enterprise where the principal was incorrectly being checked for email being Primary and Verified ## v1.4.4.1 [2018-04-16] From 7e4a1c1743b51834ebf275746b9e684aad57ba84 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 7 May 2018 14:58:15 -0700 Subject: [PATCH 082/104] Update excluded keys --- ui/src/ifql/constants/editor.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/ifql/constants/editor.ts b/ui/src/ifql/constants/editor.ts index 78bc7709f3..f91858da8e 100644 --- a/ui/src/ifql/constants/editor.ts +++ b/ui/src/ifql/constants/editor.ts @@ -8,6 +8,7 @@ export const EXCLUDED_KEYS = [ 'Enter', 'Shift', 'Ctrl', + 'Control', 'Alt', 'Pause', 'Capslock', @@ -51,5 +52,6 @@ export const EXCLUDED_KEYS = [ 'Graveaccent', 'Backslash', 'Quote', + 'Meta', ' ', ] From 29f89f04639cb75885a1b604d6c5f13cb372e285 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Mon, 7 May 2018 15:11:48 -0700 Subject: [PATCH 083/104] Fix delete confirmation copy for Tags in kapacitor configs --- ui/src/shared/components/Tags.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/src/shared/components/Tags.tsx b/ui/src/shared/components/Tags.tsx index 130791be43..17f5c95e4c 100644 --- a/ui/src/shared/components/Tags.tsx +++ b/ui/src/shared/components/Tags.tsx @@ -51,13 +51,22 @@ class Tag extends PureComponent { size="btn-xs" customClass="input-tag--remove" square={true} - confirmText="Remove user from organization?" + confirmText={this.confirmText} confirmAction={this.handleClickDelete(item)} /> ) } + private get confirmText(): string { + const {item} = this.props + if (item.name || item.text) { + return `Delete ${item.name || item.text}?` + } + + return 'Delete?' + } + private handleClickDelete = item => () => { this.props.onDelete(item) } From a89a3cfdb7b3f750fcebbd85bc367a18f9d66dc9 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 7 May 2018 16:00:37 -0700 Subject: [PATCH 084/104] Prefer pushing to array over spread for speed Co-authored-by: Deniz Kusefoglu Co-authored-by: Brandon Farmer --- ui/src/utils/groupByTimeSeriesTransform.js | 39 +++++++++++++--------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/ui/src/utils/groupByTimeSeriesTransform.js b/ui/src/utils/groupByTimeSeriesTransform.js index 3df7f0d52e..f3ae47bd04 100644 --- a/ui/src/utils/groupByTimeSeriesTransform.js +++ b/ui/src/utils/groupByTimeSeriesTransform.js @@ -166,21 +166,31 @@ const insertGroupByValues = ( sortedLabels ) => { const dashArray = Array(sortedLabels.length).fill('-') - let timeSeries = [] - forEach(serieses, (s, sind) => { - if (s.isGroupBy) { - forEach(s.values, vs => { - const tsRow = {time: vs[0], values: clone(dashArray)} - forEach(vs.slice(1), (v, i) => { - const label = seriesLabels[sind][i].label - tsRow.values[ - labelsToValueIndex[label + s.responseIndex + s.seriesIndex] - ] = v - }) - timeSeries = [...timeSeries, tsRow] - }) + const timeSeries = [] + + for (let x = 0; x < serieses.length; x++) { + const s = serieses[x] + if (!s.isGroupBy) { + continue } - }) + + for (let i = 0; i < s.values.length; i++) { + const vs = s.values[i] + const tsRow = {time: vs[0], values: clone(dashArray)} + + const vss = vs.slice(1) + for (let j = 0; j < vss.length; j++) { + const v = vss[j] + const label = seriesLabels[x][j].label + + tsRow.values[ + labelsToValueIndex[label + s.responseIndex + s.seriesIndex] + ] = v + } + + timeSeries.push(tsRow) + } + } return timeSeries } @@ -245,7 +255,6 @@ const constructTimeSeries = (serieses, cells, sortedLabels, seriesLabels) => { export const groupByTimeSeriesTransform = (raw, isTable) => { const results = constructResults(raw, isTable) const serieses = constructSerieses(results) - const {cells, sortedLabels, seriesLabels} = constructCells(serieses) const sortedTimeSeries = constructTimeSeries( From f19fce6438a9efb2b39ae7a8a45d11a4c7db8fcb Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Mon, 7 May 2018 16:23:10 -0700 Subject: [PATCH 085/104] Perform expensive transformation for csv data at time of request --- ui/src/shared/components/AutoRefresh.tsx | 4 +--- ui/src/shared/components/Layout.js | 14 +++++++------- ui/src/shared/components/LayoutCell.js | 15 +++++++++------ 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/ui/src/shared/components/AutoRefresh.tsx b/ui/src/shared/components/AutoRefresh.tsx index 32c09ccb5f..cfbb2f5a53 100644 --- a/ui/src/shared/components/AutoRefresh.tsx +++ b/ui/src/shared/components/AutoRefresh.tsx @@ -4,7 +4,6 @@ import _ from 'lodash' import {fetchTimeSeries} from 'src/shared/apis/query' import {DEFAULT_TIME_SERIES} from 'src/shared/constants/series' import {TimeSeriesServerResponse, TimeSeriesResponse} from 'src/types/series' -import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' interface Axes { bounds: { @@ -131,8 +130,7 @@ const AutoRefresh = ( }) if (grabDataForDownload) { - const {data} = timeSeriesToTableGraph(newSeries) - grabDataForDownload(data) + grabDataForDownload(newSeries) } } catch (err) { console.error(err) diff --git a/ui/src/shared/components/Layout.js b/ui/src/shared/components/Layout.js index aa441602b2..213468c1c9 100644 --- a/ui/src/shared/components/Layout.js +++ b/ui/src/shared/components/Layout.js @@ -23,11 +23,11 @@ const getSource = (cell, source, sources, defaultSource) => { @ErrorHandling class LayoutState extends Component { state = { - celldata: [[]], + cellData: [], } - grabDataForDownload = celldata => { - this.setState({celldata}) + grabDataForDownload = cellData => { + this.setState({cellData}) } render() { @@ -59,7 +59,7 @@ const Layout = ( source, sources, onZoom, - celldata, + cellData, templates, timeRange, isEditable, @@ -79,7 +79,7 @@ const Layout = ( ) => ( ) -const {array, arrayOf, bool, func, number, shape, string} = PropTypes +const {arrayOf, bool, func, number, shape, string} = PropTypes Layout.contextTypes = { source: shape(), @@ -200,7 +200,7 @@ LayoutState.propTypes = {...propTypes} Layout.propTypes = { ...propTypes, grabDataForDownload: func, - celldata: arrayOf(array), + cellData: arrayOf(shape({})), } export default LayoutState diff --git a/ui/src/shared/components/LayoutCell.js b/ui/src/shared/components/LayoutCell.js index f1658e8ac7..9b758a8884 100644 --- a/ui/src/shared/components/LayoutCell.js +++ b/ui/src/shared/components/LayoutCell.js @@ -11,6 +11,7 @@ import {notifyCSVDownloadFailed} from 'src/shared/copy/notifications' import download from 'src/external/download.js' import {ErrorHandling} from 'src/shared/decorators/errors' import {dataToCSV} from 'src/shared/parsing/dataToCSV' +import {timeSeriesToTableGraph} from 'src/utils/timeSeriesTransformers' @ErrorHandling class LayoutCell extends Component { @@ -24,9 +25,11 @@ class LayoutCell extends Component { handleCSVDownload = cell => () => { const joinedName = cell.name.split(' ').join('_') - const {celldata} = this.props + const {cellData} = this.props + const {data} = timeSeriesToTableGraph(cellData) + try { - download(dataToCSV(celldata), `${joinedName}.csv`, 'text/plain') + download(dataToCSV(data), `${joinedName}.csv`, 'text/plain') } catch (error) { notify(notifyCSVDownloadFailed()) console.error(error) @@ -34,7 +37,7 @@ class LayoutCell extends Component { } render() { - const {cell, children, isEditable, celldata, onCloneCell} = this.props + const {cell, children, isEditable, cellData, onCloneCell} = this.props const queries = _.get(cell, ['queries'], []) @@ -49,7 +52,7 @@ class LayoutCell extends Component { Date: Mon, 7 May 2018 16:42:50 -0700 Subject: [PATCH 086/104] Ensure proper division scaling on small screens --- ui/src/style/components/threesizer.scss | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ui/src/style/components/threesizer.scss b/ui/src/style/components/threesizer.scss index 42e18231a8..2bec45e63c 100644 --- a/ui/src/style/components/threesizer.scss +++ b/ui/src/style/components/threesizer.scss @@ -169,5 +169,15 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1); } .threesizer--body { - flex: 1 0 0; + .horizontal > &:only-child { + width: 100%; + } + + .vertical > &:only-child { + height: 100%; + } + + .threesizer--header + & { + flex: 1 0 0; + } } From 98c55c2c5ec964003248bd4aa59ba42ebc452de9 Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 7 May 2018 16:43:36 -0700 Subject: [PATCH 087/104] Move function argument "preview" into component --- ui/src/ifql/components/FuncArgsPreview.tsx | 66 ++++++++++++++++++++++ ui/src/ifql/components/FuncNode.tsx | 57 ++----------------- ui/src/types/ifql.ts | 2 +- 3 files changed, 71 insertions(+), 54 deletions(-) create mode 100644 ui/src/ifql/components/FuncArgsPreview.tsx diff --git a/ui/src/ifql/components/FuncArgsPreview.tsx b/ui/src/ifql/components/FuncArgsPreview.tsx new file mode 100644 index 0000000000..1425c8b064 --- /dev/null +++ b/ui/src/ifql/components/FuncArgsPreview.tsx @@ -0,0 +1,66 @@ +import React, {PureComponent} from 'react' +import {Arg} from 'src/types/ifql' +import uuid from 'uuid' + +interface Props { + args: Arg[] +} + +export default class FuncArgsPreview extends PureComponent { + public render() { + return
{this.summarizeArguments}
+ } + + private get summarizeArguments(): JSX.Element | JSX.Element[] { + const {args} = this.props + + if (!args) { + return + } + + return this.colorizedArguments + } + + private get colorizedArguments(): JSX.Element | JSX.Element[] { + const {args} = this.props + + return args.map((arg, i): JSX.Element => { + if (!arg.value) { + return + } + + const separator = i === 0 ? null : ', ' + + return ( + + {separator} + {arg.key}: {this.colorArgType(`${arg.value}`, arg.type)} + + ) + }) + } + + private colorArgType = (argument: string, type: string): JSX.Element => { + switch (type) { + case 'time': + case 'number': + case 'period': + case 'duration': + case 'array': { + return {argument} + } + case 'bool': { + return {argument} + } + case 'string': { + return "{argument}" + } + case 'invalid': { + return {argument} + } + default: { + return {argument} + } + } + } +} diff --git a/ui/src/ifql/components/FuncNode.tsx b/ui/src/ifql/components/FuncNode.tsx index 36e8b695e7..18341e5153 100644 --- a/ui/src/ifql/components/FuncNode.tsx +++ b/ui/src/ifql/components/FuncNode.tsx @@ -1,6 +1,7 @@ import React, {PureComponent, MouseEvent} from 'react' -import uuid from 'uuid' + import FuncArgs from 'src/ifql/components/FuncArgs' +import FuncArgsPreview from 'src/ifql/components/FuncArgsPreview' import {OnDeleteFuncNode, OnChangeArg, Func} from 'src/types/ifql' import {ErrorHandling} from 'src/shared/decorators/errors' @@ -33,6 +34,7 @@ export default class FuncNode extends PureComponent { public render() { const { func, + func: {args}, bodyID, onChangeArg, declarationID, @@ -47,7 +49,7 @@ export default class FuncNode extends PureComponent { onMouseLeave={this.handleMouseLeave} >
{func.name}
- {this.coloredSyntaxArgs} + {isExpanded && ( { ) } - private get coloredSyntaxArgs(): JSX.Element { - const { - func: {args}, - } = this.props - - if (!args) { - return - } - - const coloredSyntax = args.map((arg, i): JSX.Element => { - if (!arg.value) { - return - } - - const separator = i === 0 ? null : ', ' - - return ( - - {separator} - {arg.key}: {this.colorArgType(`${arg.value}`, arg.type)} - - ) - }) - - return
{coloredSyntax}
- } - - private colorArgType = (argument: string, type: string): JSX.Element => { - switch (type) { - case 'time': - case 'number': - case 'period': - case 'duration': - case 'array': { - return {argument} - } - case 'bool': { - return {argument} - } - case 'string': { - return "{argument}" - } - case 'invalid': { - return {argument} - } - default: { - return {argument} - } - } - } - private handleDelete = (): void => { const {func, bodyID, declarationID} = this.props diff --git a/ui/src/types/ifql.ts b/ui/src/types/ifql.ts index 65bd406f93..e84e4d2c07 100644 --- a/ui/src/types/ifql.ts +++ b/ui/src/types/ifql.ts @@ -51,7 +51,7 @@ export interface Func { type Value = string | boolean -interface Arg { +export interface Arg { key: string value: Value type: string From d83e8fc0c135d7a56da608f2f5510b9f2aec01db Mon Sep 17 00:00:00 2001 From: Alex P Date: Mon, 7 May 2018 16:43:50 -0700 Subject: [PATCH 088/104] Move variable names into own component --- ui/src/ifql/components/BodyBuilder.tsx | 45 ++----- ui/src/ifql/components/VariableName.tsx | 126 ++++++++++++++++++ .../components/time-machine/ifql-builder.scss | 45 +++++-- 3 files changed, 174 insertions(+), 42 deletions(-) create mode 100644 ui/src/ifql/components/VariableName.tsx diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index 3904c9a2e3..5d32b55771 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -2,6 +2,7 @@ import React, {PureComponent} from 'react' import _ from 'lodash' import ExpressionNode from 'src/ifql/components/ExpressionNode' +import VariableName from 'src/ifql/components/VariableName' import {FlatBody, Suggestion} from 'src/types/ifql' @@ -22,9 +23,7 @@ class BodyBuilder extends PureComponent { if (d.funcs) { return (
-
- {d.name} -
+ { return (
-
- {this.colorVariableSyntax(b.source)} -
+
) }) } return ( - +
+ + +
) }) return
{_.flatten(bodybuilder)}
} - private colorVariableSyntax = (varString: string) => { - const split = varString.split('=') - const varName = split[0].substring(0, split[0].length - 1) - const varValue = split[1].substring(1) - - const valueIsString = varValue.endsWith('"') - - return ( - <> - {varName} - {' = '} - - {varValue} - - - ) - } - private get funcNames() { return this.props.suggestions.map(f => f.name) } diff --git a/ui/src/ifql/components/VariableName.tsx b/ui/src/ifql/components/VariableName.tsx new file mode 100644 index 0000000000..cbb9194097 --- /dev/null +++ b/ui/src/ifql/components/VariableName.tsx @@ -0,0 +1,126 @@ +import React, {PureComponent, MouseEvent} from 'react' + +interface Props { + name?: string +} + +interface State { + isExpanded: boolean +} + +export default class VariableName extends PureComponent { + public static defaultProps: Partial = { + name: '', + } + + constructor(props) { + super(props) + + this.state = { + isExpanded: false, + } + } + + public render() { + const {isExpanded} = this.state + + return ( +
+ {this.nameElement} + {isExpanded && this.renderTooltip} +
+ ) + } + + private get renderTooltip(): JSX.Element { + const {name} = this.props + + if (name.includes('=')) { + const split = name.split('=') + const varName = split[0].substring(0, split[0].length - 1) + const varValue = split[1].substring(1) + + return ( +
+ + = + +
+ ) + } + + return ( +
+ +
+ ) + } + + private handleMouseEnter = (e: MouseEvent): void => { + e.stopPropagation() + + this.setState({isExpanded: true}) + } + + private handleMouseLeave = (e: MouseEvent): void => { + e.stopPropagation() + + this.setState({isExpanded: false}) + } + + private get nameElement(): JSX.Element { + const {name} = this.props + + if (!name) { + return Untitled + } + + if (name.includes('=')) { + return this.colorizeSyntax + } + + return {name} + } + + private get colorizeSyntax(): JSX.Element { + const {name} = this.props + const split = name.split('=') + const varName = split[0].substring(0, split[0].length - 1) + const varValue = split[1].substring(1) + + const valueIsString = varValue.endsWith('"') + + return ( + <> + {varName} + {' = '} + + {varValue} + + + ) + } +} diff --git a/ui/src/style/components/time-machine/ifql-builder.scss b/ui/src/style/components/time-machine/ifql-builder.scss index b9b187deb9..98ac266542 100644 --- a/ui/src/style/components/time-machine/ifql-builder.scss +++ b/ui/src/style/components/time-machine/ifql-builder.scss @@ -15,10 +15,16 @@ $ifql-arg-min-width: 120px; font-size: 13px; font-weight: 600; position: relative; + background-color: $g4-onyx; + transition: background-color 0.25s ease; + + &:hover { + background-color: $g6-smoke; + } } .body-builder { - padding: 12px 30px; + padding: 30px; min-width: 440px; overflow: hidden; height: 100%; @@ -30,7 +36,7 @@ $ifql-arg-min-width: 120px; width: 100%; margin-bottom: 24px; display: flex; - flex-wrap: wrap; + flex-wrap: nowrap; &:last-of-type { margin-bottom: 0; @@ -42,9 +48,11 @@ $ifql-arg-min-width: 120px; color: $g11-sidewalk; line-height: $ifql-node-height; white-space: nowrap; - background-color: $g3-castle; @include no-user-select(); } +.variable-blank { + font-style: italic; +} .variable-name { color: $c-pool; } @@ -65,9 +73,7 @@ $ifql-arg-min-width: 120px; @extend %ifql-node; display: flex; align-items: center; - background-color: $g4-onyx; margin-left: $ifql-node-gap; - transition: background-color 0.25s ease; // Connection Line &:after { @@ -81,8 +87,9 @@ $ifql-arg-min-width: 120px; transform: translate(-100%, -50%); } - &:hover { - background-color: $g6-smoke; + &:first-child:after { + content: none; + margin-left: 0; } } .func-node--name, @@ -112,7 +119,8 @@ $ifql-arg-min-width: 120px; } -.func-node--tooltip { +.func-node--tooltip, +.variable-name--tooltip { background-color: $g3-castle; border-radius: $radius; padding: 10px; @@ -176,3 +184,24 @@ $ifql-arg-min-width: 120px; .func-arg--value { flex: 1 0 0; } + + +.variable-name--tooltip { + flex-direction: row; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; +} + +.variable-name--input { + width: 140px; +} + +.variable-name--operator { + width: 20px; + height: 30px; + text-align: center; + line-height: 30px; + font-weight: 600; + @include no-user-select(); +} \ No newline at end of file From 2ade558932fbc4f6a98fc0e10ab48d09cf9b27f5 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Fri, 4 May 2018 14:43:39 -0700 Subject: [PATCH 089/104] Use custom multigrid component --- ui/src/shared/components/FancyScrollbar.js | 31 +- .../MultiGrid/CellMeasurerCacheDecorator.ts | 103 +++ .../shared/components/MultiGrid/MultiGrid.tsx | 810 ++++++++++++++++++ ui/src/shared/components/MultiGrid/index.ts | 2 + ui/src/shared/components/TableGraph.js | 3 +- ui/src/style/components/table-graph.scss | 6 +- 6 files changed, 950 insertions(+), 5 deletions(-) create mode 100644 ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts create mode 100644 ui/src/shared/components/MultiGrid/MultiGrid.tsx create mode 100644 ui/src/shared/components/MultiGrid/index.ts diff --git a/ui/src/shared/components/FancyScrollbar.js b/ui/src/shared/components/FancyScrollbar.js index 09bf06663f..e62122ab85 100644 --- a/ui/src/shared/components/FancyScrollbar.js +++ b/ui/src/shared/components/FancyScrollbar.js @@ -1,3 +1,4 @@ +import _ from 'lodash' import React, {Component} from 'react' import PropTypes from 'prop-types' import classnames from 'classnames' @@ -16,10 +17,32 @@ class FancyScrollbar extends Component { setScrollTop: () => {}, } + updateScroll() { + if (this.ref && _.isNumber(this.props.scrollTop)) { + this.ref.scrollTop(this.props.scrollTop) + } + + if (this.ref && _.isNumber(this.props.scrollLeft)) { + this.ref.scrollLeft(this.props.scrollLeft) + } + } + + componentDidMount() { + this.updateScroll() + } + + componentDidUpdate() { + this.updateScroll() + } + handleMakeDiv = className => props => { return
} + onRef = ref => { + this.ref = ref + } + render() { const { autoHide, @@ -28,6 +51,7 @@ class FancyScrollbar extends Component { className, maxHeight, setScrollTop, + style, } = this.props return ( @@ -35,6 +59,8 @@ class FancyScrollbar extends Component { className={classnames('fancy-scroll--container', { [className]: className, })} + ref={this.onRef} + style={style} onScroll={setScrollTop} autoHide={autoHide} autoHideTimeout={1000} @@ -53,7 +79,7 @@ class FancyScrollbar extends Component { } } -const {bool, func, node, number, string} = PropTypes +const {bool, func, node, number, string, object} = PropTypes FancyScrollbar.propTypes = { children: node.isRequired, @@ -62,6 +88,9 @@ FancyScrollbar.propTypes = { autoHeight: bool, maxHeight: number, setScrollTop: func, + style: object, + scrollTop: number, + scrollLeft: number, } export default FancyScrollbar diff --git a/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts b/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts new file mode 100644 index 0000000000..fdb07c0ef7 --- /dev/null +++ b/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts @@ -0,0 +1,103 @@ +import {CellMeasurerCache} from 'react-virtualized' + +interface CellMeasurerCacheDecoratorParams { + cellMeasurerCache: CellMeasurerCache + columnIndexOffset: number + rowIndexOffset: number +} + +interface IndexParam { + index: number +} + +export default class CellMeasurerCacheDecorator { + private cellMeasurerCache: CellMeasurerCache + private columnIndexOffset: number + private rowIndexOffset: number + + constructor(params: Partial = {}) { + const { + cellMeasurerCache, + columnIndexOffset = 0, + rowIndexOffset = 0, + } = params + + this.cellMeasurerCache = cellMeasurerCache + this.columnIndexOffset = columnIndexOffset + this.rowIndexOffset = rowIndexOffset + } + + public clear(rowIndex: number, columnIndex: number): void { + this.cellMeasurerCache.clear( + rowIndex + this.rowIndexOffset, + columnIndex + this.columnIndexOffset + ) + } + + public clearAll(): void { + this.cellMeasurerCache.clearAll() + } + + public columnWidth = ({index}: IndexParam) => { + this.cellMeasurerCache.columnWidth({ + index: index + this.columnIndexOffset, + }) + } + + get defaultHeight(): number { + return this.cellMeasurerCache.defaultHeight + } + + get defaultWidth(): number { + return this.cellMeasurerCache.defaultWidth + } + + public hasFixedHeight(): boolean { + return this.cellMeasurerCache.hasFixedHeight() + } + + public hasFixedWidth(): boolean { + return this.cellMeasurerCache.hasFixedWidth() + } + + public getHeight(rowIndex: number, columnIndex: number = 0): number | null { + return this.cellMeasurerCache.getHeight( + rowIndex + this.rowIndexOffset, + columnIndex + this.columnIndexOffset + ) + } + + public getWidth(rowIndex: number, columnIndex: number = 0): number | null { + return this.cellMeasurerCache.getWidth( + rowIndex + this.rowIndexOffset, + columnIndex + this.columnIndexOffset + ) + } + + public has(rowIndex: number, columnIndex: number = 0): boolean { + return this.cellMeasurerCache.has( + rowIndex + this.rowIndexOffset, + columnIndex + this.columnIndexOffset + ) + } + + public rowHeight = ({index}: IndexParam) => { + this.cellMeasurerCache.rowHeight({ + index: index + this.rowIndexOffset, + }) + } + + public set( + rowIndex: number, + columnIndex: number, + width: number, + height: number + ): void { + this.cellMeasurerCache.set( + rowIndex + this.rowIndexOffset, + columnIndex + this.columnIndexOffset, + width, + height + ) + } +} diff --git a/ui/src/shared/components/MultiGrid/MultiGrid.tsx b/ui/src/shared/components/MultiGrid/MultiGrid.tsx new file mode 100644 index 0000000000..f7c65906b3 --- /dev/null +++ b/ui/src/shared/components/MultiGrid/MultiGrid.tsx @@ -0,0 +1,810 @@ +import * as React from 'react' +import CellMeasurerCacheDecorator from './CellMeasurerCacheDecorator' +import FancyScrollbar from 'src/shared/components/FancyScrollbar' +import {Grid} from 'react-virtualized' + +const SCROLLBAR_SIZE_BUFFER = 20 + +interface Props { + columnCount?: number + classNameBottomLeftGrid?: string + classNameBottomRightGrid?: string + classNameTopLeftGrid?: string + classNameTopRightGrid?: string + enableFixedColumnScroll?: boolean + enableFixedRowScroll?: boolean + fixedColumnCount?: number + fixedRowCount?: number + style?: object + styleBottomLeftGrid?: object + styleBottomRightGrid?: object + styleTopLeftGrid?: object + styleTopRightGrid?: object + scrollTop?: number + scrollLeft?: number + rowCount?: number + rowHeight?: (arg: {index: number}) => {} | number + columnWidth?: (arg: object) => {} | number + onScroll?: (arg: object) => {} + width: number + height: number + scrollToRow?: () => {} + onSectionRendered?: () => {} + scrollToColumn?: () => {} + cellRenderer?: (arg: object) => {} +} + +interface State { + scrollLeft: number + scrollTop: number + scrollbarSize: number + showHorizontalScrollbar: boolean + showVerticalScrollbar: boolean +} + +/** + * Renders 1, 2, or 4 Grids depending on configuration. + * A main (body) Grid will always be rendered. + * Optionally, 1-2 Grids for sticky header rows will also be rendered. + * If no sticky columns, only 1 sticky header Grid will be rendered. + * If sticky columns, 2 sticky header Grids will be rendered. + */ +class MultiGrid extends React.PureComponent { + public static defaultProps = { + classNameBottomLeftGrid: '', + classNameBottomRightGrid: '', + classNameTopLeftGrid: '', + classNameTopRightGrid: '', + enableFixedColumnScroll: false, + enableFixedRowScroll: false, + fixedColumnCount: 0, + fixedRowCount: 0, + scrollToColumn: -1, + scrollToRow: -1, + style: {}, + styleBottomLeftGrid: {}, + styleBottomRightGrid: {}, + styleTopLeftGrid: {}, + styleTopRightGrid: {}, + } + + public static getDerivedStateFromProps(nextProps, prevState) { + if ( + nextProps.scrollLeft !== prevState.scrollLeft || + nextProps.scrollTop !== prevState.scrollTop + ) { + return { + scrollLeft: + nextProps.scrollLeft != null && nextProps.scrollLeft >= 0 + ? nextProps.scrollLeft + : prevState.scrollLeft, + scrollTop: + nextProps.scrollTop != null && nextProps.scrollTop >= 0 + ? nextProps.scrollTop + : prevState.scrollTop, + } + } + + return null + } + + private deferredInvalidateColumnIndex: number = 0 + private deferredInvalidateRowIndex: number = 0 + private bottomLeftGrid: Grid + private bottomRightGrid: Grid + private topLeftGrid: Grid + private topRightGrid: Grid + private deferredMeasurementCacheBottomLeftGrid: CellMeasurerCacheDecorator + private deferredMeasurementCacheBottomRightGrid: CellMeasurerCacheDecorator + private deferredMeasurementCacheTopRightGrid: CellMeasurerCacheDecorator + private leftGridWidth: number | null = 0 + private topGridHeight: number | null = 0 + private lastRenderedColumnWidth: (arg: object) => {} | number + private lastRenderedFixedColumnCount: number = 0 + private lastRenderedFixedRowCount: number = 0 + private lastRenderedRowHeight: (arg: {index: number}) => {} | number + private bottomRightGridStyle: object | null + private topRightGridStyle: object | null + private lastRenderedStyle: object | null + private lastRenderedHeight: number = 0 + private lastRenderedWidth: number = 0 + private containerTopStyle: object | null + private containerBottomStyle: object | null + private containerOuterStyle: object | null + private lastRenderedStyleBottomLeftGrid: object | null + private lastRenderedStyleBottomRightGrid: object | null + private lastRenderedStyleTopLeftGrid: object | null + private lastRenderedStyleTopRightGrid: object | null + private bottomLeftGridStyle: object | null + private topLeftGridStyle: object | null + + constructor(props, context) { + super(props, context) + + this.state = { + scrollLeft: 0, + scrollTop: 0, + scrollbarSize: 0, + showHorizontalScrollbar: false, + showVerticalScrollbar: false, + } + + const {deferredMeasurementCache, fixedColumnCount, fixedRowCount} = props + + this.maybeCalculateCachedStyles(true) + + if (deferredMeasurementCache) { + this.deferredMeasurementCacheBottomLeftGrid = + fixedRowCount > 0 + ? new CellMeasurerCacheDecorator({ + cellMeasurerCache: deferredMeasurementCache, + columnIndexOffset: 0, + rowIndexOffset: fixedRowCount, + }) + : deferredMeasurementCache + + this.deferredMeasurementCacheBottomRightGrid = + fixedColumnCount > 0 || fixedRowCount > 0 + ? new CellMeasurerCacheDecorator({ + cellMeasurerCache: deferredMeasurementCache, + columnIndexOffset: fixedColumnCount, + rowIndexOffset: fixedRowCount, + }) + : deferredMeasurementCache + + this.deferredMeasurementCacheTopRightGrid = + fixedColumnCount > 0 + ? new CellMeasurerCacheDecorator({ + cellMeasurerCache: deferredMeasurementCache, + columnIndexOffset: fixedColumnCount, + rowIndexOffset: 0, + }) + : deferredMeasurementCache + } + } + + public forceUpdateGrids() { + if (this.bottomLeftGrid) { + this.bottomLeftGrid.forceUpdate() + } + if (this.bottomRightGrid) { + this.bottomRightGrid.forceUpdate() + } + if (this.topLeftGrid) { + this.topLeftGrid.forceUpdate() + } + if (this.topRightGrid) { + this.topRightGrid.forceUpdate() + } + } + + /** See Grid#invalidateCellSizeAfterRender */ + public invalidateCellSizeAfterRender({columnIndex = 0, rowIndex = 0} = {}) { + this.deferredInvalidateColumnIndex = + typeof this.deferredInvalidateColumnIndex === 'number' + ? Math.min(this.deferredInvalidateColumnIndex, columnIndex) + : columnIndex + this.deferredInvalidateRowIndex = + typeof this.deferredInvalidateRowIndex === 'number' + ? Math.min(this.deferredInvalidateRowIndex, rowIndex) + : rowIndex + } + + /** See Grid#measureAllCells */ + public measureAllCells() { + if (this.bottomLeftGrid) { + this.bottomLeftGrid.measureAllCells() + } + if (this.bottomRightGrid) { + this.bottomRightGrid.measureAllCells() + } + if (this.topLeftGrid) { + this.topLeftGrid.measureAllCells() + } + if (this.topRightGrid) { + this.topRightGrid.measureAllCells() + } + } + + public recomputeGridSize({columnIndex = 0, rowIndex = 0} = {}) { + const {fixedColumnCount, fixedRowCount} = this.props + + const adjustedColumnIndex = Math.max(0, columnIndex - fixedColumnCount) + const adjustedRowIndex = Math.max(0, rowIndex - fixedRowCount) + + if (this.bottomLeftGrid) { + this.bottomLeftGrid.recomputeGridSize({ + columnIndex, + rowIndex: adjustedRowIndex, + }) + } + if (this.bottomRightGrid) { + this.bottomRightGrid.recomputeGridSize({ + columnIndex: adjustedColumnIndex, + rowIndex: adjustedRowIndex, + }) + } + + if (this.topLeftGrid) { + this.topLeftGrid.recomputeGridSize({ + columnIndex, + rowIndex, + }) + } + + if (this.topRightGrid) { + this.topRightGrid.recomputeGridSize({ + columnIndex: adjustedColumnIndex, + rowIndex, + }) + } + + this.leftGridWidth = null + this.topGridHeight = null + this.maybeCalculateCachedStyles(true) + } + + public componentDidMount() { + const {scrollLeft, scrollTop} = this.props + + if (scrollLeft > 0 || scrollTop > 0) { + const newState: Partial = {} + + if (scrollLeft > 0) { + newState.scrollLeft = scrollLeft + } + + if (scrollTop > 0) { + newState.scrollTop = scrollTop + } + + this.setState({...this.state, ...newState}) + } + this.handleInvalidatedGridSize() + } + + public componentDidUpdate() { + this.handleInvalidatedGridSize() + } + + public render() { + const { + onScroll, + scrollLeft: scrollLeftProp, // eslint-disable-line no-unused-vars + onSectionRendered, + scrollToRow, + scrollToColumn, + scrollTop: scrollTopProp, // eslint-disable-line no-unused-vars + ...rest + } = this.props + + this.prepareForRender() + + // Don't render any of our Grids if there are no cells. + // This mirrors what Grid does, + // And prevents us from recording inaccurage measurements when used with CellMeasurer. + if (this.props.width === 0 || this.props.height === 0) { + return null + } + + // scrollTop and scrollLeft props are explicitly filtered out and ignored + + const {scrollLeft, scrollTop} = this.state + + return ( +
+
+ {this.renderTopLeftGrid(rest)} + {this.renderTopRightGrid({ + ...rest, + ...onScroll, + scrollLeft, + })} +
+
+ {this.renderBottomLeftGrid({ + ...rest, + onScroll, + scrollTop, + })} + {this.renderBottomRightGrid({ + ...rest, + onScroll, + onSectionRendered, + scrollLeft, + scrollToColumn, + scrollToRow, + scrollTop, + })} +
+
+ ) + } + + public cellRendererBottomLeftGrid = ({rowIndex, ...rest}) => { + const {cellRenderer, fixedRowCount, rowCount} = this.props + + if (rowIndex === rowCount - fixedRowCount) { + return ( +
+ ) + } else { + return cellRenderer({ + ...rest, + parent: this, + rowIndex: rowIndex + fixedRowCount, + }) + } + } + + private getBottomGridHeight(props) { + const {height} = props + + const topGridHeight = this.getTopGridHeight(props) + + return height - topGridHeight + } + + private getLeftGridWidth(props) { + const {fixedColumnCount, columnWidth} = props + + if (this.leftGridWidth == null) { + if (typeof columnWidth === 'function') { + let leftGridWidth = 0 + + for (let index = 0; index < fixedColumnCount; index++) { + leftGridWidth += columnWidth({index}) + } + + this.leftGridWidth = leftGridWidth + } else { + this.leftGridWidth = columnWidth * fixedColumnCount + } + } + + return this.leftGridWidth + } + + private getRightGridWidth(props) { + const {width} = props + + const leftGridWidth = this.getLeftGridWidth(props) + const result = width - leftGridWidth + + return result + } + + private getTopGridHeight(props) { + const {fixedRowCount, rowHeight} = props + + if (this.topGridHeight == null) { + if (typeof rowHeight === 'function') { + let topGridHeight = 0 + + for (let index = 0; index < fixedRowCount; index++) { + topGridHeight += rowHeight({index}) + } + + this.topGridHeight = topGridHeight + } else { + this.topGridHeight = rowHeight * fixedRowCount + } + } + + return this.topGridHeight + } + + private onScrollbarsScroll = e => { + const {target} = e + this.onScroll(target) + } + + private onScroll = scrollInfo => { + const {scrollLeft, scrollTop} = scrollInfo + this.setState({ + scrollLeft, + scrollTop, + }) + + const {onScroll} = this.props + if (onScroll) { + onScroll(scrollInfo) + } + } + + private onScrollLeft = scrollInfo => { + const {scrollLeft} = scrollInfo + this.onScroll({ + scrollLeft, + scrollTop: this.state.scrollTop, + }) + } + + private renderBottomLeftGrid(props) { + const {fixedColumnCount, fixedRowCount, rowCount} = props + + if (!fixedColumnCount) { + return null + } + + const width = this.getLeftGridWidth(props) + const height = this.getBottomGridHeight(props) + + return ( + + ) + } + + private renderBottomRightGrid(props) { + const { + columnCount, + fixedColumnCount, + fixedRowCount, + rowCount, + scrollToColumn, + scrollToRow, + } = props + + const width = this.getRightGridWidth(props) + const height = this.getBottomGridHeight(props) + + return ( + + + + ) + } + + private renderTopLeftGrid(props) { + const {fixedColumnCount, fixedRowCount} = props + + if (!fixedColumnCount || !fixedRowCount) { + return null + } + + return ( + + ) + } + + private renderTopRightGrid(props) { + const { + columnCount, + enableFixedRowScroll, + fixedColumnCount, + fixedRowCount, + scrollLeft, + } = props + + if (!fixedRowCount) { + return null + } + + const width = this.getRightGridWidth(props) + const height = this.getTopGridHeight(props) + + return ( + + ) + } + + private rowHeightBottomGrid = ({index}) => { + const {fixedRowCount, rowCount, rowHeight} = this.props + const {scrollbarSize, showVerticalScrollbar} = this.state + + // An extra cell is added to the count + // This gives the smaller Grid extra room for offset, + // In case the main (bottom right) Grid has a scrollbar + // If no scrollbar, the extra space is overflow:hidden anyway + if (showVerticalScrollbar && index === rowCount - fixedRowCount) { + return scrollbarSize + } + + return typeof rowHeight === 'function' + ? rowHeight({index: index + fixedRowCount}) + : rowHeight + } + + private topLeftGridRef = ref => { + this.topLeftGrid = ref + } + + private topRightGridRef = ref => { + this.topRightGrid = ref + } + + /** + * Avoid recreating inline styles each render; this bypasses Grid's shallowCompare. + * This method recalculates styles only when specific props change. + */ + private maybeCalculateCachedStyles(resetAll) { + const { + columnWidth, + height, + fixedColumnCount, + fixedRowCount, + rowHeight, + style, + styleBottomLeftGrid, + styleBottomRightGrid, + styleTopLeftGrid, + styleTopRightGrid, + width, + } = this.props + + const sizeChange = + resetAll || + height !== this.lastRenderedHeight || + width !== this.lastRenderedWidth + const leftSizeChange = + resetAll || + columnWidth !== this.lastRenderedColumnWidth || + fixedColumnCount !== this.lastRenderedFixedColumnCount + const topSizeChange = + resetAll || + fixedRowCount !== this.lastRenderedFixedRowCount || + rowHeight !== this.lastRenderedRowHeight + + if (resetAll || sizeChange || style !== this.lastRenderedStyle) { + this.containerOuterStyle = { + height, + overflow: 'visible', // Let :focus outline show through + width, + ...style, + } + } + + if (resetAll || sizeChange || topSizeChange) { + this.containerTopStyle = { + height: this.getTopGridHeight(this.props), + position: 'relative', + width, + } + + this.containerBottomStyle = { + height: height - this.getTopGridHeight(this.props), + overflow: 'visible', // Let :focus outline show through + position: 'relative', + width, + } + } + + if ( + resetAll || + styleBottomLeftGrid !== this.lastRenderedStyleBottomLeftGrid + ) { + this.bottomLeftGridStyle = { + left: 0, + overflowY: 'hidden', + overflowX: 'hidden', + position: 'absolute', + ...styleBottomLeftGrid, + } + } + + if ( + resetAll || + leftSizeChange || + styleBottomRightGrid !== this.lastRenderedStyleBottomRightGrid + ) { + this.bottomRightGridStyle = { + left: this.getLeftGridWidth(this.props), + position: 'absolute', + ...styleBottomRightGrid, + } + } + + if (resetAll || styleTopLeftGrid !== this.lastRenderedStyleTopLeftGrid) { + this.topLeftGridStyle = { + left: 0, + overflowX: 'hidden', + overflowY: 'hidden', + position: 'absolute', + top: 0, + ...styleTopLeftGrid, + } + } + + if ( + resetAll || + leftSizeChange || + styleTopRightGrid !== this.lastRenderedStyleTopRightGrid + ) { + this.topRightGridStyle = { + left: this.getLeftGridWidth(this.props), + overflowX: 'hidden', + overflowY: 'hidden', + position: 'absolute', + top: 0, + ...styleTopRightGrid, + } + } + + this.lastRenderedColumnWidth = columnWidth + this.lastRenderedFixedColumnCount = fixedColumnCount + this.lastRenderedFixedRowCount = fixedRowCount + this.lastRenderedHeight = height + this.lastRenderedRowHeight = rowHeight + this.lastRenderedStyle = style + this.lastRenderedStyleBottomLeftGrid = styleBottomLeftGrid + this.lastRenderedStyleBottomRightGrid = styleBottomRightGrid + this.lastRenderedStyleTopLeftGrid = styleTopLeftGrid + this.lastRenderedStyleTopRightGrid = styleTopRightGrid + this.lastRenderedWidth = width + } + + private bottomLeftGridRef = ref => { + this.bottomLeftGrid = ref + } + + private bottomRightGridRef = ref => { + this.bottomRightGrid = ref + } + + private cellRendererBottomRightGrid = ({columnIndex, rowIndex, ...rest}) => { + const {cellRenderer, fixedColumnCount, fixedRowCount} = this.props + + return cellRenderer({ + ...rest, + columnIndex: columnIndex + fixedColumnCount, + parent: this, + rowIndex: rowIndex + fixedRowCount, + }) + } + + private cellRendererTopRightGrid = ({columnIndex, ...rest}) => { + const {cellRenderer, columnCount, fixedColumnCount} = this.props + + if (columnIndex === columnCount - fixedColumnCount) { + return ( +
+ ) + } else { + return cellRenderer({ + ...rest, + columnIndex: columnIndex + fixedColumnCount, + parent: this, + }) + } + } + + private columnWidthRightGrid = ({index}) => { + const {columnCount, fixedColumnCount, columnWidth} = this.props + const {scrollbarSize, showHorizontalScrollbar} = this.state + + // An extra cell is added to the count + // This gives the smaller Grid extra room for offset, + // In case the main (bottom right) Grid has a scrollbar + // If no scrollbar, the extra space is overflow:hidden anyway + if (showHorizontalScrollbar && index === columnCount - fixedColumnCount) { + return scrollbarSize + } + + return typeof columnWidth === 'function' + ? columnWidth({index: index + fixedColumnCount}) + : columnWidth + } + + private handleInvalidatedGridSize() { + if (typeof this.deferredInvalidateColumnIndex === 'number') { + const columnIndex = this.deferredInvalidateColumnIndex + const rowIndex = this.deferredInvalidateRowIndex + + this.deferredInvalidateColumnIndex = null + this.deferredInvalidateRowIndex = null + + this.recomputeGridSize({ + columnIndex, + rowIndex, + }) + this.forceUpdate() + } + } + + private prepareForRender() { + if ( + this.lastRenderedColumnWidth !== this.props.columnWidth || + this.lastRenderedFixedColumnCount !== this.props.fixedColumnCount + ) { + this.leftGridWidth = null + } + + if ( + this.lastRenderedFixedRowCount !== this.props.fixedRowCount || + this.lastRenderedRowHeight !== this.props.rowHeight + ) { + this.topGridHeight = null + } + + this.maybeCalculateCachedStyles(false) + + this.lastRenderedColumnWidth = this.props.columnWidth + this.lastRenderedFixedColumnCount = this.props.fixedColumnCount + this.lastRenderedFixedRowCount = this.props.fixedRowCount + this.lastRenderedRowHeight = this.props.rowHeight + } +} + +export default MultiGrid diff --git a/ui/src/shared/components/MultiGrid/index.ts b/ui/src/shared/components/MultiGrid/index.ts new file mode 100644 index 0000000000..5f900bac60 --- /dev/null +++ b/ui/src/shared/components/MultiGrid/index.ts @@ -0,0 +1,2 @@ +import MultiGrid from './MultiGrid' +export {MultiGrid} diff --git a/ui/src/shared/components/TableGraph.js b/ui/src/shared/components/TableGraph.js index fa9c9b97ff..cc5326ec70 100644 --- a/ui/src/shared/components/TableGraph.js +++ b/ui/src/shared/components/TableGraph.js @@ -4,7 +4,8 @@ import _ from 'lodash' import classnames from 'classnames' import {connect} from 'react-redux' -import {MultiGrid, ColumnSizer} from 'react-virtualized' +import {ColumnSizer} from 'react-virtualized' +import {MultiGrid} from 'src/shared/components/MultiGrid' import {bindActionCreators} from 'redux' import moment from 'moment' import {reduce} from 'fast.js' diff --git a/ui/src/style/components/table-graph.scss b/ui/src/style/components/table-graph.scss index 2ed426a422..e5052dfc75 100644 --- a/ui/src/style/components/table-graph.scss +++ b/ui/src/style/components/table-graph.scss @@ -31,7 +31,7 @@ // Highlight &:after { - content: ''; + content: ""; position: absolute; top: 0; left: 0; @@ -80,8 +80,8 @@ padding-right: 17px; &:before { - font-family: 'icomoon'; - content: '\e902'; + font-family: "icomoon"; + content: "\e902"; font-size: 17px; position: absolute; top: 50%; From 8b2f93148eb8dc4e8ffb3dd79ba541be6fb4c59d Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 7 May 2018 10:02:06 -0700 Subject: [PATCH 090/104] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 41c90ff45e..db7ba50117 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ 1. [#3245](https://github.com/influxdata/chronograf/pull/3245): Display 'no results' on cells without results 1. [#3354](https://github.com/influxdata/chronograf/pull/3354): Disable template variables for non editing users 1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn +1. [#3378](https://github.com/influxdata/chronograf/pull/3378): Ensure table graphs have a consistent ux between chrome and firefox ### Bug Fixes From 14e1b26b6116308f1fa16350ad564b9077426c49 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 7 May 2018 11:56:31 -0700 Subject: [PATCH 091/104] Update fancyscrollbars to use new ref api --- .../{FancyScrollbar.js => FancyScrollbar.tsx} | 66 +++++++++---------- .../shared/components/MultiGrid/MultiGrid.tsx | 2 +- 2 files changed, 32 insertions(+), 36 deletions(-) rename ui/src/shared/components/{FancyScrollbar.js => FancyScrollbar.tsx} (62%) diff --git a/ui/src/shared/components/FancyScrollbar.js b/ui/src/shared/components/FancyScrollbar.tsx similarity index 62% rename from ui/src/shared/components/FancyScrollbar.js rename to ui/src/shared/components/FancyScrollbar.tsx index e62122ab85..fed1c9e66e 100644 --- a/ui/src/shared/components/FancyScrollbar.js +++ b/ui/src/shared/components/FancyScrollbar.tsx @@ -1,49 +1,59 @@ import _ from 'lodash' import React, {Component} from 'react' -import PropTypes from 'prop-types' import classnames from 'classnames' import {Scrollbars} from 'react-custom-scrollbars' import {ErrorHandling} from 'src/shared/decorators/errors' -@ErrorHandling -class FancyScrollbar extends Component { - constructor(props) { - super(props) - } +interface Props { + autoHide?: boolean + autoHeight?: boolean + maxHeight?: number + className?: string + setScrollTop?: (value: React.MouseEvent) => void + style?: React.CSSProperties + scrollTop?: number + scrollLeft?: number +} - static defaultProps = { +@ErrorHandling +class FancyScrollbar extends Component { + public static defaultProps = { autoHide: true, autoHeight: false, setScrollTop: () => {}, } - updateScroll() { - if (this.ref && _.isNumber(this.props.scrollTop)) { - this.ref.scrollTop(this.props.scrollTop) + private ref: React.RefObject + + constructor(props) { + super(props) + this.ref = React.createRef() + } + + public updateScroll() { + const ref = this.ref.current + if (ref && _.isNumber(this.props.scrollTop)) { + ref.scrollTop(this.props.scrollTop) } - if (this.ref && _.isNumber(this.props.scrollLeft)) { - this.ref.scrollLeft(this.props.scrollLeft) + if (ref && _.isNumber(this.props.scrollLeft)) { + ref.scrollLeft(this.props.scrollLeft) } } - componentDidMount() { + public componentDidMount() { this.updateScroll() } - componentDidUpdate() { + public componentDidUpdate() { this.updateScroll() } - handleMakeDiv = className => props => { + public handleMakeDiv = (className: string) => (props): JSX.Element => { return
} - onRef = ref => { - this.ref = ref - } - - render() { + public render() { const { autoHide, autoHeight, @@ -59,7 +69,7 @@ class FancyScrollbar extends Component { className={classnames('fancy-scroll--container', { [className]: className, })} - ref={this.onRef} + ref={this.ref} style={style} onScroll={setScrollTop} autoHide={autoHide} @@ -79,18 +89,4 @@ class FancyScrollbar extends Component { } } -const {bool, func, node, number, string, object} = PropTypes - -FancyScrollbar.propTypes = { - children: node.isRequired, - className: string, - autoHide: bool, - autoHeight: bool, - maxHeight: number, - setScrollTop: func, - style: object, - scrollTop: number, - scrollLeft: number, -} - export default FancyScrollbar diff --git a/ui/src/shared/components/MultiGrid/MultiGrid.tsx b/ui/src/shared/components/MultiGrid/MultiGrid.tsx index f7c65906b3..f90619e4eb 100644 --- a/ui/src/shared/components/MultiGrid/MultiGrid.tsx +++ b/ui/src/shared/components/MultiGrid/MultiGrid.tsx @@ -400,7 +400,7 @@ class MultiGrid extends React.PureComponent { return this.topGridHeight } - private onScrollbarsScroll = e => { + private onScrollbarsScroll = (e: React.MouseEvent) => { const {target} = e this.onScroll(target) } From 3cc66e1a0de712b4318189043ec8e733f6204520 Mon Sep 17 00:00:00 2001 From: Brandon Farmer Date: Mon, 7 May 2018 17:29:57 -0700 Subject: [PATCH 092/104] Add more types to MultiGrid component --- ui/src/shared/components/FancyScrollbar.tsx | 21 ++++++++++++------- .../MultiGrid/CellMeasurerCacheDecorator.ts | 4 +++- .../shared/components/MultiGrid/MultiGrid.tsx | 7 +++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/ui/src/shared/components/FancyScrollbar.tsx b/ui/src/shared/components/FancyScrollbar.tsx index fed1c9e66e..5f54c67c8d 100644 --- a/ui/src/shared/components/FancyScrollbar.tsx +++ b/ui/src/shared/components/FancyScrollbar.tsx @@ -4,22 +4,27 @@ import classnames from 'classnames' import {Scrollbars} from 'react-custom-scrollbars' import {ErrorHandling} from 'src/shared/decorators/errors' +interface DefaultProps { + autoHide: boolean + autoHeight: boolean + maxHeight: number + setScrollTop: (value: React.MouseEvent) => void + style: React.CSSProperties +} + interface Props { - autoHide?: boolean - autoHeight?: boolean - maxHeight?: number className?: string - setScrollTop?: (value: React.MouseEvent) => void - style?: React.CSSProperties scrollTop?: number scrollLeft?: number } @ErrorHandling -class FancyScrollbar extends Component { +class FancyScrollbar extends Component> { public static defaultProps = { autoHide: true, autoHeight: false, + maxHeight: null, + style: {}, setScrollTop: () => {}, } @@ -32,11 +37,11 @@ class FancyScrollbar extends Component { public updateScroll() { const ref = this.ref.current - if (ref && _.isNumber(this.props.scrollTop)) { + if (ref && !_.isNil(this.props.scrollTop)) { ref.scrollTop(this.props.scrollTop) } - if (ref && _.isNumber(this.props.scrollLeft)) { + if (ref && !_.isNil(this.props.scrollLeft)) { ref.scrollLeft(this.props.scrollLeft) } } diff --git a/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts b/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts index fdb07c0ef7..42d9899a93 100644 --- a/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts +++ b/ui/src/shared/components/MultiGrid/CellMeasurerCacheDecorator.ts @@ -10,7 +10,7 @@ interface IndexParam { index: number } -export default class CellMeasurerCacheDecorator { +class CellMeasurerCacheDecorator { private cellMeasurerCache: CellMeasurerCache private columnIndexOffset: number private rowIndexOffset: number @@ -101,3 +101,5 @@ export default class CellMeasurerCacheDecorator { ) } } + +export default CellMeasurerCacheDecorator diff --git a/ui/src/shared/components/MultiGrid/MultiGrid.tsx b/ui/src/shared/components/MultiGrid/MultiGrid.tsx index f90619e4eb..585012026a 100644 --- a/ui/src/shared/components/MultiGrid/MultiGrid.tsx +++ b/ui/src/shared/components/MultiGrid/MultiGrid.tsx @@ -31,7 +31,7 @@ interface Props { scrollToRow?: () => {} onSectionRendered?: () => {} scrollToColumn?: () => {} - cellRenderer?: (arg: object) => {} + cellRenderer?: (arg: object) => JSX.Element } interface State { @@ -321,7 +321,10 @@ class MultiGrid extends React.PureComponent { ) } - public cellRendererBottomLeftGrid = ({rowIndex, ...rest}) => { + public cellRendererBottomLeftGrid = ({ + rowIndex, + ...rest + }: Partial & {rowIndex: number; key: string}): JSX.Element => { const {cellRenderer, fixedRowCount, rowCount} = this.props if (rowIndex === rowCount - fixedRowCount) { From 70950a3273ce37d8141d05f4cea505bb8fce1bd1 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Tue, 8 May 2018 09:58:50 -0700 Subject: [PATCH 093/104] Update swagger with opsgenie2 and pagerduty2 --- server/swagger.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/swagger.json b/server/swagger.json index 8f2188cf16..070d6df480 100644 --- a/server/swagger.json +++ b/server/swagger.json @@ -3678,7 +3678,9 @@ "http", "hipchat", "opsgenie", + "opsgenie2", "pagerduty", + "pagerduty2", "victorops", "email", "exec", From 317a7cbf5926a2d9b682a81f5f2f336ac79b2e4d Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 8 May 2018 10:08:02 -0700 Subject: [PATCH 094/104] Fix copy --- .../chronograf/AllUsersTableRow.tsx | 1 + ui/src/shared/components/Tags.tsx | 31 +++++++++++-------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/ui/src/admin/components/chronograf/AllUsersTableRow.tsx b/ui/src/admin/components/chronograf/AllUsersTableRow.tsx index 479433eac0..6a946aeb5d 100644 --- a/ui/src/admin/components/chronograf/AllUsersTableRow.tsx +++ b/ui/src/admin/components/chronograf/AllUsersTableRow.tsx @@ -61,6 +61,7 @@ export default class AllUsersTableRow extends PureComponent { void addMenuItems?: Item[] addMenuChoose?: (item: Item) => void @@ -21,11 +22,19 @@ const Tags: SFC = ({ onDeleteTag, addMenuItems, addMenuChoose, + confirmText, }) => { return (
{tags.map(item => { - return + return ( + + ) })} {addMenuItems && addMenuItems.length && addMenuChoose ? ( @@ -35,38 +44,34 @@ const Tags: SFC = ({ } interface TagProps { + confirmText?: string item: Item onDelete: (item: Item) => void } @ErrorHandling class Tag extends PureComponent { + public static defaultProps: Partial = { + confirmText: 'Delete', + } + public render() { - const {item} = this.props + const {item, confirmText} = this.props return ( {item.text || item.name || item} ) } - private get confirmText(): string { - const {item} = this.props - if (item.name || item.text) { - return `Delete ${item.name || item.text}?` - } - - return 'Delete?' - } - private handleClickDelete = item => () => { this.props.onDelete(item) } From 5ae612dd856a1c58d3ffeb20c326ea133b638765 Mon Sep 17 00:00:00 2001 From: Andrew Watkins Date: Tue, 8 May 2018 11:25:36 -0700 Subject: [PATCH 095/104] Add question mark --- ui/src/admin/components/chronograf/AllUsersTableRow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/admin/components/chronograf/AllUsersTableRow.tsx b/ui/src/admin/components/chronograf/AllUsersTableRow.tsx index 6a946aeb5d..6a2863d8f9 100644 --- a/ui/src/admin/components/chronograf/AllUsersTableRow.tsx +++ b/ui/src/admin/components/chronograf/AllUsersTableRow.tsx @@ -61,7 +61,7 @@ export default class AllUsersTableRow extends PureComponent { Date: Tue, 8 May 2018 11:53:38 -0700 Subject: [PATCH 096/104] Set default autorefresh interval to 0 --- ui/src/shared/constants/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/constants/index.tsx b/ui/src/shared/constants/index.tsx index 3e3d87a466..4dd35dcd25 100644 --- a/ui/src/shared/constants/index.tsx +++ b/ui/src/shared/constants/index.tsx @@ -402,7 +402,7 @@ export const HTTP_UNAUTHORIZED = 401 export const HTTP_FORBIDDEN = 403 export const HTTP_NOT_FOUND = 404 -export const AUTOREFRESH_DEFAULT = 15000 // in milliseconds +export const AUTOREFRESH_DEFAULT = 0 // in milliseconds export const GRAPH = 'graph' export const TABLE = 'table' From 0c0c4b64e507cb9d607ea3e07dded83e370a73b1 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 8 May 2018 11:56:20 -0700 Subject: [PATCH 097/104] Placeholder add func button at bottom of list --- ui/src/ifql/components/BodyBuilder.tsx | 25 ++++++++++++++++++- ui/src/ifql/components/FuncSelector.tsx | 8 +++++- .../time-machine/add-func-button.scss | 6 ++++- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index 5d32b55771..c4439d8827 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -3,6 +3,7 @@ import _ from 'lodash' import ExpressionNode from 'src/ifql/components/ExpressionNode' import VariableName from 'src/ifql/components/VariableName' +import FuncSelector from 'src/ifql/components/FuncSelector' import {FlatBody, Suggestion} from 'src/types/ifql' @@ -55,7 +56,29 @@ class BodyBuilder extends PureComponent { ) }) - return
{_.flatten(bodybuilder)}
+ return ( +
+ {_.flatten(bodybuilder)} +
+ +
+
+ ) + } + + private get newDeclarationFuncs(): string[] { + // 'JOIN' only available if there are at least 2 named declarations + return ['from', 'join', 'variable'] + } + + private createNewDeclaration = (bodyID, name, declarationID) => { + console.log(bodyID, name, declarationID) } private get funcNames() { diff --git a/ui/src/ifql/components/FuncSelector.tsx b/ui/src/ifql/components/FuncSelector.tsx index 7ed896d792..c2fc40c579 100644 --- a/ui/src/ifql/components/FuncSelector.tsx +++ b/ui/src/ifql/components/FuncSelector.tsx @@ -18,10 +18,15 @@ interface Props { bodyID: string declarationID: string onAddNode: OnAddNode + connectorVisible?: boolean } @ErrorHandling export class FuncSelector extends PureComponent { + public static defaultProps: Partial = { + connectorVisible: true, + } + constructor(props) { super(props) @@ -34,11 +39,12 @@ export class FuncSelector extends PureComponent { public render() { const {isOpen, inputText, selectedFunc} = this.state + const {connectorVisible} = this.props return (
-
+ {connectorVisible &&
} {isOpen ? ( Date: Tue, 8 May 2018 13:14:49 -0700 Subject: [PATCH 098/104] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db7ba50117..fb0dd96dec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ 1. [#3354](https://github.com/influxdata/chronograf/pull/3354): Disable template variables for non editing users 1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn 1. [#3378](https://github.com/influxdata/chronograf/pull/3378): Ensure table graphs have a consistent ux between chrome and firefox +1. [#3401](https://github.com/influxdata/chronograf/pull/3401): Change AutoRefresh interval to paused. ### Bug Fixes From 675b9a2625a1f958d83365e69554f798b6641151 Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Tue, 8 May 2018 13:55:24 -0700 Subject: [PATCH 099/104] Update url prefixer to ignore svgs Co-authored-by: Brandon Farmer --- server/url_prefixer.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/url_prefixer.go b/server/url_prefixer.go index 0a58436460..9d095fa121 100644 --- a/server/url_prefixer.go +++ b/server/url_prefixer.go @@ -5,6 +5,7 @@ import ( "bytes" "io" "net/http" + "regexp" "github.com/influxdata/chronograf" ) @@ -83,6 +84,12 @@ func (up *URLPrefixer) ServeHTTP(rw http.ResponseWriter, r *http.Request) { return } + isSVG, _ := regexp.Match(".svg$", []byte(r.URL.String())) + if isSVG { + up.Next.ServeHTTP(rw, r) + return + } + // chunked transfer because we're modifying the response on the fly, so we // won't know the final content-length rw.Header().Set("Connection", "Keep-Alive") From 40ebc8553207c6e09f0972004e9e75d2599326dc Mon Sep 17 00:00:00 2001 From: Iris Scholten Date: Tue, 8 May 2018 14:08:23 -0700 Subject: [PATCH 100/104] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index db7ba50117..d3cd531798 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ 1. [#3352](https://github.com/influxdata/chronograf/pull/3352): Fix not being able to change the source in the CEO display 1. [#3357](https://github.com/influxdata/chronograf/pull/3357): Fix only the selected template variable value getting loaded 1. [#3389](https://github.com/influxdata/chronograf/pull/3389): Fix Generic OAuth bug for GitHub Enterprise where the principal was incorrectly being checked for email being Primary and Verified +1. [#3402](https://github.com/influxdata/chronograf/pull/3402): Fix missing icons when using basepath ## v1.4.4.1 [2018-04-16] From f0b7ed006d80276200ff51429ffa116d1022e495 Mon Sep 17 00:00:00 2001 From: Alex P Date: Tue, 8 May 2018 14:22:35 -0700 Subject: [PATCH 101/104] Make linter chill out --- ui/src/ifql/components/BodyBuilder.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/ifql/components/BodyBuilder.tsx b/ui/src/ifql/components/BodyBuilder.tsx index c4439d8827..91d6977b3a 100644 --- a/ui/src/ifql/components/BodyBuilder.tsx +++ b/ui/src/ifql/components/BodyBuilder.tsx @@ -78,7 +78,10 @@ class BodyBuilder extends PureComponent { } private createNewDeclaration = (bodyID, name, declarationID) => { - console.log(bodyID, name, declarationID) + // Returning a string here so linter stops yelling + // TODO: write a real function + + return `${bodyID} / ${name} / ${declarationID}` } private get funcNames() { From d569d32c5ad2f043778de4228c1e1f0b4d3403e0 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Tue, 8 May 2018 14:34:21 -0700 Subject: [PATCH 102/104] Change clone cell notification to reflect cloned cell name --- ui/src/shared/copy/notifications.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/shared/copy/notifications.js b/ui/src/shared/copy/notifications.js index e175a152cd..61541ecf57 100644 --- a/ui/src/shared/copy/notifications.js +++ b/ui/src/shared/copy/notifications.js @@ -428,7 +428,7 @@ export const notifyCellCloned = name => ({ ...defaultSuccessNotification, icon: 'duplicate', duration: 1900, - message: `Added "${name}" to dashboard.`, + message: `Added "${name}" (Clone) to dashboard.`, }) export const notifyCellDeleted = name => ({ From 33155de4ef5ef40443f75b3f768fa24cf90b1af3 Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Tue, 8 May 2018 15:39:52 -0700 Subject: [PATCH 103/104] Get cloned cell name for notification from cloned cell generator function --- ui/src/dashboards/actions/index.js | 9 +++------ ui/src/dashboards/utils/cellGetters.js | 2 +- ui/src/shared/copy/notifications.js | 7 ------- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/ui/src/dashboards/actions/index.js b/ui/src/dashboards/actions/index.js index ce5c9678d1..325cfcc70f 100644 --- a/ui/src/dashboards/actions/index.js +++ b/ui/src/dashboards/actions/index.js @@ -19,7 +19,6 @@ import { notifyDashboardDeleted, notifyDashboardDeleteFailed, notifyCellAdded, - notifyCellCloned, notifyCellDeleted, } from 'shared/copy/notifications' @@ -319,12 +318,10 @@ export const addDashboardCellAsync = ( export const cloneDashboardCellAsync = (dashboard, cell) => async dispatch => { try { - const {data} = await addDashboardCellAJAX( - dashboard, - getClonedDashboardCell(dashboard, cell) - ) + const clonedCell = getClonedDashboardCell(dashboard, cell) + const {data} = await addDashboardCellAJAX(dashboard, clonedCell) dispatch(addDashboardCell(dashboard, data)) - dispatch(notify(notifyCellCloned(cell.name))) + dispatch(notify(notifyCellAdded(clonedCell.name))) } catch (error) { console.error(error) dispatch(errorThrown(error)) diff --git a/ui/src/dashboards/utils/cellGetters.js b/ui/src/dashboards/utils/cellGetters.js index 44ba00abe4..4cbe6d638d 100644 --- a/ui/src/dashboards/utils/cellGetters.js +++ b/ui/src/dashboards/utils/cellGetters.js @@ -89,7 +89,7 @@ export const getNewDashboardCell = (dashboard, cellType) => { export const getClonedDashboardCell = (dashboard, cloneCell) => { const {x, y} = getNextAvailablePosition(dashboard, cloneCell) - const name = `${cloneCell.name} (Clone)` + const name = `${cloneCell.name} (clone)` return {...cloneCell, x, y, name} } diff --git a/ui/src/shared/copy/notifications.js b/ui/src/shared/copy/notifications.js index 61541ecf57..e26610efd7 100644 --- a/ui/src/shared/copy/notifications.js +++ b/ui/src/shared/copy/notifications.js @@ -424,13 +424,6 @@ export const notifyCellAdded = name => ({ message: `Added "${name}" to dashboard.`, }) -export const notifyCellCloned = name => ({ - ...defaultSuccessNotification, - icon: 'duplicate', - duration: 1900, - message: `Added "${name}" (Clone) to dashboard.`, -}) - export const notifyCellDeleted = name => ({ ...defaultDeletionNotification, icon: 'dash-h', From 07522e6654cbe78a8c201f3fa664bb90496a989c Mon Sep 17 00:00:00 2001 From: ebb-tide Date: Tue, 8 May 2018 15:46:22 -0700 Subject: [PATCH 104/104] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe82705188..0580cb0561 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ 1. [#3353](https://github.com/influxdata/chronograf/pull/3353): YAxisLabels in Dashboard Graph Builder not showing until graph is redrawn 1. [#3378](https://github.com/influxdata/chronograf/pull/3378): Ensure table graphs have a consistent ux between chrome and firefox 1. [#3401](https://github.com/influxdata/chronograf/pull/3401): Change AutoRefresh interval to paused. +1. [#3404](https://github.com/influxdata/chronograf/pull/3404): Get cloned cell name for notification from cloned cell generator function ### Bug Fixes