Port FluxFunctionsToolbar

pull/10616/head
Christopher Henn 2018-11-19 11:17:45 -08:00 committed by Chris Henn
parent 43199e4468
commit d2019fedd5
20 changed files with 2135 additions and 25 deletions

View File

@ -40,7 +40,7 @@ const TimeMachine: SFC<StateProps> = props => {
size: 0.33,
},
{
handlePixels: 8,
handlePixels: 12,
render: () => <TimeMachineBottom />,
headerOrientation: HANDLE_HORIZONTAL,
size: 0.67,

View File

@ -1,20 +1,16 @@
@import "src/style/modules";
.time-machine-query-editor {
height: 100%;
width: 100%;
padding: 10px;
display: flex;
flex-direction: column;
> .time-machine-editor {
height: 100%;
border-radius: 4px;
overflow: hidden;
.time-machine-editor {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
}
.time-machine-query-editor--controls {
flex: 0 0 30px;
margin-bottom: 10px;
display: flex;
justify-content: flex-end;
}

View File

@ -4,7 +4,9 @@ import {connect} from 'react-redux'
// Components
import FluxEditor from 'src/shared/components/FluxEditor'
import {Button, ComponentColor} from 'src/clockface'
import {Button, ComponentColor, ComponentSize} from 'src/clockface'
import Threesizer from 'src/shared/components/threesizer/Threesizer'
import FluxFunctionsToolbar from 'src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar'
// Actions
import {setDraftScript, submitScript} from 'src/shared/actions/v2/timeMachines'
@ -12,6 +14,9 @@ import {setDraftScript, submitScript} from 'src/shared/actions/v2/timeMachines'
// Utils
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'
// Constants
import {HANDLE_VERTICAL} from 'src/shared/constants'
// Types
import {AppState} from 'src/types/v2'
@ -34,21 +39,38 @@ class TimeMachineQueryEditor extends PureComponent<Props> {
public render() {
const {draftScript, onSetDraftScript, onSubmitScript} = this.props
return (
<div className="time-machine-query-editor">
<div className="time-machine-query-editor--controls">
const divisions = [
{
name: 'Script',
size: 0.75,
headerButtons: [
<Button
key="foo"
text="Submit"
size={ComponentSize.ExtraSmall}
onClick={onSubmitScript}
color={ComponentColor.Primary}
/>,
],
render: () => (
<FluxEditor
script={draftScript}
status={{type: '', text: ''}}
onChangeScript={onSetDraftScript}
suggestions={[]}
/>
</div>
<FluxEditor
script={draftScript}
status={{type: '', text: ''}}
onChangeScript={onSetDraftScript}
suggestions={[]}
/>
),
},
{
name: 'Flux Functions',
render: () => <FluxFunctionsToolbar />,
size: 0.25,
},
]
return (
<div className="time-machine-query-editor">
<Threesizer orientation={HANDLE_VERTICAL} divisions={divisions} />
</div>
)
}

View File

@ -0,0 +1,200 @@
.flux-functions-toolbar {
height: 100%;
display: flex;
flex-direction: column;
background-color: $g3-castle;
font-size: 13px;
}
.flux-functions-toolbar--search {
padding: $ix-marg-a;
flex-shrink: 0;
padding-bottom: $ix-marg-a + 1px;
border-bottom: $ix-border solid $g4-onyx;
background-color: $g3-castle;
}
.flux-functions-toolbar--list {
padding-bottom: $ix-marg-a;
}
.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;
}
}
.flux-functions-toolbar--function {
position: relative;
}
.flux-functions-toolbar--helper {
color: $g10-wolf;
padding: 15px;
}
.flux-functions-toolbar--tooltip {
padding-right: 8px;
max-width: 600px;
position: fixed;
transform: translateY(50%);
z-index: 10;
}
.flux-functions-toolbar--tooltip-caret {
content: "";
width: 0;
height: 0;
position: fixed;
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
border-left: 6px solid $c-pool;
margin-top: 9px;
margin-left: -8px;
z-index: 9999;
}
.flux-functions-toolbar--tooltip-contents {
border: $ix-border solid $c-pool;
border-radius: $radius;
width: 100%;
max-width: 600px;
display: inline-flex;
align-items: center;
background-color: $g1-raven;
padding: 10px;
article {
margin-bottom: $ix-marg-c;
}
}
.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 {
span:first-child {
font-weight: 600;
color: $c-pool;
margin-right: $ix-marg-a;
}
span:nth-child(2) {
color: $c-rainforest;
font-style: italic;
margin-right: 2px;
}
div {
margin: $ix-marg-a 0 $ix-marg-c 0;
}
}
.flux-functions-toolbar--tooltip-dismiss {
position: absolute;
z-index: 5000;
top: 0;
right: 0;
transform: translate(0,-50%);
width: 24px;
height: 24px;
outline: none;
border-radius: 50%;
background-color: $c-pool;
transition: background-color 0.25s ease;
border: 0;
&:before,
&:after {
content: '';
position: absolute;
width: 13px;
height: 3px;
top: 50%;
left: 50%;
border-radius: 1px;
background-color: $g20-white;
}
&:before {
transform: translate(-50%, -50%) rotate(45deg);
}
&:after {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:hover {
background-color: $c-laser;
cursor: pointer;
}
}
.flux-functions-toolbar--tooltip-dismiss {
position: absolute;
z-index: 5000;
top: 0;
right: 0;
transform: translate(0,-50%);
width: 24px;
height: 24px;
outline: none;
border-radius: 50%;
background-color: $c-pool;
transition: background-color 0.25s ease;
border: 0;
&:before,
&:after {
content: '';
position: absolute;
width: 13px;
height: 3px;
top: 50%;
left: 50%;
border-radius: 1px;
background-color: $g20-white;
}
&:before {
transform: translate(-50%, -50%) rotate(45deg);
}
&:after {
transform: translate(-50%, -50%) rotate(-45deg);
}
&:hover {
background-color: $c-laser;
cursor: pointer;
}
}

View File

@ -0,0 +1,92 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
// Components
import TransformToolbarFunctions from 'src/shared/components/flux_functions_toolbar/TransformToolbarFunctions'
import FunctionCategory from 'src/shared/components/flux_functions_toolbar/FunctionCategory'
import SearchBar from 'src/shared/components/flux_functions_toolbar/SearchBar'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
import {setDraftScript} from 'src/shared/actions/v2/timeMachines'
// Utils
import {getActiveTimeMachine} from 'src/shared/selectors/timeMachines'
// Constants
import {FLUX_FUNCTIONS} from 'src/shared/constants/fluxFunctions'
// Types
import {AppState} from 'src/types/v2'
interface StateProps {
draftScript: string
}
interface DispatchProps {
onSetDraftScript: (script: string) => void
}
type Props = StateProps & DispatchProps
interface State {
searchTerm: string
}
class FluxFunctionsToolbar extends PureComponent<Props, State> {
public state: State = {searchTerm: ''}
public render() {
const {searchTerm} = this.state
return (
<div className="flux-functions-toolbar">
<SearchBar onSearch={this.handleSearch} />
<FancyScrollbar>
<div className="flux-functions-toolbar--list">
<TransformToolbarFunctions
funcs={FLUX_FUNCTIONS}
searchTerm={searchTerm}
>
{sortedFunctions =>
Object.entries(sortedFunctions).map(([category, funcs]) => (
<FunctionCategory
key={category}
category={category}
funcs={funcs}
onClickFunction={this.handleUpdateScript}
/>
))
}
</TransformToolbarFunctions>
</div>
</FancyScrollbar>
</div>
)
}
private handleSearch = (searchTerm: string): void => {
this.setState({searchTerm})
}
private handleUpdateScript = (funcExample: string) => {
const {draftScript, onSetDraftScript} = this.props
onSetDraftScript(`${draftScript}\n |> ${funcExample}`)
}
}
const mstp = (state: AppState) => ({
draftScript: getActiveTimeMachine(state).draftScript,
})
const mdtp = {
onSetDraftScript: setDraftScript,
}
export default connect<StateProps, DispatchProps, {}>(
mstp,
mdtp
)(ErrorHandling(FluxFunctionsToolbar))

View File

@ -0,0 +1,33 @@
// Libraries
import React, {SFC} from 'react'
// Components
import ToolbarFunction from 'src/shared/components/flux_functions_toolbar/ToolbarFunction'
// Types
import {FluxToolbarFunction} from 'src/types/shared'
interface Props {
category: string
funcs: FluxToolbarFunction[]
onClickFunction: (s: string) => void
}
const FunctionCategory: SFC<Props> = props => {
const {category, funcs, onClickFunction} = props
return (
<dl className="flux-functions-toolbar--category">
<dt>{category}</dt>
{funcs.map(func => (
<ToolbarFunction
onClickFunction={onClickFunction}
key={func.name}
func={func}
/>
))}
</dl>
)
}
export default FunctionCategory

View File

@ -0,0 +1,103 @@
// Libraries
import React, {PureComponent, MouseEvent, CSSProperties, createRef} from 'react'
// Components
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
import TooltipDescription from 'src/shared/components/flux_functions_toolbar/TooltipDescription'
import TooltipArguments from 'src/shared/components/flux_functions_toolbar/TooltipArguments'
import TooltipExample from 'src/shared/components/flux_functions_toolbar/TooltipExample'
import TooltipLink from 'src/shared/components/flux_functions_toolbar/TooltipLink'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Types
import {FluxToolbarFunction} from 'src/types/shared'
interface Props {
func: FluxToolbarFunction
onDismiss: () => void
tipPosition?: {top: number; right: number}
}
interface State {
bottomPosition: number | null
}
const MAX_HEIGHT = 400
class FunctionTooltip extends PureComponent<Props, State> {
public state: State = {bottomPosition: null}
private tooltipRef = createRef<HTMLDivElement>()
public componentDidMount() {
const {bottom, height} = this.tooltipRef.current.getBoundingClientRect()
if (bottom > window.innerHeight) {
this.setState({bottomPosition: height / 2})
}
}
public render() {
const {desc, args, example, link} = this.props.func
return (
<>
<div
style={this.stylePosition}
className="flux-functions-toolbar--tooltip"
ref={this.tooltipRef}
>
<button
className="flux-functions-toolbar--tooltip-dismiss"
onClick={this.handleDismiss}
/>
<div className="flux-functions-toolbar--tooltip-contents">
<FancyScrollbar
autoHeight={true}
maxHeight={MAX_HEIGHT}
autoHide={false}
>
<TooltipDescription description={desc} />
<TooltipArguments argsList={args} />
<TooltipExample example={example} />
<TooltipLink link={link} />
</FancyScrollbar>
</div>
</div>
<span
className="flux-functions-toolbar--tooltip-caret"
style={this.styleCaretPosition}
/>
</>
)
}
private get styleCaretPosition(): CSSProperties {
const {top, right} = this.props.tipPosition
return {
top: `${Math.min(top, window.innerHeight)}px`,
right: `${right + 4}px`,
}
}
private get stylePosition(): CSSProperties {
const {top, right} = this.props.tipPosition
const {bottomPosition} = this.state
return {
bottom: `${bottomPosition || window.innerHeight - top - 15}px`,
right: `${right + 2}px`,
}
}
private handleDismiss = (e: MouseEvent<HTMLElement>) => {
const {onDismiss} = this.props
e.preventDefault()
e.stopPropagation()
onDismiss()
}
}
export default ErrorHandling(FunctionTooltip)

View File

@ -0,0 +1,55 @@
// Libraries
import React, {PureComponent, ChangeEvent} from 'react'
import {debounce} from 'lodash'
// Components
import {Input, IconFont} from 'src/clockface'
// Types
import {InputType} from 'src/clockface/components/inputs/Input'
interface Props {
onSearch: (s: string) => void
}
interface State {
searchTerm: string
}
const DEBOUNCE_MS = 100
class SearchBar extends PureComponent<Props, State> {
constructor(props: Props) {
super(props)
this.state = {
searchTerm: '',
}
this.handleSearch = debounce(this.handleSearch, DEBOUNCE_MS)
}
public render() {
return (
<div className="flux-functions-toolbar--search">
<Input
type={InputType.Text}
icon={IconFont.Search}
placeholder="Filter Functions..."
onChange={this.handleChange}
value={this.state.searchTerm}
/>
</div>
)
}
private handleSearch = (): void => {
this.props.onSearch(this.state.searchTerm)
}
private handleChange = (e: ChangeEvent<HTMLInputElement>): void => {
this.setState({searchTerm: e.target.value}, this.handleSearch)
}
}
export default SearchBar

View File

@ -0,0 +1,84 @@
// Libraries
import React, {PureComponent, createRef} from 'react'
// Component
import FunctionTooltip from 'src/shared/components/flux_functions_toolbar/FunctionTooltip'
// Types
import {FluxToolbarFunction} from 'src/types/shared'
interface Props {
func: FluxToolbarFunction
onClickFunction: (s: string) => void
}
interface State {
isActive: boolean
hoverPosition: {top: number; right: number}
}
class ToolbarFunction extends PureComponent<Props, State> {
public state: State = {isActive: false, hoverPosition: undefined}
private functionRef = createRef<HTMLDivElement>()
public render() {
const {func} = this.props
return (
<div
className="flux-functions-toolbar--function"
ref={this.functionRef}
onMouseEnter={this.handleHover}
onMouseLeave={this.handleStopHover}
>
{this.tooltip}
<dd onClick={this.handleClickFunction}>
{func.name} {this.helperText}
</dd>
</div>
)
}
private get tooltip(): JSX.Element | null {
if (this.state.isActive) {
return (
<FunctionTooltip
func={this.props.func}
onDismiss={this.handleStopHover}
tipPosition={this.state.hoverPosition}
/>
)
}
return null
}
private get helperText(): JSX.Element | null {
if (this.state.isActive) {
return (
<span className="flux-functions-toolbar--helper">Click to Add</span>
)
}
return null
}
private handleHover = () => {
const {top, left} = this.functionRef.current.getBoundingClientRect()
const right = window.innerWidth - left
this.setState({isActive: true, hoverPosition: {top, right}})
}
private handleStopHover = () => {
this.setState({isActive: false})
}
private handleClickFunction = () => {
const {func, onClickFunction} = this.props
onClickFunction(func.example)
}
}
export default ToolbarFunction

View File

@ -0,0 +1,42 @@
import React, {PureComponent} from 'react'
interface Args {
name: string
type: string
desc: string
}
interface Props {
argsList?: Args[]
}
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>
</article>
)
}
private get arguments(): JSX.Element | JSX.Element[] {
const {argsList} = this.props
if (argsList.length > 0) {
return argsList.map(a => {
return (
<div className="flux-functions-toolbar--arguments" key={a.name}>
<span>{a.name}:</span>
<span>{a.type}</span>
<div>{a.desc}</div>
</div>
)
})
}
return <div className="flux-functions-toolbar--arguments">None</div>
}
}
export default TooltipArguments

View File

@ -0,0 +1,14 @@
import React, {SFC} from 'react'
interface Props {
description: string
}
const TooltipDescription: SFC<Props> = ({description}) => (
<article className="flux-functions-toolbar--description">
<div className="flux-functions-toolbar--heading">Description</div>
<span>{description}</span>
</article>
)
export default TooltipDescription

View File

@ -0,0 +1,14 @@
import React, {SFC} from 'react'
interface Props {
example: string
}
const TooltipExample: SFC<Props> = ({example}) => (
<article>
<div className="flux-functions-toolbar--heading">Example</div>
<div className="flux-functions-toolbar--snippet">{example}</div>
</article>
)
export default TooltipExample

View File

@ -0,0 +1,23 @@
import React, {PureComponent} from 'react'
interface Props {
link: string
}
class TooltipLink extends PureComponent<Props> {
public render() {
const {link} = this.props
return (
<p>
Still have questions? Check out the{' '}
<a target="_blank" href={link}>
Flux Docs
</a>
.
</p>
)
}
}
export default TooltipLink

View File

@ -0,0 +1,28 @@
// Libraries
import {SFC, ReactElement} from 'react'
import {groupBy} from 'lodash'
// Types
import {FluxToolbarFunction} from 'src/types/shared'
interface Props {
funcs: FluxToolbarFunction[]
searchTerm?: string
children: (
funcs: {[category: string]: FluxToolbarFunction[]}
) => JSX.Element | JSX.Element[]
}
const TransformToolbarFunctions: SFC<Props> = props => {
const {searchTerm, funcs, children} = props
const filteredFunctions = funcs.filter(func =>
func.name.toLowerCase().includes(searchTerm.toLowerCase())
)
const groupedFunctions = groupBy(filteredFunctions, 'category')
return children(groupedFunctions) as ReactElement<any>
}
export default TransformToolbarFunctions

View File

@ -43,6 +43,8 @@ class Division extends PureComponent<Props> {
name: '',
handleDisplay: 'visible',
style: {},
headerButtons: [],
menuOptions: [],
}
private collapseThreshold: number = 0
@ -282,7 +284,7 @@ class Division extends PureComponent<Props> {
return true
}
if (!this.divisionRef || this.props.size >= 0.33) {
if (!this.divisionRef.current || this.props.size >= 0.33) {
return false
}

View File

@ -171,6 +171,8 @@ $threesizer-shadow-stop: fade-out($g0-obsidian, 1);
}
.threesizer--body {
position: relative;
.horizontal>&:only-child {
width: 100%;
}

View File

@ -136,6 +136,7 @@ class Threesizer extends Component<Props, State> {
>
{divisions.map((d, i) => {
const headerOrientation = _.get(d, 'headerOrientation', orientation)
return (
<Division
key={d.id}

File diff suppressed because it is too large Load Diff

View File

@ -44,6 +44,7 @@
@import "src/shared/components/custom_singular_time/CustomSingularTime";
@import "src/onboarding/OnboardingWizard.scss";
@import "src/shared/components/RawFluxDataTable.scss";
@import "src/shared/components/flux_functions_toolbar/FluxFunctionsToolbar.scss";
@import "src/logs/containers/logs_page/LogsPage";
@import "src/logs/components/loading_status/LoadingStatus";

View File

@ -16,3 +16,18 @@ export interface PageSection {
component: ReactNode
enabled: boolean
}
export interface FluxToolbarArg {
name: string
desc: string
type: string
}
export interface FluxToolbarFunction {
name: string
args: FluxToolbarArg[]
desc: string
example: string
category: string
link: string
}