Introduce FluxScriptWizard
parent
1775261280
commit
093a5af033
|
@ -71,7 +71,7 @@ class TimeMachineEditor extends PureComponent<Props, State> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {script} = this.props
|
||||
const {script, children} = this.props
|
||||
|
||||
const options = {
|
||||
tabIndex: 1,
|
||||
|
@ -96,6 +96,7 @@ class TimeMachineEditor extends PureComponent<Props, State> {
|
|||
editorDidMount={this.handleMount}
|
||||
onKeyUp={this.handleKeyUp}
|
||||
/>
|
||||
{children}
|
||||
</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 = {
|
||||
type: 'File',
|
||||
start: 0,
|
||||
|
|
|
@ -171,3 +171,30 @@
|
|||
.dropdown--menu-container .fancy-scroll--track-h {
|
||||
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 selectedChild = children.find(child => child.props.id === selectedID)
|
||||
const dropdownLabel =
|
||||
(selectedChild && selectedChild.props.children) || titleText
|
||||
const isLoading = status === ComponentStatus.Loading
|
||||
|
||||
let dropdownLabel
|
||||
|
||||
if (isLoading) {
|
||||
dropdownLabel = <div className="dropdown--loading" />
|
||||
} else if (selectedChild) {
|
||||
dropdownLabel = selectedChild.props.children
|
||||
} else {
|
||||
dropdownLabel = titleText
|
||||
}
|
||||
|
||||
return (
|
||||
<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 => {
|
||||
const {onChange} = this.props
|
||||
onChange(value)
|
||||
|
@ -219,7 +236,7 @@ class Dropdown extends Component<Props, State> {
|
|||
private validateChildCount = (): void => {
|
||||
const {children} = this.props
|
||||
|
||||
if (React.Children.count(children) === 0) {
|
||||
if (this.shouldHaveChildren && React.Children.count(children) === 0) {
|
||||
throw new Error(
|
||||
'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.')
|
||||
}
|
||||
|
||||
if (mode === DropdownMode.Radio && selectedID === '') {
|
||||
if (
|
||||
mode === DropdownMode.Radio &&
|
||||
this.shouldHaveChildren &&
|
||||
selectedID === ''
|
||||
) {
|
||||
throw new Error('Dropdowns in Radio mode require a selectedID prop.')
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,33 +34,55 @@ class DropdownButton extends Component<Props> {
|
|||
}
|
||||
|
||||
public render() {
|
||||
const {onClick, status, children, title} = this.props
|
||||
const {onClick, children, title} = this.props
|
||||
return (
|
||||
<button
|
||||
className={this.classname}
|
||||
onClick={onClick}
|
||||
disabled={status === ComponentStatus.Disabled}
|
||||
disabled={this.isDisabled}
|
||||
title={title}
|
||||
>
|
||||
{this.icon}
|
||||
<span className="dropdown--selected">{children}</span>
|
||||
<span className="dropdown--caret icon caret-down" />
|
||||
{this.caret}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
private get classname(): string {
|
||||
const {status, active, color, size} = this.props
|
||||
const {active, color, size} = this.props
|
||||
|
||||
return classnames('dropdown--button button', {
|
||||
'button-stretch': true,
|
||||
'button--disabled': this.isDisabled,
|
||||
[`button-${color}`]: color,
|
||||
[`button-${size}`]: size,
|
||||
disabled: status === ComponentStatus.Disabled,
|
||||
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 {
|
||||
const {icon} = this.props
|
||||
|
||||
|
|
|
@ -135,9 +135,11 @@ class MultiSelectDropdown extends Component<Props, State> {
|
|||
_.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) => {
|
||||
if (i < selectedChildren.length - 1) {
|
||||
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 => {
|
||||
const {onChange, selectedIDs} = this.props
|
||||
let updatedSelection
|
||||
|
@ -254,7 +264,7 @@ class MultiSelectDropdown extends Component<Props, State> {
|
|||
private validateChildCount = (): void => {
|
||||
const {children} = this.props
|
||||
|
||||
if (React.Children.count(children) === 0) {
|
||||
if (this.shouldHaveChildren && React.Children.count(children) === 0) {
|
||||
throw new Error(
|
||||
'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
|
||||
import SchemaExplorer from 'src/flux/components/SchemaExplorer'
|
||||
import TimeMachineEditor from 'src/flux/components/TimeMachineEditor'
|
||||
import FluxScriptWizard from 'src/shared/components/TimeMachine/FluxScriptWizard'
|
||||
import Threesizer from 'src/shared/components/threesizer/Threesizer'
|
||||
import {
|
||||
Button,
|
||||
ComponentSize,
|
||||
ComponentColor,
|
||||
ComponentStatus,
|
||||
} from 'src/reusable_ui'
|
||||
import {Button, ComponentSize, ComponentColor} from 'src/reusable_ui'
|
||||
|
||||
// Constants
|
||||
import {HANDLE_VERTICAL} from 'src/shared/constants'
|
||||
import {emptyAST} from 'src/flux/constants'
|
||||
|
||||
// Utils
|
||||
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 {parseError} from 'src/flux/helpers/scriptBuilder'
|
||||
|
||||
|
@ -26,7 +21,8 @@ import {parseError} from 'src/flux/helpers/scriptBuilder'
|
|||
import {NotificationAction, Source} from 'src/types'
|
||||
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 {
|
||||
script: string
|
||||
|
@ -41,29 +37,27 @@ interface State {
|
|||
suggestions: Suggestion[]
|
||||
draftScript: string
|
||||
draftScriptStatus: ScriptStatus
|
||||
ast: object
|
||||
hasChangedScript: boolean
|
||||
isWizardActive: boolean
|
||||
}
|
||||
|
||||
class FluxQueryMaker extends PureComponent<Props, State> {
|
||||
private restarter: Restarter = new Restarter()
|
||||
private debouncer: Debouncer = new DefaultDebouncer()
|
||||
private getAST = restartable(getAST)
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props)
|
||||
|
||||
this.state = {
|
||||
suggestions: [],
|
||||
ast: {},
|
||||
draftScript: props.script,
|
||||
draftScriptStatus: {type: 'none', text: ''},
|
||||
hasChangedScript: false,
|
||||
isWizardActive: false,
|
||||
}
|
||||
}
|
||||
|
||||
public componentDidMount() {
|
||||
this.fetchSuggestions()
|
||||
this.updateBody()
|
||||
this.checkDraftScript()
|
||||
}
|
||||
|
||||
public render() {
|
||||
|
@ -72,26 +66,27 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
|||
suggestions,
|
||||
draftScript,
|
||||
draftScriptStatus,
|
||||
hasChangedScript,
|
||||
isWizardActive,
|
||||
} = this.state
|
||||
|
||||
const submitStatus = hasChangedScript
|
||||
? ComponentStatus.Default
|
||||
: ComponentStatus.Disabled
|
||||
|
||||
const divisions = [
|
||||
{
|
||||
name: 'Script',
|
||||
size: 0.66,
|
||||
headerOrientation: HANDLE_VERTICAL,
|
||||
headerButtons: [
|
||||
<Button
|
||||
key={0}
|
||||
text={'Submit'}
|
||||
titleText={'Submit Flux Query (Ctrl-Enter)'}
|
||||
text={'Script Wizard'}
|
||||
onClick={this.handleShowWizard}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
/>,
|
||||
<Button
|
||||
key={1}
|
||||
text={'Run Script'}
|
||||
onClick={this.handleSubmitScript}
|
||||
size={ComponentSize.ExtraSmall}
|
||||
color={ComponentColor.Primary}
|
||||
status={submitStatus}
|
||||
/>,
|
||||
],
|
||||
menuOptions: [],
|
||||
|
@ -103,11 +98,24 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
|||
suggestions={suggestions}
|
||||
onChangeScript={this.handleChangeDraftScript}
|
||||
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',
|
||||
size: 0.34,
|
||||
headerButtons: [],
|
||||
menuOptions: [],
|
||||
render: () => <SchemaExplorer source={source} notify={notify} />,
|
||||
|
@ -116,11 +124,18 @@ class FluxQueryMaker extends PureComponent<Props, State> {
|
|||
]
|
||||
|
||||
return (
|
||||
<Threesizer
|
||||
orientation={HANDLE_VERTICAL}
|
||||
divisions={divisions}
|
||||
containerClass="page-contents"
|
||||
/>
|
||||
<FluxScriptWizard
|
||||
source={source}
|
||||
isWizardActive={isWizardActive}
|
||||
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) {
|
||||
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 (
|
||||
draftScript: string
|
||||
): Promise<void> => {
|
||||
this.setState(
|
||||
{
|
||||
draftScript,
|
||||
hasChangedScript: true,
|
||||
},
|
||||
() => this.debouncer.call(this.updateBody, AST_DEBOUNCE_DELAY)
|
||||
this.setState({draftScript}, () =>
|
||||
this.debouncer.call(this.checkDraftScript, CHECK_SCRIPT_DELAY)
|
||||
)
|
||||
}
|
||||
|
||||
private updateBody = async () => {
|
||||
private checkDraftScript = async () => {
|
||||
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
|
||||
|
||||
try {
|
||||
ast = await this.restarter.perform(
|
||||
getAST({url: this.props.links.ast, body: draftScript})
|
||||
)
|
||||
await this.getAST({url: this.props.links.ast, body: draftScript})
|
||||
|
||||
draftScriptStatus = {type: 'success', text: ''}
|
||||
draftScriptStatus = VALID_SCRIPT_STATUS
|
||||
} catch (error) {
|
||||
ast = emptyAST
|
||||
draftScriptStatus = parseError(error)
|
||||
}
|
||||
|
||||
this.setState({
|
||||
ast,
|
||||
draftScriptStatus,
|
||||
})
|
||||
this.setState({draftScriptStatus})
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
if (!this.divisionRef || this.props.size >= 0.33) {
|
||||
if (!this.divisionRef.current || this.props.size >= 0.33) {
|
||||
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-editor';
|
||||
@import 'src/shared/components/TimeMachine/RawFluxDataTable';
|
||||
@import 'src/shared/components/TimeMachine/FluxScriptWizard';
|
||||
@import 'src/shared/components/Spinner';
|
||||
|
||||
|
||||
// Reusable UI Components
|
||||
@import '../reusable_ui/components/panel/Panel.scss';
|
||||
@import '../reusable_ui/components/overlays/Overlay.scss';
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
.time-machine-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.error-warning {
|
||||
|
@ -28,4 +29,4 @@
|
|||
.inline-error-message {
|
||||
color: white;
|
||||
background-color: red;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue