Introduce FluxScriptWizard

pull/4574/head
Christopher Henn 2018-10-07 14:39:40 -07:00 committed by Chris Henn
parent 1775261280
commit 093a5af033
15 changed files with 782 additions and 121 deletions

View File

@ -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>
)
}

View File

@ -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,

View File

@ -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..."
}

View File

@ -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.')
}
}

View File

@ -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

View File

@ -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.'
)

View File

@ -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> => {

View File

@ -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;
}
}
}

View File

@ -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}
>
&times;
</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

View File

@ -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
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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';

View File

@ -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;
}
}