Introduce FluxScriptWizard
parent
1775261280
commit
093a5af033
|
@ -71,7 +71,7 @@ class TimeMachineEditor extends PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {script} = this.props
|
const {script, children} = this.props
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
tabIndex: 1,
|
tabIndex: 1,
|
||||||
|
@ -96,6 +96,7 @@ class TimeMachineEditor extends PureComponent<Props, State> {
|
||||||
editorDidMount={this.handleMount}
|
editorDidMount={this.handleMount}
|
||||||
onKeyUp={this.handleKeyUp}
|
onKeyUp={this.handleKeyUp}
|
||||||
/>
|
/>
|
||||||
|
{children}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,19 +1,3 @@
|
||||||
export const emptyAST = {
|
|
||||||
type: 'Program',
|
|
||||||
location: {
|
|
||||||
start: {
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
end: {
|
|
||||||
line: 1,
|
|
||||||
column: 1,
|
|
||||||
},
|
|
||||||
source: '',
|
|
||||||
},
|
|
||||||
body: [],
|
|
||||||
}
|
|
||||||
|
|
||||||
export const ast = {
|
export const ast = {
|
||||||
type: 'File',
|
type: 'File',
|
||||||
start: 0,
|
start: 0,
|
||||||
|
|
|
@ -171,3 +171,30 @@
|
||||||
.dropdown--menu-container .fancy-scroll--track-h {
|
.dropdown--menu-container .fancy-scroll--track-h {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@keyframes loading-dots {
|
||||||
|
0% {
|
||||||
|
content: "Loading "
|
||||||
|
}
|
||||||
|
|
||||||
|
25% {
|
||||||
|
content: "Loading."
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
content: "Loading.."
|
||||||
|
}
|
||||||
|
|
||||||
|
75% {
|
||||||
|
content: "Loading..."
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
content: "Loading "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown--loading::after {
|
||||||
|
animation: 1.7s linear loading-dots infinite;
|
||||||
|
content: "Loading..."
|
||||||
|
}
|
||||||
|
|
|
@ -123,8 +123,17 @@ class Dropdown extends Component<Props, State> {
|
||||||
const {expanded} = this.state
|
const {expanded} = this.state
|
||||||
|
|
||||||
const selectedChild = children.find(child => child.props.id === selectedID)
|
const selectedChild = children.find(child => child.props.id === selectedID)
|
||||||
const dropdownLabel =
|
const isLoading = status === ComponentStatus.Loading
|
||||||
(selectedChild && selectedChild.props.children) || titleText
|
|
||||||
|
let dropdownLabel
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
dropdownLabel = <div className="dropdown--loading" />
|
||||||
|
} else if (selectedChild) {
|
||||||
|
dropdownLabel = selectedChild.props.children
|
||||||
|
} else {
|
||||||
|
dropdownLabel = titleText
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownButton
|
<DropdownButton
|
||||||
|
@ -210,6 +219,14 @@ class Dropdown extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get shouldHaveChildren(): boolean {
|
||||||
|
const {status} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
status === ComponentStatus.Default || status === ComponentStatus.Valid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private handleItemClick = (value: any): void => {
|
private handleItemClick = (value: any): void => {
|
||||||
const {onChange} = this.props
|
const {onChange} = this.props
|
||||||
onChange(value)
|
onChange(value)
|
||||||
|
@ -219,7 +236,7 @@ class Dropdown extends Component<Props, State> {
|
||||||
private validateChildCount = (): void => {
|
private validateChildCount = (): void => {
|
||||||
const {children} = this.props
|
const {children} = this.props
|
||||||
|
|
||||||
if (React.Children.count(children) === 0) {
|
if (this.shouldHaveChildren && React.Children.count(children) === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
||||||
)
|
)
|
||||||
|
@ -233,7 +250,11 @@ class Dropdown extends Component<Props, State> {
|
||||||
throw new Error('Dropdowns in ActionList mode require a titleText prop.')
|
throw new Error('Dropdowns in ActionList mode require a titleText prop.')
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === DropdownMode.Radio && selectedID === '') {
|
if (
|
||||||
|
mode === DropdownMode.Radio &&
|
||||||
|
this.shouldHaveChildren &&
|
||||||
|
selectedID === ''
|
||||||
|
) {
|
||||||
throw new Error('Dropdowns in Radio mode require a selectedID prop.')
|
throw new Error('Dropdowns in Radio mode require a selectedID prop.')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,33 +34,55 @@ class DropdownButton extends Component<Props> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
const {onClick, status, children, title} = this.props
|
const {onClick, children, title} = this.props
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={this.classname}
|
className={this.classname}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
disabled={status === ComponentStatus.Disabled}
|
disabled={this.isDisabled}
|
||||||
title={title}
|
title={title}
|
||||||
>
|
>
|
||||||
{this.icon}
|
{this.icon}
|
||||||
<span className="dropdown--selected">{children}</span>
|
<span className="dropdown--selected">{children}</span>
|
||||||
<span className="dropdown--caret icon caret-down" />
|
{this.caret}
|
||||||
</button>
|
</button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private get classname(): string {
|
private get classname(): string {
|
||||||
const {status, active, color, size} = this.props
|
const {active, color, size} = this.props
|
||||||
|
|
||||||
return classnames('dropdown--button button', {
|
return classnames('dropdown--button button', {
|
||||||
'button-stretch': true,
|
'button-stretch': true,
|
||||||
|
'button--disabled': this.isDisabled,
|
||||||
[`button-${color}`]: color,
|
[`button-${color}`]: color,
|
||||||
[`button-${size}`]: size,
|
[`button-${size}`]: size,
|
||||||
disabled: status === ComponentStatus.Disabled,
|
|
||||||
active,
|
active,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get caret(): JSX.Element {
|
||||||
|
const {active} = this.props
|
||||||
|
|
||||||
|
if (active) {
|
||||||
|
return <span className="dropdown--caret icon caret-up" />
|
||||||
|
}
|
||||||
|
|
||||||
|
return <span className="dropdown--caret icon caret-down" />
|
||||||
|
}
|
||||||
|
|
||||||
|
private get isDisabled(): boolean {
|
||||||
|
const {status} = this.props
|
||||||
|
|
||||||
|
const isDisabled = [
|
||||||
|
ComponentStatus.Disabled,
|
||||||
|
ComponentStatus.Loading,
|
||||||
|
ComponentStatus.Error,
|
||||||
|
].includes(status)
|
||||||
|
|
||||||
|
return isDisabled
|
||||||
|
}
|
||||||
|
|
||||||
private get icon(): JSX.Element {
|
private get icon(): JSX.Element {
|
||||||
const {icon} = this.props
|
const {icon} = this.props
|
||||||
|
|
||||||
|
|
|
@ -135,9 +135,11 @@ class MultiSelectDropdown extends Component<Props, State> {
|
||||||
_.includes(selectedIDs, child.props.id)
|
_.includes(selectedIDs, child.props.id)
|
||||||
)
|
)
|
||||||
|
|
||||||
let label: string | Array<string | JSX.Element>
|
let label
|
||||||
|
|
||||||
if (selectedChildren.length) {
|
if (status === ComponentStatus.Loading) {
|
||||||
|
label = <div className="dropdown--loading" />
|
||||||
|
} else if (selectedChildren.length) {
|
||||||
label = selectedChildren.map((sc, i) => {
|
label = selectedChildren.map((sc, i) => {
|
||||||
if (i < selectedChildren.length - 1) {
|
if (i < selectedChildren.length - 1) {
|
||||||
return (
|
return (
|
||||||
|
@ -238,6 +240,14 @@ class MultiSelectDropdown extends Component<Props, State> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private get shouldHaveChildren(): boolean {
|
||||||
|
const {status} = this.props
|
||||||
|
|
||||||
|
return (
|
||||||
|
status === ComponentStatus.Default || status === ComponentStatus.Valid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private handleItemClick = (value: any): void => {
|
private handleItemClick = (value: any): void => {
|
||||||
const {onChange, selectedIDs} = this.props
|
const {onChange, selectedIDs} = this.props
|
||||||
let updatedSelection
|
let updatedSelection
|
||||||
|
@ -254,7 +264,7 @@ class MultiSelectDropdown extends Component<Props, State> {
|
||||||
private validateChildCount = (): void => {
|
private validateChildCount = (): void => {
|
||||||
const {children} = this.props
|
const {children} = this.props
|
||||||
|
|
||||||
if (React.Children.count(children) === 0) {
|
if (this.shouldHaveChildren && React.Children.count(children) === 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
'Dropdowns require at least 1 child element. We recommend using Dropdown.Item and/or Dropdown.Divider.'
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,21 +4,16 @@ import React, {PureComponent} from 'react'
|
||||||
// Components
|
// Components
|
||||||
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
|
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
|
||||||
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
|
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
|
||||||
|
import FluxScriptWizard from 'src/shared/components/TimeMachine/FluxScriptWizard'
|
||||||
import Threesizer from 'src/shared/components/threesizer/Threesizer'
|
import Threesizer from 'src/shared/components/threesizer/Threesizer'
|
||||||
import {
|
import {Button, ComponentSize, ComponentColor} from 'src/reusable_ui'
|
||||||
Button,
|
|
||||||
ComponentSize,
|
|
||||||
ComponentColor,
|
|
||||||
ComponentStatus,
|
|
||||||
} from 'src/reusable_ui'
|
|
||||||
|
|
||||||
// Constants
|
// Constants
|
||||||
import {HANDLE_VERTICAL} from 'src/shared/constants'
|
import {HANDLE_VERTICAL} from 'src/shared/constants'
|
||||||
import {emptyAST} from 'src/flux/constants'
|
|
||||||
|
|
||||||
// Utils
|
// Utils
|
||||||
import {getSuggestions, getAST} from 'src/flux/apis'
|
import {getSuggestions, getAST} from 'src/flux/apis'
|
||||||
import Restarter from 'src/shared/utils/Restarter'
|
import {restartable} from 'src/shared/utils/restartable'
|
||||||
import DefaultDebouncer, {Debouncer} from 'src/shared/utils/debouncer'
|
import DefaultDebouncer, {Debouncer} from 'src/shared/utils/debouncer'
|
||||||
import {parseError} from 'src/flux/helpers/scriptBuilder'
|
import {parseError} from 'src/flux/helpers/scriptBuilder'
|
||||||
|
|
||||||
|
@ -26,7 +21,8 @@ import {parseError} from 'src/flux/helpers/scriptBuilder'
|
||||||
import {NotificationAction, Source} from 'src/types'
|
import {NotificationAction, Source} from 'src/types'
|
||||||
import {Suggestion, Links, ScriptStatus} from 'src/types/flux'
|
import {Suggestion, Links, ScriptStatus} from 'src/types/flux'
|
||||||
|
|
||||||
const AST_DEBOUNCE_DELAY = 600
|
const CHECK_SCRIPT_DELAY = 600
|
||||||
|
const VALID_SCRIPT_STATUS = {type: 'success', text: ''}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
script: string
|
script: string
|
||||||
|
@ -41,29 +37,27 @@ interface State {
|
||||||
suggestions: Suggestion[]
|
suggestions: Suggestion[]
|
||||||
draftScript: string
|
draftScript: string
|
||||||
draftScriptStatus: ScriptStatus
|
draftScriptStatus: ScriptStatus
|
||||||
ast: object
|
isWizardActive: boolean
|
||||||
hasChangedScript: boolean
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class FluxQueryMaker extends PureComponent<Props, State> {
|
class FluxQueryMaker extends PureComponent<Props, State> {
|
||||||
private restarter: Restarter = new Restarter()
|
|
||||||
private debouncer: Debouncer = new DefaultDebouncer()
|
private debouncer: Debouncer = new DefaultDebouncer()
|
||||||
|
private getAST = restartable(getAST)
|
||||||
|
|
||||||
public constructor(props: Props) {
|
public constructor(props: Props) {
|
||||||
super(props)
|
super(props)
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
ast: {},
|
|
||||||
draftScript: props.script,
|
draftScript: props.script,
|
||||||
draftScriptStatus: {type: 'none', text: ''},
|
draftScriptStatus: {type: 'none', text: ''},
|
||||||
hasChangedScript: false,
|
isWizardActive: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public componentDidMount() {
|
public componentDidMount() {
|
||||||
this.fetchSuggestions()
|
this.fetchSuggestions()
|
||||||
this.updateBody()
|
this.checkDraftScript()
|
||||||
}
|
}
|
||||||
|
|
||||||
public render() {
|
public render() {
|
||||||
|
@ -72,26 +66,27 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
||||||
suggestions,
|
suggestions,
|
||||||
draftScript,
|
draftScript,
|
||||||
draftScriptStatus,
|
draftScriptStatus,
|
||||||
hasChangedScript,
|
isWizardActive,
|
||||||
} = this.state
|
} = this.state
|
||||||
|
|
||||||
const submitStatus = hasChangedScript
|
|
||||||
? ComponentStatus.Default
|
|
||||||
: ComponentStatus.Disabled
|
|
||||||
|
|
||||||
const divisions = [
|
const divisions = [
|
||||||
{
|
{
|
||||||
name: 'Script',
|
name: 'Script',
|
||||||
|
size: 0.66,
|
||||||
headerOrientation: HANDLE_VERTICAL,
|
headerOrientation: HANDLE_VERTICAL,
|
||||||
headerButtons: [
|
headerButtons: [
|
||||||
<Button
|
<Button
|
||||||
key={0}
|
key={0}
|
||||||
text={'Submit'}
|
text={'Script Wizard'}
|
||||||
titleText={'Submit Flux Query (Ctrl-Enter)'}
|
onClick={this.handleShowWizard}
|
||||||
|
size={ComponentSize.ExtraSmall}
|
||||||
|
/>,
|
||||||
|
<Button
|
||||||
|
key={1}
|
||||||
|
text={'Run Script'}
|
||||||
onClick={this.handleSubmitScript}
|
onClick={this.handleSubmitScript}
|
||||||
size={ComponentSize.ExtraSmall}
|
size={ComponentSize.ExtraSmall}
|
||||||
color={ComponentColor.Primary}
|
color={ComponentColor.Primary}
|
||||||
status={submitStatus}
|
|
||||||
/>,
|
/>,
|
||||||
],
|
],
|
||||||
menuOptions: [],
|
menuOptions: [],
|
||||||
|
@ -103,11 +98,24 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
||||||
suggestions={suggestions}
|
suggestions={suggestions}
|
||||||
onChangeScript={this.handleChangeDraftScript}
|
onChangeScript={this.handleChangeDraftScript}
|
||||||
onSubmitScript={this.handleSubmitScript}
|
onSubmitScript={this.handleSubmitScript}
|
||||||
/>
|
>
|
||||||
|
{draftScript.trim() === '' && (
|
||||||
|
<div className="flux-script-wizard--bg-hint">
|
||||||
|
<p>
|
||||||
|
New to Flux? Give the{' '}
|
||||||
|
<a title="Open Script Wizard" onClick={this.handleShowWizard}>
|
||||||
|
Script Wizard
|
||||||
|
</a>{' '}
|
||||||
|
a try
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TimeMachineEditor>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Explore',
|
name: 'Explore',
|
||||||
|
size: 0.34,
|
||||||
headerButtons: [],
|
headerButtons: [],
|
||||||
menuOptions: [],
|
menuOptions: [],
|
||||||
render: () => <SchemaExplorer source={source} notify={notify} />,
|
render: () => <SchemaExplorer source={source} notify={notify} />,
|
||||||
|
@ -116,11 +124,18 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
||||||
]
|
]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Threesizer
|
<FluxScriptWizard
|
||||||
orientation={HANDLE_VERTICAL}
|
source={source}
|
||||||
divisions={divisions}
|
isWizardActive={isWizardActive}
|
||||||
containerClass="page-contents"
|
onSetIsWizardActive={this.handleSetIsWizardActive}
|
||||||
/>
|
onAddToScript={this.handleAddToScript}
|
||||||
|
>
|
||||||
|
<Threesizer
|
||||||
|
orientation={HANDLE_VERTICAL}
|
||||||
|
divisions={divisions}
|
||||||
|
containerClass="page-contents"
|
||||||
|
/>
|
||||||
|
</FluxScriptWizard>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -133,43 +148,49 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
||||||
if (onUpdateStatus) {
|
if (onUpdateStatus) {
|
||||||
onUpdateStatus(draftScriptStatus)
|
onUpdateStatus(draftScriptStatus)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.setState({hasChangedScript: false})
|
private handleShowWizard = (): void => {
|
||||||
|
this.setState({isWizardActive: true})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSetIsWizardActive = (isWizardActive: boolean): void => {
|
||||||
|
this.setState({isWizardActive})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAddToScript = (draftScript): void => {
|
||||||
|
this.setState({draftScript}, this.handleSubmitScript)
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleChangeDraftScript = async (
|
private handleChangeDraftScript = async (
|
||||||
draftScript: string
|
draftScript: string
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
this.setState(
|
this.setState({draftScript}, () =>
|
||||||
{
|
this.debouncer.call(this.checkDraftScript, CHECK_SCRIPT_DELAY)
|
||||||
draftScript,
|
|
||||||
hasChangedScript: true,
|
|
||||||
},
|
|
||||||
() => this.debouncer.call(this.updateBody, AST_DEBOUNCE_DELAY)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private updateBody = async () => {
|
private checkDraftScript = async () => {
|
||||||
const {draftScript} = this.state
|
const {draftScript} = this.state
|
||||||
|
|
||||||
let ast: object
|
if (draftScript.trim() === '') {
|
||||||
|
// Don't attempt to validate an empty script
|
||||||
|
this.setState({draftScriptStatus: VALID_SCRIPT_STATUS})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let draftScriptStatus: ScriptStatus
|
let draftScriptStatus: ScriptStatus
|
||||||
|
|
||||||
try {
|
try {
|
||||||
ast = await this.restarter.perform(
|
await this.getAST({url: this.props.links.ast, body: draftScript})
|
||||||
getAST({url: this.props.links.ast, body: draftScript})
|
|
||||||
)
|
|
||||||
|
|
||||||
draftScriptStatus = {type: 'success', text: ''}
|
draftScriptStatus = VALID_SCRIPT_STATUS
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
ast = emptyAST
|
|
||||||
draftScriptStatus = parseError(error)
|
draftScriptStatus = parseError(error)
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setState({
|
this.setState({draftScriptStatus})
|
||||||
ast,
|
|
||||||
draftScriptStatus,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fetchSuggestions = async (): Promise<void> => {
|
private fetchSuggestions = async (): Promise<void> => {
|
||||||
|
|
|
@ -0,0 +1,72 @@
|
||||||
|
.flux-script-wizard {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--children, .flux-script-wizard--backdrop {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--backdrop {
|
||||||
|
background-color: rgba(28, 28, 33, 0.8); // g1-raven at 0.8 opacity
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--wizard {
|
||||||
|
width: 400px;
|
||||||
|
border-radius: 4px;
|
||||||
|
background-color: $g3-castle;
|
||||||
|
padding: 15px 15px 25px 15px;
|
||||||
|
z-index: 200;
|
||||||
|
|
||||||
|
.form--element {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--wizard-header {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
position: relative;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--close {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 34px;
|
||||||
|
font-weight: 100;
|
||||||
|
line-height: 18px;
|
||||||
|
position: absolute;
|
||||||
|
right: 30px;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flux-script-wizard--bg-hint {
|
||||||
|
position: absolute;
|
||||||
|
top: 45%;
|
||||||
|
left: 50%;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
p {
|
||||||
|
font-size: 24px;
|
||||||
|
color: $g9-mountain;
|
||||||
|
position: relative;
|
||||||
|
left: -50%;
|
||||||
|
top: -45%;
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: $g12-forge;
|
||||||
|
border-bottom: 1px solid $g12-forge;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,381 @@
|
||||||
|
// Libraries
|
||||||
|
import React, {PureComponent} from 'react'
|
||||||
|
import {flatten} from 'lodash'
|
||||||
|
|
||||||
|
// Components
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
ComponentColor,
|
||||||
|
ComponentStatus,
|
||||||
|
Dropdown,
|
||||||
|
MultiSelectDropdown,
|
||||||
|
} from 'src/reusable_ui'
|
||||||
|
|
||||||
|
// Utils
|
||||||
|
import {restartable} from 'src/shared/utils/restartable'
|
||||||
|
import {
|
||||||
|
fetchDBsToRPs,
|
||||||
|
fetchMeasurements,
|
||||||
|
fetchFields,
|
||||||
|
formatDBwithRP,
|
||||||
|
toComponentStatus,
|
||||||
|
renderScript,
|
||||||
|
getDefaultDBandRP,
|
||||||
|
DBsToRPs,
|
||||||
|
} from 'src/shared/utils/fluxScriptWizard'
|
||||||
|
|
||||||
|
// Types
|
||||||
|
import {RemoteDataState, Source} from 'src/types'
|
||||||
|
|
||||||
|
// This constant is selected so that dropdown menus will not overflow out of
|
||||||
|
// the `.flux-script-wizard--wizard` window
|
||||||
|
const DROPDOWN_MENU_HEIGHT = 110
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
source: Source
|
||||||
|
children: JSX.Element
|
||||||
|
isWizardActive: boolean
|
||||||
|
onSetIsWizardActive: (isWizardActive: boolean) => void
|
||||||
|
onAddToScript: (script: string) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
dbsToRPs: DBsToRPs
|
||||||
|
dbsToRPsStatus: RemoteDataState
|
||||||
|
selectedDB: string | null
|
||||||
|
selectedRP: string | null
|
||||||
|
measurements: string[]
|
||||||
|
measurementsStatus: RemoteDataState
|
||||||
|
selectedMeasurement: string | null
|
||||||
|
fields: string[]
|
||||||
|
fieldsStatus: RemoteDataState
|
||||||
|
selectedFields: string[] | null
|
||||||
|
}
|
||||||
|
|
||||||
|
class FluxScriptWizard extends PureComponent<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
dbsToRPs: {},
|
||||||
|
dbsToRPsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedDB: null,
|
||||||
|
selectedRP: null,
|
||||||
|
measurements: [],
|
||||||
|
measurementsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedMeasurement: null,
|
||||||
|
fields: [],
|
||||||
|
fieldsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedFields: null,
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchDBsToRPs = restartable(fetchDBsToRPs)
|
||||||
|
private fetchMeasurements = restartable(fetchMeasurements)
|
||||||
|
private fetchFields = restartable(fetchFields)
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
this.fetchAndSetDBsToRPs()
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
const {children, isWizardActive} = this.props
|
||||||
|
const {
|
||||||
|
measurements,
|
||||||
|
fields,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedFields,
|
||||||
|
} = this.state
|
||||||
|
|
||||||
|
if (!isWizardActive) {
|
||||||
|
return (
|
||||||
|
<div className="flux-script-wizard">
|
||||||
|
<div className="flux-script-wizard--children">{children}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flux-script-wizard">
|
||||||
|
<div className="flux-script-wizard--children">{children}</div>
|
||||||
|
<div
|
||||||
|
className="flux-script-wizard--backdrop"
|
||||||
|
onClick={this.handleClose}
|
||||||
|
/>
|
||||||
|
<div className="flux-script-wizard--wizard">
|
||||||
|
<div className="flux-script-wizard--wizard-header">
|
||||||
|
<h3>Flux Script Wizard</h3>
|
||||||
|
<div
|
||||||
|
className="flux-script-wizard--close"
|
||||||
|
onClick={this.handleClose}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flux-script-wizard--wizard-body">
|
||||||
|
<Form>
|
||||||
|
<Form.Element label="Choose a Bucket">
|
||||||
|
<Dropdown
|
||||||
|
status={this.bucketDropdownStatus}
|
||||||
|
selectedID={this.bucketDropdownSelectedID}
|
||||||
|
maxMenuHeight={DROPDOWN_MENU_HEIGHT}
|
||||||
|
onChange={this.handleSelectBucket}
|
||||||
|
>
|
||||||
|
{this.bucketDropdownItems}
|
||||||
|
</Dropdown>
|
||||||
|
</Form.Element>
|
||||||
|
<Form.Element label="Choose a Measurement">
|
||||||
|
<Dropdown
|
||||||
|
status={this.measurementDropdownStatus}
|
||||||
|
selectedID={selectedMeasurement}
|
||||||
|
maxMenuHeight={DROPDOWN_MENU_HEIGHT}
|
||||||
|
onChange={this.handleSelectMeasurement}
|
||||||
|
>
|
||||||
|
{measurements.map(measurement => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={measurement}
|
||||||
|
id={measurement}
|
||||||
|
value={measurement}
|
||||||
|
>
|
||||||
|
{measurement}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</Dropdown>
|
||||||
|
</Form.Element>
|
||||||
|
<Form.Element label="Choose Measurement Fields">
|
||||||
|
<MultiSelectDropdown
|
||||||
|
status={this.fieldsDropdownStatus}
|
||||||
|
selectedIDs={selectedFields}
|
||||||
|
emptyText={'All Fields'}
|
||||||
|
maxMenuHeight={DROPDOWN_MENU_HEIGHT}
|
||||||
|
onChange={this.handleSelectFields}
|
||||||
|
>
|
||||||
|
{fields.map(field => (
|
||||||
|
<Dropdown.Item key={field} id={field} value={{id: field}}>
|
||||||
|
{field}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
</MultiSelectDropdown>
|
||||||
|
</Form.Element>
|
||||||
|
<Form.Footer>
|
||||||
|
<Button
|
||||||
|
text="Insert Script"
|
||||||
|
color={ComponentColor.Primary}
|
||||||
|
status={this.buttonStatus}
|
||||||
|
onClick={this.handleAddToScript}
|
||||||
|
/>
|
||||||
|
</Form.Footer>
|
||||||
|
</Form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get bucketDropdownItems(): JSX.Element[] {
|
||||||
|
const {dbsToRPs} = this.state
|
||||||
|
|
||||||
|
const itemData = flatten(
|
||||||
|
Object.entries(dbsToRPs).map(([db, rps]) => rps.map(rp => [db, rp]))
|
||||||
|
)
|
||||||
|
|
||||||
|
const bucketDropdownItems = itemData.map(([db, rp]) => {
|
||||||
|
const name = formatDBwithRP(db, rp)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown.Item key={name} id={name} value={[db, rp]}>
|
||||||
|
{name}
|
||||||
|
</Dropdown.Item>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return bucketDropdownItems
|
||||||
|
}
|
||||||
|
|
||||||
|
private get bucketDropdownSelectedID(): string {
|
||||||
|
const {selectedDB, selectedRP} = this.state
|
||||||
|
return formatDBwithRP(selectedDB, selectedRP)
|
||||||
|
}
|
||||||
|
|
||||||
|
private get bucketDropdownStatus(): ComponentStatus {
|
||||||
|
const {dbsToRPsStatus} = this.state
|
||||||
|
const bucketDropdownStatus = toComponentStatus(dbsToRPsStatus)
|
||||||
|
|
||||||
|
return bucketDropdownStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private get measurementDropdownStatus(): ComponentStatus {
|
||||||
|
const {measurementsStatus} = this.state
|
||||||
|
const measurementDropdownStatus = toComponentStatus(measurementsStatus)
|
||||||
|
|
||||||
|
return measurementDropdownStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private get fieldsDropdownStatus(): ComponentStatus {
|
||||||
|
const {fieldsStatus} = this.state
|
||||||
|
const fieldsDropdownStatus = toComponentStatus(fieldsStatus)
|
||||||
|
|
||||||
|
return fieldsDropdownStatus
|
||||||
|
}
|
||||||
|
|
||||||
|
private get buttonStatus(): ComponentStatus {
|
||||||
|
const {dbsToRPsStatus, measurementsStatus, fieldsStatus} = this.state
|
||||||
|
const allDone = [dbsToRPsStatus, measurementsStatus, fieldsStatus].every(
|
||||||
|
s => s === RemoteDataState.Done
|
||||||
|
)
|
||||||
|
|
||||||
|
if (allDone) {
|
||||||
|
return ComponentStatus.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
return ComponentStatus.Disabled
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleClose = () => {
|
||||||
|
this.props.onSetIsWizardActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelectBucket = ([selectedDB, selectedRP]: [string, string]) => {
|
||||||
|
this.setState({selectedDB, selectedRP}, this.fetchAndSetMeasurements)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelectMeasurement = (selectedMeasurement: string) => {
|
||||||
|
this.setState({selectedMeasurement}, this.fetchAndSetFields)
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSelectFields = (selectedFields: string[]) => {
|
||||||
|
this.setState({selectedFields})
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleAddToScript = () => {
|
||||||
|
const {onSetIsWizardActive, onAddToScript} = this.props
|
||||||
|
const {
|
||||||
|
selectedDB,
|
||||||
|
selectedRP,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedFields,
|
||||||
|
} = this.state
|
||||||
|
const selectedBucket = formatDBwithRP(selectedDB, selectedRP)
|
||||||
|
const script = renderScript(
|
||||||
|
selectedBucket,
|
||||||
|
selectedMeasurement,
|
||||||
|
selectedFields
|
||||||
|
)
|
||||||
|
|
||||||
|
onAddToScript(script)
|
||||||
|
onSetIsWizardActive(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAndSetDBsToRPs = async () => {
|
||||||
|
const {source} = this.props
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
dbsToRPs: {},
|
||||||
|
dbsToRPsStatus: RemoteDataState.Loading,
|
||||||
|
selectedDB: null,
|
||||||
|
selectedRP: null,
|
||||||
|
measurements: [],
|
||||||
|
measurementsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedMeasurement: null,
|
||||||
|
fields: [],
|
||||||
|
fieldsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedFields: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let dbsToRPs
|
||||||
|
|
||||||
|
try {
|
||||||
|
dbsToRPs = await this.fetchDBsToRPs(source.links.proxy)
|
||||||
|
} catch {
|
||||||
|
this.setState({dbsToRPsStatus: RemoteDataState.Error})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const [selectedDB, selectedRP] = getDefaultDBandRP(dbsToRPs)
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
dbsToRPs,
|
||||||
|
dbsToRPsStatus: RemoteDataState.Done,
|
||||||
|
selectedDB,
|
||||||
|
selectedRP,
|
||||||
|
},
|
||||||
|
this.fetchAndSetMeasurements
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAndSetMeasurements = async () => {
|
||||||
|
const {source} = this.props
|
||||||
|
const {selectedDB} = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
measurements: [],
|
||||||
|
measurementsStatus: RemoteDataState.Loading,
|
||||||
|
selectedMeasurement: null,
|
||||||
|
fields: [],
|
||||||
|
fieldsStatus: RemoteDataState.NotStarted,
|
||||||
|
selectedFields: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let measurements
|
||||||
|
|
||||||
|
try {
|
||||||
|
measurements = await this.fetchMeasurements(
|
||||||
|
source.links.proxy,
|
||||||
|
selectedDB
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
this.setState({
|
||||||
|
measurements: [],
|
||||||
|
measurementsStatus: RemoteDataState.Error,
|
||||||
|
selectedMeasurement: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState(
|
||||||
|
{
|
||||||
|
measurements,
|
||||||
|
measurementsStatus: RemoteDataState.Done,
|
||||||
|
selectedMeasurement: measurements[0],
|
||||||
|
},
|
||||||
|
this.fetchAndSetFields
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fetchAndSetFields = async () => {
|
||||||
|
const {source} = this.props
|
||||||
|
const {selectedDB, selectedMeasurement} = this.state
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
fields: [],
|
||||||
|
fieldsStatus: RemoteDataState.Loading,
|
||||||
|
selectedFields: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
let fields
|
||||||
|
|
||||||
|
try {
|
||||||
|
fields = await this.fetchFields(
|
||||||
|
source.links.proxy,
|
||||||
|
selectedDB,
|
||||||
|
selectedMeasurement
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
this.setState({
|
||||||
|
fields: [],
|
||||||
|
fieldsStatus: RemoteDataState.Error,
|
||||||
|
selectedFields: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
fields,
|
||||||
|
fieldsStatus: RemoteDataState.Done,
|
||||||
|
selectedFields: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FluxScriptWizard
|
|
@ -282,7 +282,7 @@ class Division extends PureComponent<Props> {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.divisionRef || this.props.size >= 0.33) {
|
if (!this.divisionRef.current || this.props.size >= 0.33) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,43 +0,0 @@
|
||||||
import Deferred from 'src/worker/Deferred'
|
|
||||||
|
|
||||||
class Restarter {
|
|
||||||
private deferred?: Deferred
|
|
||||||
private id: number = 0
|
|
||||||
|
|
||||||
public perform<T>(promise: Promise<T>): Promise<T> {
|
|
||||||
if (!this.deferred) {
|
|
||||||
this.deferred = new Deferred()
|
|
||||||
}
|
|
||||||
|
|
||||||
this.id += 1
|
|
||||||
this.awaitResult(promise, this.id)
|
|
||||||
|
|
||||||
return this.deferred.promise
|
|
||||||
}
|
|
||||||
|
|
||||||
private awaitResult = async (promise: Promise<any>, id: number) => {
|
|
||||||
let result
|
|
||||||
let shouldReject = false
|
|
||||||
|
|
||||||
try {
|
|
||||||
result = await promise
|
|
||||||
} catch (error) {
|
|
||||||
result = error
|
|
||||||
shouldReject = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (id !== this.id) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldReject) {
|
|
||||||
this.deferred.reject(result)
|
|
||||||
} else {
|
|
||||||
this.deferred.resolve(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
this.deferred = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Restarter
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
import {proxy} from 'src/utils/queryUrlGenerator'
|
||||||
|
import {parseMetaQuery} from 'src/tempVars/parsing'
|
||||||
|
|
||||||
|
import {RemoteDataState} from 'src/types'
|
||||||
|
import {ComponentStatus} from 'src/reusable_ui'
|
||||||
|
|
||||||
|
export interface DBsToRPs {
|
||||||
|
[databaseName: string]: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchDBsToRPs(proxyLink: string): Promise<DBsToRPs> {
|
||||||
|
const dbsQuery = 'SHOW DATABASES'
|
||||||
|
const dbsResp = await proxy({source: proxyLink, query: dbsQuery})
|
||||||
|
const dbs = parseMetaQuery(dbsQuery, dbsResp.data).sort()
|
||||||
|
|
||||||
|
const rpsQuery = dbs
|
||||||
|
.map(db => `SHOW RETENTION POLICIES ON "${db}"`)
|
||||||
|
.join('; ')
|
||||||
|
|
||||||
|
const rpsResp = await proxy({source: proxyLink, query: rpsQuery})
|
||||||
|
|
||||||
|
const dbsToRPs: DBsToRPs = dbs.reduce((acc, db, i) => {
|
||||||
|
const series = rpsResp.data.results[i].series[0]
|
||||||
|
const namesIndex = series.columns.indexOf('name')
|
||||||
|
const rpNames = series.values.map(row => row[namesIndex])
|
||||||
|
|
||||||
|
return {...acc, [db]: rpNames}
|
||||||
|
}, {})
|
||||||
|
|
||||||
|
return dbsToRPs
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchMeasurements(
|
||||||
|
proxyLink: string,
|
||||||
|
database: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const query = `SHOW MEASUREMENTS ON "${database}"`
|
||||||
|
const resp = await proxy({source: proxyLink, query})
|
||||||
|
const measurements = parseMetaQuery(query, resp.data)
|
||||||
|
|
||||||
|
measurements.sort()
|
||||||
|
|
||||||
|
return measurements
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchFields(
|
||||||
|
proxyLink: string,
|
||||||
|
database: string,
|
||||||
|
measurement: string
|
||||||
|
): Promise<string[]> {
|
||||||
|
const query = `SHOW FIELD KEYS ON "${database}" FROM "${measurement}"`
|
||||||
|
const resp = await proxy({source: proxyLink, query})
|
||||||
|
const fields = parseMetaQuery(query, resp.data)
|
||||||
|
|
||||||
|
fields.sort()
|
||||||
|
|
||||||
|
return fields
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDBwithRP(db: string, rp: string): string {
|
||||||
|
return `${db}/${rp}`
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toComponentStatus(state: RemoteDataState): ComponentStatus {
|
||||||
|
switch (state) {
|
||||||
|
case RemoteDataState.NotStarted:
|
||||||
|
return ComponentStatus.Disabled
|
||||||
|
case RemoteDataState.Loading:
|
||||||
|
return ComponentStatus.Loading
|
||||||
|
case RemoteDataState.Error:
|
||||||
|
return ComponentStatus.Error
|
||||||
|
case RemoteDataState.Done:
|
||||||
|
return ComponentStatus.Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDefaultDBandRP(
|
||||||
|
dbsToRPs: DBsToRPs
|
||||||
|
): [string, string] | [null, null] {
|
||||||
|
const dbs = Object.keys(dbsToRPs)
|
||||||
|
|
||||||
|
// Pick telegraf if it exists
|
||||||
|
if (dbs.includes('telegraf')) {
|
||||||
|
return ['telegraf', dbsToRPs.telegraf[0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pick nothing if nothing exists
|
||||||
|
if (!dbs.length || !dbsToRPs[dbs[0][0]]) {
|
||||||
|
return [null, null]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise pick the first available DB and RP
|
||||||
|
return [dbs[0], dbsToRPs[dbs[0]][0]]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderScript(
|
||||||
|
selectedBucket: string,
|
||||||
|
selectedMeasurement: string,
|
||||||
|
selectedFields: string[]
|
||||||
|
): string {
|
||||||
|
let filterPredicate = `r._measurement == "${selectedMeasurement}"`
|
||||||
|
|
||||||
|
if (selectedFields.length) {
|
||||||
|
const fieldsPredicate = selectedFields
|
||||||
|
.map(f => `r._field == "${f}"`)
|
||||||
|
.join(' or ')
|
||||||
|
|
||||||
|
filterPredicate += ` and (${fieldsPredicate})`
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = `from(bucket: "${selectedBucket}")`
|
||||||
|
const range = `|> range(start: -1h)`
|
||||||
|
const filter = `|> filter(fn: (r) => ${filterPredicate})`
|
||||||
|
const script = [from, range, filter].join('\n ')
|
||||||
|
|
||||||
|
return script
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
import Deferred from 'src/worker/Deferred'
|
||||||
|
|
||||||
|
export function restartable<T extends any[], V>(
|
||||||
|
f: (...args: T) => Promise<V>
|
||||||
|
): ((...args: T) => Promise<V>) {
|
||||||
|
let deferred: Deferred
|
||||||
|
let id: number = 0
|
||||||
|
|
||||||
|
const checkResult = async (promise: Promise<V>, promiseID: number) => {
|
||||||
|
let result
|
||||||
|
let isOk = true
|
||||||
|
|
||||||
|
try {
|
||||||
|
result = await promise
|
||||||
|
} catch (error) {
|
||||||
|
result = error
|
||||||
|
isOk = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (promiseID !== id) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOk) {
|
||||||
|
deferred.resolve(result)
|
||||||
|
} else {
|
||||||
|
deferred.reject(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
deferred = null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (...args: T): Promise<V> => {
|
||||||
|
if (!deferred) {
|
||||||
|
deferred = new Deferred()
|
||||||
|
}
|
||||||
|
|
||||||
|
const promise = f(...args)
|
||||||
|
|
||||||
|
id += 1
|
||||||
|
checkResult(promise, id)
|
||||||
|
|
||||||
|
return deferred.promise
|
||||||
|
}
|
||||||
|
}
|
|
@ -87,8 +87,10 @@
|
||||||
@import 'components/annotation-control-bar';
|
@import 'components/annotation-control-bar';
|
||||||
@import 'components/annotation-editor';
|
@import 'components/annotation-editor';
|
||||||
@import 'src/shared/components/TimeMachine/RawFluxDataTable';
|
@import 'src/shared/components/TimeMachine/RawFluxDataTable';
|
||||||
|
@import 'src/shared/components/TimeMachine/FluxScriptWizard';
|
||||||
@import 'src/shared/components/Spinner';
|
@import 'src/shared/components/Spinner';
|
||||||
|
|
||||||
|
|
||||||
// Reusable UI Components
|
// Reusable UI Components
|
||||||
@import '../reusable_ui/components/panel/Panel.scss';
|
@import '../reusable_ui/components/panel/Panel.scss';
|
||||||
@import '../reusable_ui/components/overlays/Overlay.scss';
|
@import '../reusable_ui/components/overlays/Overlay.scss';
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
.time-machine-editor {
|
.time-machine-editor {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-warning {
|
.error-warning {
|
||||||
|
@ -28,4 +29,4 @@
|
||||||
.inline-error-message {
|
.inline-error-message {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: red;
|
background-color: red;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue