feat: add query type for notebooks (#18168)

pull/18204/head
Alex Boatwright 2020-05-22 10:51:47 -07:00 committed by GitHub
parent 78466ba736
commit 6e67fb4f6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 348 additions and 225 deletions

View File

@ -54,6 +54,23 @@ import {LIMIT} from 'src/resources/constants'
type Action = BucketAction | NotifyAction
export const fetchAllBuckets = async (orgID: string) => {
const resp = await api.getBuckets({
query: {orgID, limit: LIMIT},
})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const demoDataBuckets = await fetchDemoDataBuckets()
return normalize<Bucket, BucketEntities, string[]>(
[...resp.data.buckets, ...demoDataBuckets],
arrayOfBuckets
)
}
export const getBuckets = () => async (
dispatch: Dispatch<Action>,
getState: GetState
@ -65,20 +82,7 @@ export const getBuckets = () => async (
}
const org = getOrg(state)
const resp = await api.getBuckets({
query: {orgID: org.id, limit: LIMIT},
})
if (resp.status !== 200) {
throw new Error(resp.data.message)
}
const demoDataBuckets = await fetchDemoDataBuckets()
const buckets = normalize<Bucket, BucketEntities, string[]>(
[...resp.data.buckets, ...demoDataBuckets],
arrayOfBuckets
)
const buckets = await fetchAllBuckets(org.id)
dispatch(setBuckets(RemoteDataState.Done, buckets))
} catch (error) {

View File

@ -22,6 +22,8 @@ import {getAllVariables, asAssignment} from 'src/variables/selectors'
import {buildVarsOption} from 'src/variables/utils/buildVarsOption'
import {runQuery} from 'src/shared/apis/query'
import {parseResponse as parse} from 'src/shared/parsing/flux/response'
import {getOrg} from 'src/organizations/selectors'
import {fetchAllBuckets} from 'src/buckets/actions/thunks'
import {store} from 'src/index'
@ -109,8 +111,6 @@ const queryTagValues = async (orgID, bucket, tag) => {
export class LSPServer {
private server: WASMServer
private messageID: number = 0
private buckets: string[] = []
private orgID: string = ''
private documentVersions: {[key: string]: number} = {}
public store: Store<AppState & LocalStorage>
@ -125,7 +125,8 @@ export class LSPServer {
getTagKeys = async bucket => {
try {
const response = await queryTagKeys(this.orgID, bucket)
const org = getOrg(this.store.getState())
const response = await queryTagKeys(org.id, bucket)
return parseQueryResponse(response)
} catch (e) {
console.error(e)
@ -135,7 +136,8 @@ export class LSPServer {
getTagValues = async (bucket, tag) => {
try {
const response = await queryTagValues(this.orgID, bucket, tag)
const org = getOrg(this.store.getState())
const response = await queryTagValues(org.id, bucket, tag)
return parseQueryResponse(response)
} catch (e) {
console.error(e)
@ -143,13 +145,22 @@ export class LSPServer {
}
}
getBuckets = () => {
return Promise.resolve(this.buckets)
getBuckets = async () => {
try {
const org = getOrg(this.store.getState())
const buckets = await fetchAllBuckets(org.id)
return Object.values(buckets.entities.buckets).map(b => b.name)
} catch (e) {
console.error(e)
return []
}
}
getMeasurements = async (bucket: string) => {
try {
const response = await queryMeasurements(this.orgID, bucket)
const org = getOrg(this.store.getState())
const response = await queryMeasurements(org.id, bucket)
return parseQueryResponse(response)
} catch (e) {
console.error(e)
@ -157,14 +168,6 @@ export class LSPServer {
}
}
updateBuckets(buckets: string[]) {
this.buckets = buckets
}
setOrg(orgID: string) {
this.orgID = orgID
}
initialize() {
return this.send(initialize(this.currentMessageID))
}

View File

@ -1,4 +1,4 @@
import React, {FC, useContext} from 'react'
import React, {FC, useContext, useCallback, useMemo} from 'react'
import {Page} from '@influxdata/clockface'
import {NotebookContext} from 'src/notebooks/context/notebook'
@ -14,25 +14,34 @@ import {SubmitQueryButton} from 'src/timeMachine/components/SubmitQueryButton'
const FULL_WIDTH = true
const Header: FC = () => {
const {id} = useContext(NotebookContext)
const {timeContext, addTimeContext, updateTimeContext} = useContext(
TimeContext
)
const ConnectedTimeZoneDropdown = React.memo(() => {
const {timeZone, onSetTimeZone} = useContext(AppSettingContext)
if (!timeContext.hasOwnProperty(id)) {
addTimeContext(id)
return null
return <TimeZoneDropdown timeZone={timeZone} onSetTimeZone={onSetTimeZone} />
})
const ConnectedTimeRangeDropdown = ({context, update}) => {
const {range} = context
const updateRange = range => {
update({
range,
})
}
const {refresh, range} = timeContext[id]
return useMemo(() => {
return <TimeRangeDropdown timeRange={range} onSetTimeRange={updateRange} />
}, [range])
}
function updateRefresh(interval: number) {
const ConnectedAutoRefreshDropdown = ({context, update}) => {
const {refresh} = context
const updateRefresh = (interval: number) => {
const status =
interval === 0 ? AutoRefreshStatus.Paused : AutoRefreshStatus.Active
updateTimeContext(id, {
update({
refresh: {
status,
interval,
@ -40,13 +49,46 @@ const Header: FC = () => {
} as TimeBlock)
}
function updateRange(range) {
updateTimeContext(id, {
...timeContext[id],
range,
})
return useMemo(
() => (
<AutoRefreshDropdown
selected={refresh}
onChoose={updateRefresh}
showManualRefresh={false}
/>
),
[refresh]
)
}
const EnsureTimeContextExists: FC = () => {
const {id} = useContext(NotebookContext)
const {timeContext, addTimeContext, updateTimeContext} = useContext(
TimeContext
)
const update = useCallback(
data => {
updateTimeContext(id, data)
},
[id]
)
if (!timeContext.hasOwnProperty(id)) {
addTimeContext(id)
return null
}
return (
<>
<ConnectedTimeZoneDropdown />
<ConnectedTimeRangeDropdown context={timeContext[id]} update={update} />
<ConnectedAutoRefreshDropdown context={timeContext[id]} update={update} />
</>
)
}
const Header: FC = () => {
function submit() {} // eslint-disable-line @typescript-eslint/no-empty-function
return (
@ -60,16 +102,7 @@ const Header: FC = () => {
</Page.ControlBarLeft>
<Page.ControlBarRight>
<div className="notebook-header--buttons">
<TimeZoneDropdown
timeZone={timeZone}
onSetTimeZone={onSetTimeZone}
/>
<TimeRangeDropdown timeRange={range} onSetTimeRange={updateRange} />
<AutoRefreshDropdown
selected={refresh}
onChoose={updateRefresh}
showManualRefresh={false}
/>
<EnsureTimeContextExists />
<SubmitQueryButton
submitButtonDisabled={false}
queryStatus={RemoteDataState.NotStarted}
@ -82,8 +115,6 @@ const Header: FC = () => {
)
}
export {Header}
export default () => (
<TimeProvider>
<AppSettingProvider>
@ -91,3 +122,5 @@ export default () => (
</AppSettingProvider>
</TimeProvider>
)
export {Header}

View File

@ -4,7 +4,6 @@ import {Page} from '@influxdata/clockface'
import {NotebookProvider} from 'src/notebooks/context/notebook'
import Header from 'src/notebooks/components/Header'
import PipeList from 'src/notebooks/components/PipeList'
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
// NOTE: uncommon, but using this to scope the project
// within the page and not bleed it's dependancies outside
@ -24,5 +23,4 @@ const NotebookPage: FC = () => {
)
}
export {NotebookPanel}
export default NotebookPage

View File

@ -1,4 +1,4 @@
import {FC, createElement} from 'react'
import {FC, createElement, useMemo} from 'react'
import {PIPE_DEFINITIONS, PipeProp} from 'src/notebooks'
@ -10,7 +10,10 @@ const Pipe: FC<PipeProp> = props => {
return null
}
return createElement(PIPE_DEFINITIONS[data.type].component, props)
return useMemo(
() => createElement(PIPE_DEFINITIONS[data.type].component, props),
[props.data]
)
}
export default Pipe

View File

@ -1,30 +1,46 @@
import React, {FC, useContext, createElement} from 'react'
import React, {FC, useContext, useCallback, createElement, useMemo} from 'react'
import {PipeContextProps, PipeData} from 'src/notebooks'
import Pipe from 'src/notebooks/components/Pipe'
import {NotebookContext} from 'src/notebooks/context/notebook'
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
const PipeList: FC = () => {
const {id, pipes, updatePipe} = useContext(NotebookContext)
const _pipes = pipes.map((pipe, index) => {
const panel: FC<PipeContextProps> = props => {
interface NotebookPipeProps {
index: number
data: PipeData
onUpdate: (index: number, pipe: PipeData) => void
}
const NotebookPipe: FC<NotebookPipeProps> = ({index, data, onUpdate}) => {
const panel: FC<PipeContextProps> = useMemo(
() => props => {
const _props = {
...props,
index,
}
return createElement(NotebookPanel, _props)
}
const onUpdate = (data: PipeData) => {
updatePipe(index, data)
}
},
[index]
)
const _onUpdate = (data: PipeData) => {
onUpdate(index, data)
}
return <Pipe data={data} onUpdate={_onUpdate} Context={panel} />
}
const PipeList: FC = () => {
const {id, pipes, updatePipe} = useContext(NotebookContext)
const update = useCallback(updatePipe, [id])
const _pipes = pipes.map((_, index) => {
return (
<Pipe
<NotebookPipe
key={`pipe-${id}-${index}`}
data={pipe}
onUpdate={onUpdate}
Context={panel}
index={index}
data={pipes[index]}
onUpdate={update}
/>
)
})

View File

@ -1,5 +1,5 @@
// Libraries
import React, {FC, useContext} from 'react'
import React, {FC, useContext, useCallback} from 'react'
import classnames from 'classnames'
// Components
@ -20,15 +20,57 @@ export interface Props extends PipeContextProps {
index: number
}
const NotebookPanel: FC<Props> = ({index, children}) => {
const {pipes, removePipe, movePipe, meta} = useContext(NotebookContext)
export interface HeaderProps {
index: number
}
const NotebookPanelHeader: FC<HeaderProps> = ({index}) => {
const {pipes, removePipe, movePipe} = useContext(NotebookContext)
const canBeMovedUp = index > 0
const canBeMovedDown = index < pipes.length - 1
const canBeRemoved = index !== 0
const moveUp = canBeMovedUp ? () => movePipe(index, index - 1) : null
const moveDown = canBeMovedDown ? () => movePipe(index, index + 1) : null
const remove = canBeRemoved ? () => removePipe(index) : null
const moveUp = useCallback(
canBeMovedUp ? () => movePipe(index, index - 1) : null,
[index, pipes]
)
const moveDown = useCallback(
canBeMovedDown ? () => movePipe(index, index + 1) : null,
[index, pipes]
)
const remove = useCallback(canBeRemoved ? () => removePipe(index) : null, [
index,
pipes,
])
return (
<div className="notebook-panel--header">
<FlexBox
className="notebook-panel--header-left"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexStart}
>
<NotebookPanelTitle index={index} />
</FlexBox>
<FlexBox
className="notebook-panel--header-right"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexEnd}
>
<MovePanelButton direction="up" onClick={moveUp} />
<MovePanelButton direction="down" onClick={moveDown} />
<PanelVisibilityToggle index={index} />
<RemovePanelButton onRemove={remove} />
</FlexBox>
</div>
)
}
const NotebookPanel: FC<Props> = props => {
const {index, children} = props
const {meta} = useContext(NotebookContext)
const isVisible = meta[index].visible
@ -39,28 +81,8 @@ const NotebookPanel: FC<Props> = ({index, children}) => {
return (
<div className={panelClassName}>
<div className="notebook-panel--header">
<FlexBox
className="notebook-panel--header-left"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexStart}
>
<NotebookPanelTitle index={index} />
</FlexBox>
<FlexBox
className="notebook-panel--header-right"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexEnd}
>
<MovePanelButton direction="up" onClick={moveUp} />
<MovePanelButton direction="down" onClick={moveDown} />
<PanelVisibilityToggle index={index} />
<RemovePanelButton onRemove={remove} />
</FlexBox>
</div>
<div className="notebook-panel--body">{isVisible && children}</div>
<NotebookPanelHeader index={index} />
<div className="notebook-panel--body">{children}</div>
</div>
)
}

View File

@ -30,22 +30,20 @@ export const AppSettingContext = React.createContext<AppSettingContextType>(
DEFAULT_CONTEXT
)
export const AppSettingProvider: FC<Props> = ({
timeZone,
onSetTimeZone,
children,
}) => {
return (
<AppSettingContext.Provider
value={{
timeZone,
onSetTimeZone,
}}
>
{children}
</AppSettingContext.Provider>
)
}
export const AppSettingProvider: FC<Props> = React.memo(
({timeZone, onSetTimeZone, children}) => {
return (
<AppSettingContext.Provider
value={{
timeZone,
onSetTimeZone,
}}
>
{children}
</AppSettingContext.Provider>
)
}
)
const mstp = (state: AppState): StateProps => {
return {

View File

@ -1,4 +1,4 @@
import React, {FC, useState} from 'react'
import React, {FC, useState, useCallback} from 'react'
import {PipeData} from 'src/notebooks'
export interface PipeMeta {
@ -49,68 +49,86 @@ export const NotebookProvider: FC = ({children}) => {
const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes)
const [meta, setMeta] = useState(DEFAULT_CONTEXT.meta)
function addPipe(pipe: PipeData) {
const add = data => {
return pipes => {
pipes.push(data)
const _setPipes = useCallback(setPipes, [id])
const _setMeta = useCallback(setMeta, [id])
const addPipe = useCallback(
(pipe: PipeData) => {
const add = data => {
return pipes => {
pipes.push(data)
return pipes.slice()
}
}
_setPipes(add(pipe))
_setMeta(
add({
title: `Notebook_${++GENERATOR_INDEX}`,
visible: true,
})
)
},
[id]
)
const updatePipe = useCallback(
(idx: number, pipe: PipeData) => {
_setPipes(pipes => {
pipes[idx] = {
...pipes[idx],
...pipe,
}
return pipes.slice()
})
},
[id]
)
const updateMeta = useCallback(
(idx: number, pipe: PipeMeta) => {
_setMeta(pipes => {
pipes[idx] = {
...pipes[idx],
...pipe,
}
return pipes.slice()
})
},
[id]
)
const movePipe = useCallback(
(currentIdx: number, newIdx: number) => {
const move = list => {
const idx = ((newIdx % list.length) + list.length) % list.length
if (idx === currentIdx) {
return list
}
const pipe = list.splice(currentIdx, 1)
list.splice(idx, 0, pipe[0])
return list.slice()
}
_setPipes(move)
_setMeta(move)
},
[id]
)
const removePipe = useCallback(
(idx: number) => {
const remove = pipes => {
pipes.splice(idx, 1)
return pipes.slice()
}
}
setPipes(add(pipe))
setMeta(
add({
title: `Notebook_${++GENERATOR_INDEX}`,
visible: true,
})
)
}
function updatePipe(idx: number, pipe: PipeData) {
setPipes(pipes => {
pipes[idx] = {
...pipes[idx],
...pipe,
}
return pipes.slice()
})
}
function updateMeta(idx: number, pipe: PipeMeta) {
setMeta(pipes => {
pipes[idx] = {
...pipes[idx],
...pipe,
}
return pipes.slice()
})
}
function movePipe(currentIdx: number, newIdx: number) {
const move = list => {
const idx = ((newIdx % list.length) + list.length) % list.length
if (idx === currentIdx) {
return list
}
const pipe = list.splice(currentIdx, 1)
list.splice(idx, 0, pipe[0])
return list.slice()
}
setPipes(move)
setMeta(move)
}
function removePipe(idx: number) {
const remove = pipes => {
pipes.splice(idx, 1)
return pipes.slice()
}
setPipes(remove)
setMeta(remove)
}
_setPipes(remove)
_setMeta(remove)
},
[id]
)
return (
<NotebookContext.Provider

View File

@ -1,4 +1,4 @@
import React, {FC, useState} from 'react'
import React, {FC, useState, useCallback} from 'react'
import {AutoRefresh, TimeRange} from 'src/types'
import {DEFAULT_TIME_RANGE} from 'src/shared/constants/timeRanges'
import {AUTOREFRESH_DEFAULT} from 'src/shared/constants'
@ -36,7 +36,7 @@ export const TimeContext = React.createContext<TimeContext>(DEFAULT_CONTEXT)
export const TimeProvider: FC = ({children}) => {
const [timeContext, setTimeContext] = useState({})
function addTimeContext(id: string, block?: TimeBlock) {
const addTimeContext = useCallback((id: string, block?: TimeBlock) => {
setTimeContext(ranges => {
if (ranges.hasOwnProperty(id)) {
throw new Error(
@ -50,9 +50,9 @@ export const TimeProvider: FC = ({children}) => {
[id]: {...(block || DEFAULT_STATE)},
}
})
}
}, [])
function updateTimeContext(id: string, block: TimeBlock) {
const updateTimeContext = useCallback((id: string, block: TimeBlock) => {
setTimeContext(ranges => {
return {
...ranges,
@ -62,9 +62,9 @@ export const TimeProvider: FC = ({children}) => {
},
}
})
}
}, [])
function removeTimeContext(id: string) {
const removeTimeContext = useCallback((id: string) => {
setTimeContext(ranges => {
if (!ranges.hasOwnProperty(id)) {
throw new Error(`TimeContext[${id}] doesn't exist`)
@ -73,7 +73,7 @@ export const TimeProvider: FC = ({children}) => {
delete ranges[id]
return {...ranges}
})
}
}, [])
return (
<TimeContext.Provider

View File

@ -0,0 +1,23 @@
import {register} from 'src/notebooks'
import View from './view'
import './style.scss'
register({
type: 'query',
component: View,
button: 'Custom Script',
initial: {
activeQuery: 0,
queries: [
{
text: '',
editMode: 'advanced',
builderConfig: {
buckets: [],
tags: [],
functions: [],
},
},
],
},
})

View File

@ -0,0 +1,4 @@
.notebook-query {
height: 300px;
position: relative;
}

View File

@ -0,0 +1,33 @@
import React, {FC, useMemo} from 'react'
import {PipeProp} from 'src/notebooks'
import FluxMonacoEditor from 'src/shared/components/FluxMonacoEditor'
const Query: FC<PipeProp> = ({data, onUpdate, Context}) => {
const {queries, activeQuery} = data
const query = queries[activeQuery]
function updateText(text) {
const _queries = queries.slice()
_queries[activeQuery] = {
...queries[activeQuery],
text,
}
onUpdate({queries: _queries})
}
return useMemo(
() => (
<Context>
<FluxMonacoEditor
script={query.text}
onChangeScript={updateText}
onSubmitScript={() => {}}
/>
</Context>
),
[query.text]
)
}
export default Query

View File

@ -143,6 +143,7 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border;
padding-top: 0;
// flex: 1 0 0;
position: relative;
min-height: 200px;
}
// Special styling for query builder inside notebook panel

View File

@ -1,30 +0,0 @@
// Libraries
import {FC} from 'react'
import {connect} from 'react-redux'
import {AppState, Bucket, ResourceType} from 'src/types'
import {getAll} from 'src/resources/selectors'
import {getOrg} from 'src/organizations/selectors'
import loadServer from 'src/external/monaco.flux.server'
const FluxBucketProvider: FC<{}> = () => {
return null
}
const mstp = (state: AppState): {} => {
const buckets = getAll<Bucket>(state, ResourceType.Buckets)
const org = getOrg(state)
loadServer().then(server => {
server.updateBuckets(buckets.map(b => b.name))
server.setOrg(org.id || '')
})
return {}
}
export default connect<{}, {}>(
mstp,
null
)(FluxBucketProvider)

View File

@ -4,8 +4,6 @@ import {ProtocolToMonacoConverter} from 'monaco-languageclient/lib/monaco-conver
// Components
import MonacoEditor from 'react-monaco-editor'
import FluxBucketProvider from 'src/shared/components/FluxBucketProvider'
import GetResources from 'src/resources/components/GetResources'
// Utils
import FLUXLANGID from 'src/external/monaco.flux.syntax'
@ -16,7 +14,7 @@ import {isFlagEnabled} from 'src/shared/utils/featureFlag'
// Types
import {OnChangeScript} from 'src/types/flux'
import {EditorType, ResourceType} from 'src/types'
import {EditorType} from 'src/types'
import './FluxMonacoEditor.scss'
import {editor as monacoEditor} from 'monaco-editor'
@ -103,9 +101,6 @@ const FluxEditorMonaco: FC<Props> = ({
return (
<div className="flux-editor--monaco" data-testid="flux-editor">
<GetResources resources={[ResourceType.Buckets]}>
<FluxBucketProvider />
</GetResources>
<MonacoEditor
language={FLUXLANGID}
theme={THEME_NAME}

View File

@ -28,8 +28,8 @@ interface StateProps {
}
interface DispatchProps {
onSetActiveQueryText: typeof setActiveQueryText
onSubmitQueries: typeof saveAndExecuteQueries
onSetActiveQueryText: typeof setActiveQueryText | ((text: string) => void)
onSubmitQueries: typeof saveAndExecuteQueries | (() => void)
}
type Props = StateProps & DispatchProps
@ -160,6 +160,8 @@ const TimeMachineFluxEditor: FC<Props> = ({
)
}
export {TimeMachineFluxEditor}
const mstp = (state: AppState) => {
const activeQueryText = getActiveQuery(state).text
const {activeTab} = getActiveTimeMachine(state)