feat(ui): update design of flux editor toolbar (#17488)

* feat(ui): redesign flux editor

* chore(ui): delete threesizer

* fix(ui): repair explorer e2e tests

* refactor(ui): make variables easier to test

* fix(ui): appease prettier

* fix(ui): update selectors in dashboards view test
pull/17525/head
alexpaxton 2020-03-31 14:45:47 -07:00 committed by GitHub
parent 1816425e52
commit f617657e8d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
30 changed files with 541 additions and 1448 deletions

View File

@ -133,17 +133,20 @@ describe('Dashboard', () => {
cy.getByTestID('switch-to-script-editor').click()
cy.getByTestID('toolbar-tab').click()
// check to see if the default timeRange variables are available
cy.get('.variables-toolbar--label').contains('timeRangeStart')
cy.get('.variables-toolbar--label').contains('timeRangeStop')
cy.get('.variables-toolbar--label')
cy.get('.flux-toolbar--list-item').contains('timeRangeStart')
cy.get('.flux-toolbar--list-item').contains('timeRangeStop')
cy.get('.flux-toolbar--list-item')
.first()
.click()
.within(() => {
cy.get('.cf-button').click()
})
cy.getByTestID('flux-editor')
.should('be.visible')
.click()
.focused()
.type(' ')
cy.get('.variables-toolbar--label')
cy.get('.flux-toolbar--list-item')
.eq(2)
.click()
cy.getByTestID('save-cell--button').click()
@ -182,7 +185,7 @@ describe('Dashboard', () => {
.should('equal', 'c2')
cy.getByTestID('toolbar-tab').click()
cy.get('.variables-toolbar--label')
cy.get('.flux-toolbar--list-item')
.first()
.trigger('mouseover')
// toggle the variable dropdown in the VEO cell dashboard
@ -241,7 +244,7 @@ describe('Dashboard', () => {
.should('equal', 'v2')
cy.getByTestID('toolbar-tab').click()
cy.get('.variables-toolbar--label')
cy.get('.flux-toolbar--list-item')
.eq(2)
.trigger('mouseover')
// toggle the variable dropdown in the VEO cell dashboard

View File

@ -369,7 +369,7 @@ describe('DataExplorer', () => {
cy.getByTestID('switch-to-script-editor')
.should('be.visible')
.click()
cy.getByTestID('flux-function aggregate.rate').click()
cy.getByTestID('flux--aggregate.rate--inject').click()
// check to see if import is defaulted to the top
cy.get('.view-line')
.first()
@ -384,9 +384,9 @@ describe('DataExplorer', () => {
})
// can hover over flux functions
cy.getByTestID('toolbar-popover--contents').should('not.exist')
cy.getByTestID('flux-function aggregateWindow').trigger('mouseover')
cy.getByTestID('toolbar-popover--contents').should('exist')
cy.getByTestID('flux-docs--aggregateWindow').should('not.exist')
cy.getByTestID('flux--aggregateWindow').trigger('mouseover')
cy.getByTestID('flux-docs--aggregateWindow').should('exist')
cy.getByTestID('switch-query-builder-confirm--button').click()
@ -463,12 +463,12 @@ describe('DataExplorer', () => {
it('imports the appropriate packages to build a query', () => {
cy.getByTestID('functions-toolbar-tab').click()
cy.getByTestID('flux-function from').click()
cy.getByTestID('flux-function range').click()
cy.getByTestID('flux-function math.abs').click()
cy.getByTestID('flux-function math.floor').click()
cy.getByTestID('flux-function strings.title').click()
cy.getByTestID('flux-function strings.trim').click()
cy.getByTestID('flux--from--inject').click()
cy.getByTestID('flux--range--inject').click()
cy.getByTestID('flux--math.abs--inject').click()
cy.getByTestID('flux--math.floor--inject').click()
cy.getByTestID('flux--strings.title--inject').click()
cy.getByTestID('flux--strings.trim--inject').click()
cy.wait(100)
@ -490,7 +490,7 @@ describe('DataExplorer', () => {
it('can use the function selector to build a query', () => {
cy.getByTestID('functions-toolbar-tab').click()
cy.getByTestID('flux-function from').click()
cy.getByTestID('flux--from--inject').click()
getTimeMachineText().then(text => {
const expected = FROM.example
@ -498,7 +498,7 @@ describe('DataExplorer', () => {
cy.fluxEqual(text, expected).should('be.true')
})
cy.getByTestID('flux-function range').click()
cy.getByTestID('flux--range--inject').click()
getTimeMachineText().then(text => {
const expected = `${FROM.example}|>${RANGE.example}`
@ -506,7 +506,7 @@ describe('DataExplorer', () => {
cy.fluxEqual(text, expected).should('be.true')
})
cy.getByTestID('flux-function mean').click()
cy.getByTestID('flux--mean--inject').click()
getTimeMachineText().then(text => {
const expected = `${FROM.example}|>${RANGE.example}|>${MEAN.example}`
@ -517,7 +517,7 @@ describe('DataExplorer', () => {
it('can filter aggregation functions by name from script editor mode', () => {
cy.get('.cf-input-field').type('covariance')
cy.getByTestID('toolbar-function').should('have.length', 1)
cy.get('.flux-toolbar--list-item').should('have.length', 1)
})
it('shows the empty state when the query returns no results', () => {
@ -556,13 +556,15 @@ describe('DataExplorer', () => {
cy.getByTestID('toolbar-tab').click()
// checks to see if the default variables exist
cy.get('.variables-toolbar--label').contains('timeRangeStart')
cy.get('.variables-toolbar--label').contains('timeRangeStop')
cy.get('.variables-toolbar--label').contains('windowPeriod')
cy.getByTestID('variable--timeRangeStart')
cy.getByTestID('variable--timeRangeStop')
cy.getByTestID('variable--windowPeriod')
//insert variable name by clicking on variable
cy.get('.variables-toolbar--label')
cy.get('.flux-toolbar--variable')
.first()
.click()
.within(() => {
cy.contains('Inject').click()
})
cy.getByTestID('save-query-as').click()
cy.getByTestID('task--radio-button').click()

View File

@ -88,7 +88,7 @@ const FluxEditorMonaco: FC<Props> = ({
}
return (
<div className="time-machine-editor" data-testid="flux-editor">
<div className="flux-editor--monaco" data-testid="flux-editor">
<GetResources resources={[ResourceType.Buckets]}>
<FluxBucketProvider />
</GetResources>

View File

@ -1,330 +0,0 @@
import React, {
CSSProperties,
PureComponent,
ReactElement,
MouseEvent,
} from 'react'
import classnames from 'classnames'
import calculateSize from 'calculate-size'
import DivisionHeader from 'src/shared/components/threesizer/DivisionHeader'
import {
HANDLE_VERTICAL,
HANDLE_HORIZONTAL,
MIN_HANDLE_PIXELS,
} from 'src/shared/constants/index'
const NOOP = () => {}
interface Props {
handlePixels: number
id: string
size: number
name: string
offset: number
draggable: boolean
orientation: string
handleDisplay: string
style: CSSProperties
activeHandleID: string
headerOrientation: string
render: (visibility: string, pixels: number) => ReactElement<any>
onHandleStartDrag: (id: string, e: MouseEvent<HTMLElement>) => void
onDoubleClick: (id: string) => void
onMaximize: (id: string) => void
onMinimize: (id: string) => void
headerButtons: JSX.Element[]
}
class Division extends PureComponent<Props> {
public static defaultProps = {
name: '',
handleDisplay: 'visible',
style: {},
headerButtons: [],
}
private collapseThreshold: number = 0
private divisionRef: React.RefObject<HTMLDivElement>
private divisionPixels: number = 0
constructor(props) {
super(props)
this.divisionRef = React.createRef<HTMLDivElement>()
}
public componentDidMount() {
const {name} = this.props
this.calcDivisionPixels()
if (!name) {
return 0
}
const {width} = calculateSize(name, {
font: '"Roboto", Helvetica, Arial, Tahoma, Verdana, sans-serif',
fontSize: '16px',
fontWeight: '500',
})
const NAME_OFFSET = 96
this.collapseThreshold = width + NAME_OFFSET
}
public componentDidUpdate() {
this.calcDivisionPixels()
}
public render() {
const {render} = this.props
return (
<div
className={this.containerClass}
style={this.containerStyle}
ref={this.divisionRef}
>
{this.renderDragHandle}
<div className={this.contentsClass} style={this.contentStyle}>
{this.renderHeader}
<div className="threesizer--body">
{render(this.visibility, this.divisionPixels)}
</div>
</div>
</div>
)
}
private get renderHeader(): JSX.Element {
const {name, headerButtons, orientation} = this.props
if (!name) {
return null
}
if (orientation === HANDLE_VERTICAL) {
return (
<DivisionHeader
buttons={headerButtons}
onMinimize={this.handleMinimize}
onMaximize={this.handleMaximize}
/>
)
}
}
private get visibility(): string {
if (this.props.size === 0) {
return 'hidden'
}
return 'visible'
}
private get title(): string {
return 'Drag to resize.\nDouble click to expand.'
}
private get contentStyle(): CSSProperties {
if (this.props.orientation === HANDLE_HORIZONTAL) {
return {
height: `calc(100% - ${this.handlePixels}px)`,
}
}
return {
width: `calc(100% - ${this.handlePixels}px)`,
}
}
private get renderDragHandle(): JSX.Element {
const {draggable} = this.props
return (
<div
style={this.handleStyle}
title={this.title}
draggable={draggable}
onDragStart={this.drag}
className={this.handleClass}
onDoubleClick={this.handleDoubleClick}
>
{this.renderDragHandleContents}
</div>
)
}
private get renderDragHandleContents(): JSX.Element {
const {name, handlePixels, orientation, headerButtons} = this.props
if (!name) {
return
}
if (
orientation === HANDLE_HORIZONTAL &&
handlePixels >= MIN_HANDLE_PIXELS
) {
return (
<DivisionHeader
buttons={headerButtons}
onMinimize={this.handleMinimize}
onMaximize={this.handleMaximize}
name={name}
/>
)
}
if (handlePixels >= MIN_HANDLE_PIXELS) {
return <div className={this.titleClass}>{name}</div>
}
}
private get handleStyle(): CSSProperties {
const {handleDisplay: display, orientation, handlePixels} = this.props
if (orientation === HANDLE_HORIZONTAL) {
return {
display,
height: `${handlePixels}px`,
}
}
return {
display,
width: `${handlePixels}px`,
}
}
private get containerStyle(): CSSProperties {
const {style, orientation} = this.props
if (orientation === HANDLE_HORIZONTAL) {
return {
...style,
height: this.size,
}
}
return {
...style,
width: this.size,
}
}
private get size(): string {
const {size, offset} = this.props
return `calc((100% - ${offset}px) * ${size} + ${this.handlePixels}px)`
}
private get handlePixels(): number {
if (this.props.handleDisplay === 'none') {
return 0
}
return this.props.handlePixels
}
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,
})
}
private get handleClass(): string {
const {draggable, orientation, name} = this.props
const collapsed = orientation === HANDLE_VERTICAL && this.isTitleObscured
return classnames('threesizer--handle', {
'threesizer--collapsed': collapsed,
disabled: !draggable,
dragging: this.isDragging,
vertical: orientation === HANDLE_VERTICAL,
horizontal: orientation === HANDLE_HORIZONTAL,
named: name,
})
}
private get contentsClass(): string {
const {headerOrientation, size} = this.props
return classnames(`threesizer--contents ${headerOrientation}`, {
'no-shadows': !size,
})
}
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.divisionRef.current || this.props.size >= 0.33) {
return false
}
const {width} = this.divisionRef.current.getBoundingClientRect()
return width <= this.collapseThreshold
}
private get isDragging(): boolean {
const {id, activeHandleID} = this.props
return id === activeHandleID
}
private drag = e => {
const {draggable, id} = this.props
if (!draggable) {
return NOOP
}
this.props.onHandleStartDrag(id, e)
}
private handleDoubleClick = (): void => {
const {onDoubleClick, id} = this.props
onDoubleClick(id)
}
private handleMinimize = (): void => {
const {id, onMinimize} = this.props
onMinimize(id)
}
private handleMaximize = (): void => {
const {id, onMaximize} = this.props
onMaximize(id)
}
private calcDivisionPixels = (): void => {
const {orientation} = this.props
const {clientWidth, clientHeight} = this.divisionRef.current
let divisionPixels = clientWidth
if (orientation === HANDLE_HORIZONTAL) {
divisionPixels = clientHeight
}
this.divisionPixels = divisionPixels
}
}
export default Division

View File

@ -1,33 +0,0 @@
import React, {PureComponent} from 'react'
interface Props {
onMinimize: () => void
onMaximize: () => void
buttons: JSX.Element[]
name?: string
}
class DivisionHeader extends PureComponent<Props> {
public render() {
return (
<div className="threesizer--header">
{this.renderName}
<div className="threesizer--header-controls">
{this.props.buttons.map(b => b)}
</div>
</div>
)
}
private get renderName(): JSX.Element {
const {name} = this.props
if (!name) {
return
}
return <div className="threesizer--header-name">{name}</div>
}
}
export default DivisionHeader

View File

@ -1,226 +0,0 @@
/*
Resizable Container with 3 divisions
------------------------------------------------------------------------------
*/
.threesizer {
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
&.dragging .threesizer--division {
@include no-user-select();
pointer-events: none;
}
&.vertical {
flex-direction: row;
}
&.horizontal {
flex-direction: column;
}
}
.threesizer--division {
/* 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: row;
}
&.horizontal {
flex-direction: column;
}
}
/* Draggable Handle With Title */
.threesizer--handle {
@include no-user-select();
background-color: $g4-onyx;
transition: background-color 0.25s ease, color 0.25s ease;
&.vertical {
border-right: solid 2px $g3-castle;
&:hover,
&.dragging {
cursor: col-resize;
}
}
&.horizontal {
border-bottom: solid 2px $g3-castle;
&:hover,
&.dragging {
cursor: row-resize;
}
}
&:hover {
&.disabled {
cursor: pointer;
}
color: $g16-pearl;
background-color: $g5-pepper;
}
&.dragging {
color: $c-laser;
background-color: $g5-pepper;
}
}
.threesizer--title {
padding-left: 14px;
position: relative;
font-size: 14px;
font-weight: 600;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $g11-sidewalk;
z-index: 1;
transition: transform 0.25s ease, letter-spacing 0.25s ease;
&.vertical {
transform: translate(20px, 14px);
&.threesizer--collapsed {
transform: translate(0, 5px) rotate(90deg) scale(0.75);
letter-spacing: 0.15em;
}
}
}
$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 {
display: flex;
align-items: stretch;
flex-wrap: nowrap;
position: relative;
&.horizontal {
flex-direction: row;
}
&.vertical {
flex-direction: column;
} // 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;
}
// Header
.threesizer--header {
display: flex;
align-items: center;
justify-content: flex-end;
padding: 0 11px;
background-color: $g2-kevlar;
.horizontal>& {
width: 44px;
border-right: 2px solid $g4-onyx;
}
.vertical>& {
height: 44px;
border-bottom: 2px solid $g4-onyx;
}
}
.threesizer--header-name {
font-size: 14px;
font-weight: 600;
white-space: nowrap;
text-transform: uppercase;
letter-spacing: 0.05em;
color: $g11-sidewalk;
padding-left: 4px;
padding-right: 2px;
}
.threesizer--header-controls {
display: flex;
align-items: center;
flex-wrap: nowrap;
> * {
margin-left: 4px;
}
}
.threesizer--body {
position: relative;
.horizontal>&:only-child {
width: 100%;
}
.vertical>&:only-child {
height: 100%;
}
.threesizer--header+& {
flex: 1 1 0;
overflow: hidden;
}
}
// Division context menus
.threesizer-context--buttons {
display: flex;
justify-content: flex-end;
margin-top: 10px;
}
// Header Dropdown Menu
.threesizer--menu {
.dropdown-menu {
right: 0;
}
}
// Hide Header children when collapsed
.threesizer--handle.vertical.threesizer--collapsed+.threesizer--contents>.threesizer--header>* {
display: none;
}
// When threesizer has horizontal handles, division headers appear inside
// the drag handle and styles must adapt
.threesizer--handle.horizontal.named {
background-color: transparent;
border: 0;
.threesizer--header {
border-right: 0;
border-bottom: 2px solid $g4-onyx;
width: 100%;
height: 100%;
justify-content: space-between;
transition: background-color 0.25s ease;
}
&:hover .threesizer--header,
&.dragging .threesizer--header {
background-color: $g3-castle;
}
}

View File

@ -1,508 +0,0 @@
import React, {Component, ReactElement, MouseEvent, CSSProperties} from 'react'
import classnames from 'classnames'
import uuid from 'uuid'
import _ from 'lodash'
import Division from 'src/shared/components/threesizer/Division'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {
HANDLE_NONE,
HANDLE_PIXELS,
HANDLE_HORIZONTAL,
HANDLE_VERTICAL,
MIN_SIZE,
MAX_SIZE,
} from 'src/shared/constants/'
const initialDragEvent = {
percentX: 0,
percentY: 0,
mouseX: null,
mouseY: null,
}
interface State {
activeHandleID: string
divisions: DivisionState[]
dragDirection: string
dragEvent: any
}
export interface DivisionProps {
name?: string
handleDisplay?: string
handlePixels?: number
style?: CSSProperties
size?: number
headerButtons?: JSX.Element[]
headerOrientation?: string
render: (visibility: string, pixels: number) => ReactElement<any>
}
interface DivisionState extends DivisionProps {
id: string
size: number
}
interface Props {
divisions: DivisionProps[]
orientation: string
containerClass: string
}
@ErrorHandling
class Threesizer extends Component<Props, State> {
public static defaultProps = {
orientation: HANDLE_HORIZONTAL,
containerClass: '',
}
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 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
if (_.isEqual(dragEvent, prevState.dragEvent)) {
return
}
this.percentChangeX = this.pixelsToPercentX(
prevState.dragEvent.mouseX,
dragEvent.mouseX
)
this.percentChangeY = this.pixelsToPercentY(
prevState.dragEvent.mouseY,
dragEvent.mouseY
)
const {percentX, percentY} = dragEvent
const {dragEvent: prevDrag} = prevState
if (orientation === HANDLE_VERTICAL) {
const left = percentX < prevDrag.percentX
if (left) {
return this.move.left()
}
return this.move.right()
}
const up = percentY < prevDrag.percentY
if (up) {
return this.move.up()
}
return this.move.down()
}
public render() {
const {activeHandleID, divisions} = this.state
const {orientation} = this.props
return (
<div
className={this.className}
onMouseUp={this.handleStopDrag}
onMouseMove={this.handleDrag}
ref={r => (this.containerRef = r)}
>
{divisions.map((d, i) => {
const headerOrientation = _.get(d, 'headerOrientation', orientation)
return (
<Division
key={d.id}
id={d.id}
name={d.name}
size={d.size}
style={d.style}
offset={this.offset}
draggable={i > 0}
orientation={orientation}
handlePixels={d.handlePixels}
handleDisplay={d.handleDisplay}
activeHandleID={activeHandleID}
onMaximize={this.handleMaximize}
onMinimize={this.handleMinimize}
headerOrientation={headerOrientation}
onDoubleClick={this.handleDoubleClick}
render={this.props.divisions[i].render}
onHandleStartDrag={this.handleStartDrag}
headerButtons={this.props.divisions[i].headerButtons}
/>
)
})}
</div>
)
}
private get offset(): number {
const handlesPixelCount = this.state.divisions.reduce((acc, d) => {
if (d.handleDisplay === HANDLE_NONE) {
return acc
}
return acc + d.handlePixels
}, 0)
return handlesPixelCount
}
private get className(): string {
const {orientation, containerClass} = this.props
const {activeHandleID} = this.state
return classnames(`threesizer ${containerClass}`, {
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: d.size || size,
handlePixels: d.handlePixels || HANDLE_PIXELS,
}))
}
private handleDoubleClick = (id: string): void => {
const clickedDiv = this.state.divisions.find(d => d.id === id)
if (!clickedDiv) {
return
}
const isMaxed = clickedDiv.size === 1
if (isMaxed) {
return this.equalize()
}
const divisions = this.state.divisions.map(d => {
if (d.id !== id) {
return {...d, size: 0}
}
return {...d, size: 1}
})
this.setState({divisions})
}
private handleMaximize = (id: string): void => {
const maxDiv = this.state.divisions.find(d => d.id === id)
if (!maxDiv) {
return
}
const divisions = this.state.divisions.map(d => {
if (d.id !== id) {
return {...d, size: 0}
}
return {...d, size: 1}
})
this.setState({divisions})
}
private handleMinimize = (id: string): void => {
const minDiv = this.state.divisions.find(d => d.id === id)
const numDivisions = this.state.divisions.length
if (!minDiv) {
return
}
let size
if (numDivisions <= 1) {
size = 1
} else {
size = 1 / (this.state.divisions.length - 1)
}
const divisions = this.state.divisions.map(d => {
if (d.id !== id) {
return {...d, size}
}
return {...d, size: 0}
})
this.setState({divisions})
}
private equalize = () => {
const denominator = this.state.divisions.length
const divisions = this.state.divisions.map(d => {
return {...d, size: 1 / denominator}
})
this.setState({divisions})
}
private handleStartDrag = (activeHandleID, e: MouseEvent<HTMLElement>) => {
const dragEvent = this.mousePosWithinContainer(e)
this.setState({activeHandleID, dragEvent})
}
private handleStopDrag = () => {
this.setState({activeHandleID: '', dragEvent: initialDragEvent})
}
private mousePosWithinContainer = (e: MouseEvent<HTMLElement>) => {
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 || !endValue) {
return 0
}
const delta = Math.abs(startValue - endValue)
const {width} = this.containerRef.getBoundingClientRect()
return delta / width
}
private pixelsToPercentY = (startValue, endValue) => {
if (!startValue || !endValue) {
return 0
}
const delta = startValue - endValue
const {height} = this.containerRef.getBoundingClientRect()
return Math.abs(delta / height)
}
private handleDrag = (e: MouseEvent<HTMLElement>) => {
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) => {
if (!activePosition) {
return d
}
const first = i === 0
const before = i === activePosition - 1
const current = i === activePosition
if (first && !before) {
const second = this.state.divisions[1]
if (second && second.size === 0) {
return {...d, size: this.shorter(d.size)}
}
return {...d}
}
if (before) {
return {...d, size: this.shorter(d.size)}
}
if (current) {
return {...d, size: this.taller(d.size)}
}
return {...d}
})
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 && 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 && 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
const current = i === activePosition
const after = i === activePosition + 1
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 (above && above.size === 0) {
return {...d, size: this.shorter(d.size)}
}
return {...d}
}
return {...d}
})
this.setState({divisions})
}
private taller = (size: number): number => {
const newSize = size + this.percentChangeY
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 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
}
}
export default Threesizer

View File

@ -14,7 +14,6 @@
@import 'src/shared/components/avatar/Avatar.scss';
@import 'src/shared/components/tables/TableGraphs.scss';
@import 'src/shared/components/notifications/Notifications.scss';
@import 'src/shared/components/threesizer/Threesizer.scss';
@import 'src/shared/components/graph_tips/GraphTips.scss';
@import 'src/shared/components/cells/Dashboards.scss';
@import 'src/shared/components/code_mirror/CodeMirror.scss';
@ -81,16 +80,14 @@
@import 'src/timeMachine/components/SelectorList.scss';
@import 'src/timeMachine/components/Queries.scss';
@import 'src/timeMachine/components/EditorShortcutsTooltip.scss';
@import 'src/timeMachine/components/SearchBar.scss';
@import 'src/timeMachine/components/QueriesSwitcher.scss';
@import 'src/timeMachine/components/TimeMachineFluxEditor.scss';
@import 'src/timeMachine/components/FluxToolbar.scss';
@import 'src/timeMachine/components/TagSelector.scss';
@import 'src/timeMachine/components/QueryTab.scss';
@import 'src/timeMachine/components/TimeMachine.scss';
@import 'src/timeMachine/components/QueryBuilder.scss';
@import 'src/timeMachine/components/RawFluxDataTable.scss';
@import 'src/timeMachine/components/ToolbarTab.scss';
@import 'src/timeMachine/components/variableToolbar/VariableToolbar.scss';
@import 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar.scss';
@import 'src/timeMachine/components/view_options/HistogramOptions.scss';
@import 'src/timeMachine/components/view_options/ViewOptions.scss';

View File

@ -0,0 +1,163 @@
$flux-toolbar--search-height: $cf-form-sm-height + $cf-marg-c;
$flux-toolbar--item-height: $cf-marg-d;
.flux-toolbar {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
flex: 1 0 0;
}
.flux-toolbar--tabs {
width: 100%;
display: flex;
align-items: stretch;
height: $cf-form-md-height;
flex: 0 0 $cf-form-md-height;
}
.flux-toolbar--tab {
border-radius: $cf-radius $cf-radius 0 0;
flex: 1 0 0;
display: flex;
justify-content: center;
align-items: center;
font-size: $cf-form-sm-font;
line-height: $cf-form-sm-font;
font-weight: $cf-font-weight--medium;
background-color: transparent;
color: $g8-storm;
margin-right: $cf-marg-a;
transition: color 0.25s ease, background-color 0.25s ease;
&:last-child {
margin-right: 0;
}
&:hover {
cursor: pointer;
background-color: $g2-kevlar;
color: $g13-mist;
}
&.flux-toolbar--tab__active {
background-color: $g3-castle;
color: $g15-platinum;
}
}
.flux-toolbar--tab-contents {
background-color: $g3-castle;
flex: 1 0 0;
position: relative;
border-radius: 0 0 $cf-radius $cf-radius;
overflow: hidden;
}
.flux-toolbar--search {
padding: 0 $cf-marg-b;
display: flex;
align-items: center;
width: 100%;
height: $flux-toolbar--search-height;
}
.flux-toolbar--scroll-area {
height: calc(100% - #{$flux-toolbar--search-height}) !important;
}
.flux-toolbar--list {
width: 100%;
display: flex;
flex-direction: column;
align-items: stretch;
}
.flux-toolbar--list-item,
.flux-toolbar--heading {
font-size: $cf-form-sm-font;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.flux-toolbar--list-item {
padding-left: $cf-marg-b;
padding-right: $cf-marg-b + $cf-marg-a + $cf-border;
height: $flux-toolbar--item-height;
line-height: $flux-toolbar--item-height;
font-family: $cf-code-font;
font-weight: $cf-font-weight--bold;
transition: background-color 0.25s ease, color 0.25s ease;
background-color: $g3-castle;
display: flex;
align-items: center;
justify-content: space-between;
> code {
font-size: $cf-form-sm-font;
background-color: $g1-raven;
border-radius: $cf-radius-sm;
padding: 0 $cf-marg-b;
height: $cf-form-xs-height;
line-height: $cf-form-xs-height;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background-color 0.25s ease, color 0.25s ease, text-shadow 0.25s ease;
}
.flux-toolbar--injector {
opacity: 0;
transition: opacity 0.25s ease, color 0.25s ease, box-shadow 0.25s ease;
}
&:hover {
background-color: $g4-onyx;
> code {
background-color: $g2-kevlar;
}
.flux-toolbar--injector {
opacity: 1;
}
}
}
.flux-toolbar--heading {
padding: $cf-marg-b $cf-marg-c;
padding-bottom: $cf-marg-b + $cf-marg-a;
font-weight: $cf-font-weight--medium;
color: $g15-platinum;
border-bottom: $cf-border solid $g5-pepper;
margin-bottom: $cf-marg-a;
}
.flux-toolbar--variable {
> code {
color: $c-honeydew;
}
&:hover > code {
color: $c-krypton;
text-shadow: 0 0 4px $c-rainforest;
}
}
.flux-toolbar--function {
> code {
color: $c-laser;
}
&:hover > code {
color: $c-hydrogen;
text-shadow: 0 0 4px $c-ocean;
}
}
.flux-toolbar--popover {
font-size: 13px;
padding: $cf-marg-c + $cf-marg-a;
}

View File

@ -0,0 +1,63 @@
// Libraries
import React, {FC, useState} from 'react'
// Components
import FluxFunctionsToolbar from 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar'
import VariableToolbar from 'src/timeMachine/components/variableToolbar/VariableToolbar'
import FluxToolbarTab from 'src/timeMachine/components/FluxToolbarTab'
// Types
import {FluxToolbarFunction} from 'src/types'
interface Props {
activeQueryBuilderTab: string
onInsertFluxFunction: (func: FluxToolbarFunction) => void
onInsertVariable: (variableName: string) => void
}
type FluxToolbarTabs = 'functions' | 'variables'
const FluxToolbar: FC<Props> = ({
activeQueryBuilderTab,
onInsertFluxFunction,
onInsertVariable,
}) => {
const [activeTab, setActiveTab] = useState<FluxToolbarTabs>('functions')
const handleTabClick = (id: FluxToolbarTabs): void => {
setActiveTab(id)
}
let activeToolbar = (
<FluxFunctionsToolbar onInsertFluxFunction={onInsertFluxFunction} />
)
if (activeTab === 'variables') {
activeToolbar = <VariableToolbar onClickVariable={onInsertVariable} />
}
return (
<div className="flux-toolbar">
<div className="flux-toolbar--tabs">
<FluxToolbarTab
id="functions"
onClick={handleTabClick}
name="Functions"
active={activeTab === 'functions'}
testID="functions-toolbar-tab"
/>
{activeQueryBuilderTab !== 'customCheckQuery' && (
<FluxToolbarTab
id="variables"
onClick={handleTabClick}
name="Variables"
active={activeTab === 'variables'}
/>
)}
</div>
<div className="flux-toolbar--tab-contents">{activeToolbar}</div>
</div>
)
}
export default FluxToolbar

View File

@ -19,7 +19,7 @@ interface State {
const DEBOUNCE_MS = 100
class SearchBar extends PureComponent<Props, State> {
class FluxToolbarSearch extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
@ -32,7 +32,7 @@ class SearchBar extends PureComponent<Props, State> {
public render() {
return (
<div className="search-bar">
<div className="flux-toolbar--search">
<Input
type={InputType.Text}
icon={IconFont.Search}
@ -53,4 +53,4 @@ class SearchBar extends PureComponent<Props, State> {
}
}
export default SearchBar
export default FluxToolbarSearch

View File

@ -0,0 +1,40 @@
// Libraries
import React, {FC} from 'react'
import classnames from 'classnames'
interface Props {
onClick: (id: string) => void
id: string
name: string
active: boolean
testID?: string
}
const ToolbarTab: FC<Props> = ({
onClick,
name,
active,
testID = 'toolbar-tab',
id,
}) => {
const toolbarTabClass = classnames('flux-toolbar--tab', {
'flux-toolbar--tab__active': active,
})
const handleClick = (): void => {
onClick(id)
}
return (
<div
className={toolbarTabClass}
onClick={handleClick}
title={name}
data-testid={testID}
>
{name}
</div>
)
}
export default ToolbarTab

View File

@ -1,8 +0,0 @@
@import "src/style/modules";
.search-bar {
padding: $ix-marg-b;
flex-shrink: 0;
border-bottom: $ix-border solid $g4-onyx;
background-color: $g3-castle;
}

View File

@ -1,24 +1,32 @@
.toolbar-tab-container {
width: 100%;
display: inline-flex;
align-items: stretch;
height: 38px;
background-color: $g2-kevlar;
padding: $ix-marg-b;
padding-bottom: 0;
}
$flux-editor--right-panel: 330px;
.time-machine-flux-editor {
.flux-editor {
position: absolute;
top: 0;
top: $cf-marg-b;
right: 0;
bottom: 0;
left: 0;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: stretch;
}
.time-machine-editor {
// Monaco Editor
.flux-editor--left-panel {
flex: 1 0 0;
position: relative;
border-radius: $cf-radius;
overflow: hidden;
}
// Variables / Functions List
.flux-editor--right-panel {
flex: 0 0 ($flux-editor--right-panel - $cf-marg-b);
margin-left: $cf-marg-b;
display: flex;
}
.flux-editor--monaco {
position: absolute;
top: 0;
left: 0;
@ -27,10 +35,9 @@
}
.time-machine-editor--embedded {
@extend .time-machine-editor;
border-radius: 4px;
border: 2px solid $g5-pepper;
@extend .flux-editor--monaco;
border-radius: $cf-radius;
border: $cf-border solid $g5-pepper;
overflow: hidden;
}

View File

@ -4,10 +4,7 @@ import {connect} from 'react-redux'
// Components
import FluxEditor from 'src/shared/components/FluxMonacoEditor'
import Threesizer from 'src/shared/components/threesizer/Threesizer'
import FluxFunctionsToolbar from 'src/timeMachine/components/fluxFunctionsToolbar/FluxFunctionsToolbar'
import VariableToolbar from 'src/timeMachine/components/variableToolbar/VariableToolbar'
import ToolbarTab from 'src/timeMachine/components/ToolbarTab'
import FluxToolbar from 'src/timeMachine/components/FluxToolbar'
// Actions
import {setActiveQueryText} from 'src/timeMachine/actions'
@ -20,9 +17,6 @@ import {
generateImport,
} from 'src/timeMachine/utils/insertFunction'
// Constants
import {HANDLE_VERTICAL, HANDLE_NONE} from 'src/shared/constants'
// Types
import {AppState, FluxToolbarFunction, EditorType} from 'src/types'
@ -44,17 +38,8 @@ const TimeMachineFluxEditor: FC<Props> = ({
onSetActiveQueryText,
activeTab,
}) => {
const [displayFluxFunctions, setDisplayFluxFunctions] = useState(true)
const [editorInstance, setEditorInstance] = useState<EditorType>(null)
const showFluxFunctions = () => {
setDisplayFluxFunctions(true)
}
const hideFluxFunctions = () => {
setDisplayFluxFunctions(false)
}
const handleInsertVariable = (variableName: string): void => {
const p = editorInstance.getPosition()
editorInstance.executeEdits('', [
@ -112,59 +97,23 @@ const TimeMachineFluxEditor: FC<Props> = ({
onSetActiveQueryText(editorInstance.getValue())
}
const divisions = [
{
size: 0.75,
handleDisplay: HANDLE_NONE,
render: () => {
return (
<FluxEditor
script={activeQueryText}
onChangeScript={onSetActiveQueryText}
onSubmitScript={onSubmitQueries}
setEditorInstance={setEditorInstance}
/>
)
},
},
{
style: {overflow: 'hidden'},
render: () => {
return (
<>
<div className="toolbar-tab-container">
{activeTab !== 'customCheckQuery' && (
<ToolbarTab
onSetActive={hideFluxFunctions}
name="Variables"
active={!displayFluxFunctions}
/>
)}
<ToolbarTab
onSetActive={showFluxFunctions}
name="Functions"
active={displayFluxFunctions}
testID="functions-toolbar-tab"
/>
</div>
{displayFluxFunctions ? (
<FluxFunctionsToolbar
onInsertFluxFunction={handleInsertFluxFunction}
/>
) : (
<VariableToolbar onClickVariable={handleInsertVariable} />
)}
</>
)
},
handlePixels: 6,
size: 0.25,
},
]
return (
<div className="time-machine-flux-editor">
<Threesizer orientation={HANDLE_VERTICAL} divisions={divisions} />
<div className="flux-editor">
<div className="flux-editor--left-panel">
<FluxEditor
script={activeQueryText}
onChangeScript={onSetActiveQueryText}
onSubmitScript={onSubmitQueries}
setEditorInstance={setEditorInstance}
/>
</div>
<div className="flux-editor--right-panel">
<FluxToolbar
activeQueryBuilderTab={activeTab}
onInsertFluxFunction={handleInsertFluxFunction}
onInsertVariable={handleInsertVariable}
/>
</div>
</div>
)
}

View File

@ -1,29 +0,0 @@
.toolbar-tab {
background: rgba($g3-castle, 0.5);
margin-right: $ix-border;
border-radius: $ix-radius $ix-radius 0 0;
display: flex;
align-items: center;
color: $g10-wolf;
font-weight: 700;
padding: 0 ($ix-marg-c - $ix-marg-a);
font-size: $ix-text-tiny;
user-select: none;
white-space: nowrap;
transition: color 0.25s ease, background-color 0.25s ease;
&:last-child {
margin-right: 0;
}
&.active {
flex: 0 0 auto;
background: $g3-castle;
color: $g16-pearl;
}
&:hover {
color: $g16-pearl;
cursor: pointer;
}
}

View File

@ -1,29 +1,40 @@
// Libraries
import React, {PureComponent} from 'react'
import React, {FC} from 'react'
import classnames from 'classnames'
interface Props {
onSetActive: () => void
onClick: (id: string) => void
id: string
name: string
active: boolean
testID: string
testID?: string
}
export default class ToolbarTab extends PureComponent<Props> {
public static defaultProps = {
testID: 'toolbar-tab',
const ToolbarTab: FC<Props> = ({
onClick,
name,
active,
testID = 'toolbar-tab',
id,
}) => {
const toolbarTabClass = classnames('flux-toolbar--tab', {
'flux-toolbar--tab__active': active,
})
const handleClick = (): void => {
onClick(id)
}
public render() {
const {active, onSetActive, name, testID} = this.props
return (
<div
className={`toolbar-tab ${active ? 'active' : ''}`}
onClick={onSetActive}
title={name}
data-testid={testID}
>
{name}
</div>
)
}
return (
<div
className={toolbarTabClass}
onClick={handleClick}
title={name}
data-testid={testID}
>
{name}
</div>
)
}
export default ToolbarTab

View File

@ -1,86 +1,44 @@
@import "src/style/modules";
.flux-functions-toolbar {
height: 100%;
display: flex;
flex-direction: column;
background-color: $g3-castle;
font-size: 13px;
.flux-function-docs {
width: 420px;
height: 330px;
}
.flux-functions-toolbar--list {
padding-bottom: $ix-marg-a;
}
.flux-function-docs--heading {
font-weight: $cf-font-weight--bold;
margin-top: $cf-marg-b;
margin-bottom: $cf-marg-b;
display: inline-block;
width: 100%;
.flux-functions-toolbar--category {
dt, dd {
height: 30px;
display: flex;
align-items: center;
padding-left: 10px;
}
dt {
background-color: $g6-smoke;
font-weight: 600;
color: $g18-cloud;
}
dd {
font-family: "RobotoMono", monospace;
cursor: pointer;
}
dd:hover, dd:active {
background-color: $g4-onyx;
color: $c-laser;
article:first-child & {
margin-top: 0;
}
}
.flux-functions-toolbar--helper {
color: $g10-wolf;
padding: 15px;
visibility: hidden;
.flux-function-docs--snippet {
background-color: $g1-raven;
border-radius: $cf-radius;
margin: $cf-marg-a 0;
padding: $cf-marg-b;
font-family: $cf-code-font;
}
.flux-functions-toolbar--function {
position: relative;
&:hover {
.flux-functions-toolbar--helper {
visibility: visible;
}
}
}
.flux-functions-toolbar--heading {
font-weight: 600;
margin-bottom: $ix-marg-a;
}
.flux-functions-toolbar--snippet {
background-color: $g3-castle;
border-radius: $radius;
margin: $ix-marg-a 0;
padding: $ix-marg-b;
font-family: "RobotoMono", monospace;
}
.flux-functions-toolbar--arguments {
.flux-function-docs--arguments {
span:first-child {
font-weight: 600;
font-weight: $cf-font-weight--bold;
color: $c-pool;
margin-right: $ix-marg-a;
margin-right: $cf-marg-a;
}
span:nth-child(2) {
color: $c-rainforest;
font-style: italic;
margin-right: 2px;
margin-right: $cf-border;
}
div {
margin: $ix-marg-a 0 $ix-marg-c 0;
margin: $cf-marg-a 0 $cf-marg-c 0;
}
}

View File

@ -5,8 +5,8 @@ import {connect} from 'react-redux'
// Components
import TransformToolbarFunctions from 'src/timeMachine/components/fluxFunctionsToolbar/TransformToolbarFunctions'
import FunctionCategory from 'src/timeMachine/components/fluxFunctionsToolbar/FunctionCategory'
import SearchBar from 'src/timeMachine/components/SearchBar'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import FluxToolbarSearch from 'src/timeMachine/components/FluxToolbarSearch'
import {DapperScrollbars} from '@influxdata/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
@ -46,10 +46,13 @@ class FluxFunctionsToolbar extends PureComponent<Props, State> {
const {searchTerm} = this.state
return (
<div className="flux-functions-toolbar">
<SearchBar onSearch={this.handleSearch} resourceName="Functions" />
<FancyScrollbar>
<div className="flux-functions-toolbar--list">
<>
<FluxToolbarSearch
onSearch={this.handleSearch}
resourceName="Functions"
/>
<DapperScrollbars className="flux-toolbar--scroll-area">
<div className="flux-toolbar--list">
<TransformToolbarFunctions
funcs={FLUX_FUNCTIONS}
searchTerm={searchTerm}
@ -66,8 +69,8 @@ class FluxFunctionsToolbar extends PureComponent<Props, State> {
}
</TransformToolbarFunctions>
</div>
</FancyScrollbar>
</div>
</DapperScrollbars>
</>
)
}

View File

@ -17,14 +17,14 @@ const FunctionCategory: SFC<Props> = props => {
const {category, funcs, onClickFunction} = props
return (
<dl className="flux-functions-toolbar--category">
<dt>{category}</dt>
<dl className="flux-toolbar--category">
<dt className="flux-toolbar--heading">{category}</dt>
{funcs.map(func => (
<ToolbarFunction
onClickFunction={onClickFunction}
key={`${func.name}_${func.desc}`}
func={func}
testID="toolbar-function"
testID={func.name}
/>
))}
</dl>

View File

@ -2,7 +2,7 @@
import React, {FunctionComponent} from 'react'
// Components
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import {DapperScrollbars} from '@influxdata/clockface'
import TooltipDescription from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipDescription'
import TooltipArguments from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipArguments'
import TooltipExample from 'src/timeMachine/components/fluxFunctionsToolbar/TooltipExample'
@ -11,22 +11,24 @@ import TooltipLink from 'src/timeMachine/components/fluxFunctionsToolbar/Tooltip
// Types
import {FluxToolbarFunction} from 'src/types/shared'
const MAX_HEIGHT = 400
interface Props {
func: FluxToolbarFunction
}
const FunctionTooltipContents: FunctionComponent<Props> = ({
func: {desc, args, example, link},
func: {desc, args, example, link, name},
}) => {
return (
<FancyScrollbar autoHeight={true} maxHeight={MAX_HEIGHT} autoHide={false}>
<TooltipDescription description={desc} />
<TooltipArguments argsList={args} />
<TooltipExample example={example} />
<TooltipLink link={link} />
</FancyScrollbar>
<div className="flux-function-docs" data-testid={`flux-docs--${name}`}>
<DapperScrollbars autoHide={false}>
<div className="flux-toolbar--popover">
<TooltipDescription description={desc} />
<TooltipArguments argsList={args} />
<TooltipExample example={example} />
<TooltipLink link={link} />
</div>
</DapperScrollbars>
</div>
)
}

View File

@ -8,6 +8,9 @@ import {
PopoverPosition,
PopoverInteraction,
Appearance,
Button,
ComponentSize,
ComponentColor,
} from '@influxdata/clockface'
// Types
@ -20,22 +23,19 @@ interface Props {
}
const defaultProps = {
testID: 'toolbar-function',
testID: 'flux-function',
}
const ToolbarFunction: FC<Props> = ({func, onClickFunction, testID}) => {
const functionRef = createRef<HTMLDivElement>()
const functionRef = createRef<HTMLDListElement>()
const handleClickFunction = () => {
onClickFunction(func)
}
return (
<div
className="flux-functions-toolbar--function"
ref={functionRef}
data-testid={testID}
>
<>
<Popover
appearance={Appearance.Outline}
enableDefaultStyles={false}
position={PopoverPosition.ToTheLeft}
triggerRef={functionRef}
showEvent={PopoverInteraction.Hover}
@ -45,14 +45,21 @@ const ToolbarFunction: FC<Props> = ({func, onClickFunction, testID}) => {
contents={() => <FunctionTooltipContents func={func} />}
/>
<dd
onClick={handleClickFunction}
data-testid={`flux-function ${func.name}`}
ref={functionRef}
data-testid={`flux--${testID}`}
className="flux-toolbar--list-item flux-toolbar--function"
>
{func.name}
&nbsp;
<span className="flux-functions-toolbar--helper">Click to Add</span>
<code>{func.name}</code>
<Button
testID={`flux--${testID}--inject`}
text="Inject"
onClick={handleClickFunction}
size={ComponentSize.ExtraSmall}
className="flux-toolbar--injector"
color={ComponentColor.Primary}
/>
</dd>
</div>
</>
)
}

View File

@ -14,8 +14,8 @@ class TooltipArguments extends PureComponent<Props> {
public render() {
return (
<article>
<div className="flux-functions-toolbar--heading">Arguments</div>
<div className="flux-functions-toolbar--snippet">{this.arguments}</div>
<div className="flux-function-docs--heading">Arguments</div>
<div className="flux-function-docs--snippet">{this.arguments}</div>
</article>
)
}
@ -26,7 +26,7 @@ class TooltipArguments extends PureComponent<Props> {
if (argsList.length > 0) {
return argsList.map(a => {
return (
<div className="flux-functions-toolbar--arguments" key={a.name}>
<div className="flux-function-docs--arguments" key={a.name}>
<span>{a.name}:</span>
<span>{a.type}</span>
<div>{a.desc}</div>
@ -35,7 +35,7 @@ class TooltipArguments extends PureComponent<Props> {
})
}
return <div className="flux-functions-toolbar--arguments">None</div>
return <div className="flux-function-docs--arguments">None</div>
}
}

View File

@ -6,7 +6,7 @@ interface Props {
const TooltipDescription: SFC<Props> = ({description}) => (
<article className="flux-functions-toolbar--description">
<div className="flux-functions-toolbar--heading">Description</div>
<div className="flux-function-docs--heading">Description</div>
<span>{description}</span>
</article>
)

View File

@ -6,8 +6,8 @@ interface Props {
const TooltipExample: SFC<Props> = ({example}) => (
<article>
<div className="flux-functions-toolbar--heading">Example</div>
<div className="flux-functions-toolbar--snippet">{example}</div>
<div className="flux-function-docs--heading">Example</div>
<div className="flux-function-docs--snippet">{example}</div>
</article>
)

View File

@ -1,7 +1,10 @@
// Libraries
import {SFC, ReactElement} from 'react'
import React, {SFC, ReactElement} from 'react'
import {groupBy} from 'lodash'
// Components
import {EmptyState, ComponentSize} from '@influxdata/clockface'
// Types
import {FluxToolbarFunction} from 'src/types/shared'
@ -22,6 +25,14 @@ const TransformToolbarFunctions: SFC<Props> = props => {
const groupedFunctions = groupBy(filteredFunctions, 'category')
if (filteredFunctions.length === 0) {
return (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text>No functions match your search</EmptyState.Text>
</EmptyState>
)
}
return children(groupedFunctions) as ReactElement<any>
}

View File

@ -1,22 +1,26 @@
// Libraries
import React, {FC, useRef} from 'react'
import {get} from 'lodash'
// Components
import VariableTooltipContents from 'src/timeMachine/components/variableToolbar/VariableTooltipContents'
import {
Button,
Popover,
PopoverPosition,
PopoverInteraction,
Appearance,
ComponentColor,
ComponentSize,
} from '@influxdata/clockface'
// Types
import {Variable} from 'src/types'
import VariableLabel from 'src/timeMachine/components/variableToolbar/VariableLabel'
interface Props {
variable: Variable
onClickVariable: (variableName: string) => void
testID?: string
}
function shouldShowTooltip(variable: Variable): boolean {
@ -46,31 +50,67 @@ function shouldShowTooltip(variable: Variable): boolean {
return true
}
const VariableItem: FC<Props> = ({variable, onClickVariable}) => {
const VariableItem: FC<Props> = ({
variable,
onClickVariable,
testID = 'variable',
}) => {
const trigger = useRef<HTMLDivElement>(null)
const handleClick = (): void => {
const variableName = get(variable, 'name', 'variableName')
onClickVariable(variableName)
}
if (!shouldShowTooltip(variable)) {
return (
<div className="variables-toolbar--item" ref={trigger}>
<VariableLabel name={variable.name} onClickVariable={onClickVariable} />
<div
className="flux-toolbar--list-item flux-toolbar--variable"
data-testid={`variable--${testID}`}
>
<code data-testid={`variable-name--${testID}`}>{variable.name}</code>
<Button
testID={`variable--${testID}--inject`}
text="Inject"
onClick={handleClick}
size={ComponentSize.ExtraSmall}
className="flux-toolbar--injector"
color={ComponentColor.Success}
/>
</div>
)
}
return (
<div className="variables-toolbar--item" ref={trigger}>
<VariableLabel name={variable.name} onClickVariable={onClickVariable} />
<>
<div
className="flux-toolbar--list-item flux-toolbar--variable"
ref={trigger}
data-testid={`variable--${testID}`}
>
<code data-testid={`variable-name--${testID}`}>{variable.name}</code>
<Button
testID={`variable--${testID}--inject`}
text="Inject"
onClick={handleClick}
size={ComponentSize.ExtraSmall}
className="flux-toolbar--injector"
color={ComponentColor.Success}
/>
</div>
<Popover
appearance={Appearance.Outline}
position={PopoverPosition.ToTheLeft}
triggerRef={trigger}
showEvent={PopoverInteraction.Hover}
hideEvent={PopoverInteraction.Hover}
color={ComponentColor.Success}
distanceFromTrigger={8}
testID="toolbar-popover"
enableDefaultStyles={false}
contents={() => <VariableTooltipContents variableID={variable.id} />}
/>
</div>
</>
)
}

View File

@ -1,44 +0,0 @@
.variable-toolbar {
height: 100%;
display: flex;
flex-direction: column;
background-color: $g3-castle;
font-size: 13px;
}
.variables-toolbar--list {
padding-bottom: $ix-marg-a;
}
.variables-toolbar--item {
position: relative;
}
.variables-toolbar--label,
.variables-toolbar--separator {
height: 30px;
display: flex;
align-items: center;
padding-left: $ix-marg-b;
}
.variables-toolbar--label {
font-family: 'RobotoMono', monospace;
cursor: pointer;
&:hover,
&:active {
background-color: $g4-onyx;
color: $c-laser;
}
}
.variables-toolbar--separator {
background-color: $g6-smoke;
font-weight: 600;
color: $g18-cloud;
}
.variable-tooltip--contents .form--element {
margin-bottom: 0;
}

View File

@ -3,8 +3,12 @@ import React, {useState, FunctionComponent} from 'react'
import {connect} from 'react-redux'
// Components
import SearchBar from 'src/timeMachine/components/SearchBar'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import FluxToolbarSearch from 'src/timeMachine/components/FluxToolbarSearch'
import {
DapperScrollbars,
EmptyState,
ComponentSize,
} from '@influxdata/clockface'
import VariableItem from 'src/timeMachine/components/variableToolbar/VariableItem'
// Utils
@ -26,24 +30,32 @@ const VariableToolbar: FunctionComponent<OwnProps & StateProps> = ({
onClickVariable,
}) => {
const [searchTerm, setSearchTerm] = useState('')
const filteredVariables = variables.filter(v => v.name.includes(searchTerm))
let content: JSX.Element | JSX.Element[] = (
<EmptyState size={ComponentSize.ExtraSmall}>
<EmptyState.Text>No variables match your search</EmptyState.Text>
</EmptyState>
)
if (Boolean(filteredVariables.length)) {
content = filteredVariables.map(v => (
<VariableItem
variable={v}
key={v.id}
onClickVariable={onClickVariable}
testID={v.name}
/>
))
}
return (
<div className="variable-toolbar">
<SearchBar onSearch={setSearchTerm} resourceName="Variables" />
<FancyScrollbar style={{marginBottom: '40px'}}>
<div className="variables-toolbar--list">
{variables
.filter(v => v.name.includes(searchTerm))
.map(v => (
<VariableItem
variable={v}
key={v.id}
onClickVariable={onClickVariable}
/>
))}
</div>
</FancyScrollbar>
</div>
<>
<FluxToolbarSearch onSearch={setSearchTerm} resourceName="Variables" />
<DapperScrollbars className="flux-toolbar--scroll-area">
<div className="flux-toolbar--list">{content}</div>
</DapperScrollbars>
</>
)
}

View File

@ -35,7 +35,10 @@ const VariableTooltipContents: FunctionComponent<Props> = ({
execute()
}
return (
<div>
<div
className="flux-toolbar--popover"
data-testid="flux-toolbar--variable-popover"
>
<Form.Element label="Value">
<VariableDropdown
variableID={variableID}