feat: add visualizations to notebooks (#18348)

pull/18365/head
Alex Boatwright 2020-06-04 11:00:02 -07:00 committed by GitHub
parent 0caee29f24
commit 1ccac90a8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 243 additions and 42 deletions

View File

@ -15,6 +15,7 @@ const NotebookPipe: FC<NotebookPipeProps> = ({
data,
onUpdate,
results,
loading,
}) => {
const panel: FC<PipeContextProps> = useMemo(
() => props => {
@ -33,7 +34,13 @@ const NotebookPipe: FC<NotebookPipeProps> = ({
}
return (
<Pipe data={data} onUpdate={_onUpdate} results={results} Context={panel} />
<Pipe
data={data}
onUpdate={_onUpdate}
loading={loading}
results={results}
Context={panel}
/>
)
}

View File

@ -11,7 +11,7 @@ import EmptyPipeList from 'src/notebooks/components/EmptyPipeList'
import {DapperScrollbars} from '@influxdata/clockface'
const PipeList: FC = () => {
const {id, pipes, updatePipe, results} = useContext(NotebookContext)
const {id, pipes, updatePipe, results, meta} = useContext(NotebookContext)
const {scrollPosition} = useContext(ScrollContext)
const update = useCallback(updatePipe, [id])
@ -27,6 +27,7 @@ const PipeList: FC = () => {
data={pipes[index]}
onUpdate={update}
results={results[index]}
loading={meta[index].loading}
/>
)
})

View File

@ -1,7 +1,10 @@
// Libraries
import React, {FC, useContext, useEffect} from 'react'
import {SubmitQueryButton} from 'src/timeMachine/components/SubmitQueryButton'
import QueryProvider, {QueryContext} from 'src/notebooks/context/query'
import QueryProvider, {
QueryContext,
BothResults,
} from 'src/notebooks/context/query'
import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook'
import {TimeContext} from 'src/notebooks/context/time'
import {IconFont} from '@influxdata/clockface'
@ -47,7 +50,7 @@ export const Submit: FC = () => {
instances: [index],
requirements,
})
} else {
} else if (stages.length) {
stages[stages.length - 1].instances.push(index)
}
@ -59,12 +62,21 @@ export const Submit: FC = () => {
.map(([key, value]) => `${key} = (\n${value}\n)\n\n`)
.join('') + queryStruct.text
return query(queryText).then(response => {
queryStruct.instances.forEach(index => {
updateMeta(index, {loading: RemoteDataState.Done} as PipeMeta)
updateResult(index, response)
return query(queryText)
.then(response => {
queryStruct.instances.forEach(index => {
updateMeta(index, {loading: RemoteDataState.Done} as PipeMeta)
updateResult(index, response)
})
})
.catch(e => {
queryStruct.instances.forEach(index => {
updateMeta(index, {loading: RemoteDataState.Error} as PipeMeta)
updateResult(index, {
error: e.message,
} as BothResults)
})
})
})
})
}

View File

@ -92,7 +92,7 @@ const NotebookPanel: FC<Props> = ({index, children, controls}) => {
useEffect(() => {
updateMeta(index, {panelRef} as PipeMeta)
})
}, [])
const panelClassName = classnames('notebook-panel', {
[`notebook-panel__visible`]: isVisible,

View File

@ -74,18 +74,30 @@ export const NotebookProvider: FC = ({children}) => {
return pipes.slice()
}
}
if (pipes.length && pipe.type !== 'query') {
_setResults(add({...results[results.length - 1]}))
_setMeta(
add({
title: `Cell_${++GENERATOR_INDEX}`,
visible: true,
loading: meta[meta.length - 1].loading,
focus: false,
})
)
} else {
_setResults(add({}))
_setMeta(
add({
title: `Cell_${++GENERATOR_INDEX}`,
visible: true,
loading: RemoteDataState.NotStarted,
focus: false,
})
)
}
_setPipes(add(pipe))
_setResults(add({}))
_setMeta(
add({
title: `Cell_${++GENERATOR_INDEX}`,
visible: true,
loading: RemoteDataState.NotStarted,
focus: false,
})
)
},
[id]
[id, pipes, meta, results]
)
const updatePipe = useCallback(
@ -98,7 +110,7 @@ export const NotebookProvider: FC = ({children}) => {
return pipes.slice()
})
},
[id]
[id, pipes]
)
const updateMeta = useCallback(
@ -111,7 +123,7 @@ export const NotebookProvider: FC = ({children}) => {
return pipes.slice()
})
},
[id]
[id, meta]
)
const updateResult = useCallback(
@ -123,7 +135,7 @@ export const NotebookProvider: FC = ({children}) => {
return pipes.slice()
})
},
[id]
[id, results]
)
const movePipe = useCallback(

View File

@ -14,6 +14,7 @@ import {fromFlux as parse, FromFluxResult} from '@influxdata/giraffe'
export interface BothResults {
parsed: FromFluxResult
raw: string
error?: string
}
export interface QueryContextType {
@ -46,8 +47,7 @@ export const QueryProvider: FC<Props> = ({children, variables, org}) => {
return runQuery(org.id, text, extern)
.promise.then(raw => {
if (raw.type !== 'SUCCESS') {
// TODO actually pipe this somewhere
throw new Error('Unable to fetch results')
throw new Error(raw.message)
}
return raw
@ -56,6 +56,7 @@ export const QueryProvider: FC<Props> = ({children, variables, org}) => {
return {
raw: raw.csv,
parsed: parse(raw.csv),
error: null,
}
})
}

View File

@ -1,4 +1,5 @@
import {FunctionComponent, ComponentClass, ReactNode} from 'react'
import {RemoteDataState} from 'src/types'
import {BothResults} from 'src/notebooks/context/query'
export interface PipeContextProps {
@ -12,6 +13,7 @@ export interface PipeProp {
data: PipeData
onUpdate: (data: PipeData) => void
results?: BothResults
loading: RemoteDataState
Context:
| FunctionComponent<PipeContextProps>

View File

@ -0,0 +1,38 @@
import {register} from 'src/notebooks'
import View from './view'
import './style.scss'
register({
type: 'visualization',
component: View,
button: 'Visualization',
initial: {
properties: {
type: 'xy',
position: 'overlaid',
legend: {},
note: '',
showNoteWhenEmpty: false,
axes: {
x: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
y: {
bounds: ['', ''],
label: '',
prefix: '',
suffix: '',
base: '10',
scale: 'linear',
},
},
geom: 'line',
shape: 'chronograf-v2',
},
},
})

View File

@ -0,0 +1,10 @@
.notebook-visualization {
min-height: 250px;
.empty-graph-error {
height: 200px;
}
.time-machine-tables {
height: 200px;
}
}

View File

@ -0,0 +1,113 @@
import React, {FC, useContext} from 'react'
import {PipeProp} from 'src/notebooks'
import EmptyQueryView, {ErrorFormat} from 'src/shared/components/EmptyQueryView'
import ViewSwitcher from 'src/shared/components/ViewSwitcher'
import {ViewTypeDropdown} from 'src/timeMachine/components/view_options/ViewTypeDropdown'
import {checkResultsLength} from 'src/shared/utils/vis'
import {ViewType} from 'src/types'
import {createView} from 'src/views/helpers'
// NOTE we dont want any pipe component to be directly dependent
// to any notebook concepts as this'll limit future reusability
// but timezone seems like an app setting, and its existance within
// the notebook folder is purely a convenience
import {AppSettingContext} from 'src/notebooks/context/app'
const Visualization: FC<PipeProp> = ({
data,
results,
onUpdate,
Context,
loading,
}) => {
const {timeZone} = useContext(AppSettingContext)
const updateType = (type: ViewType) => {
const newView = createView(type)
if (newView.properties.type === 'table' && results.parsed) {
const existing = (newView.properties.fieldOptions || []).reduce(
(prev, curr) => {
prev[curr.internalName] = curr
return prev
},
{}
)
results.parsed.table.columnKeys
.filter(o => !existing.hasOwnProperty(o))
.filter(o => !['result', '', 'table', 'time'].includes(o))
.forEach(o => {
existing[o] = {
internalName: o,
displayName: o,
visible: true,
}
})
const fieldOptions = Object.keys(existing).map(e => existing[e])
newView.properties = {...newView.properties, fieldOptions}
}
if (
(newView.properties.type === 'histogram' ||
newView.properties.type === 'scatter') &&
results.parsed
) {
newView.properties.fillColumns = results.parsed.fluxGroupKeyUnion
}
if (newView.properties.type === 'scatter' && results.parsed) {
newView.properties.symbolColumns = results.parsed.fluxGroupKeyUnion
}
if (
(newView.properties.type === 'heatmap' ||
newView.properties.type === 'scatter') &&
results.parsed
) {
newView.properties.xColumn =
['_time', '_start', '_stop'].filter(field =>
results.parsed.table.columnKeys.includes(field)
)[0] || results.parsed.table.columnKeys[0]
newView.properties.yColumn =
['_value'].filter(field =>
results.parsed.table.columnKeys.includes(field)
)[0] || results.parsed.table.columnKeys[0]
}
onUpdate({
properties: newView.properties,
})
}
const controls = (
<ViewTypeDropdown
viewType={data.properties.type}
onUpdateType={updateType}
/>
)
return (
<Context controls={controls}>
<div className="notebook-visualization">
<EmptyQueryView
loading={loading}
errorMessage={results.error}
errorFormat={ErrorFormat.Scroll}
hasResults={checkResultsLength(results.parsed)}
>
<ViewSwitcher
giraffeResult={results.parsed}
files={[results.raw]}
loading={loading}
properties={data.properties}
timeZone={timeZone}
theme="dark"
/>
</EmptyQueryView>
</div>
</Context>
)
}
export default Visualization

View File

@ -20,12 +20,12 @@ export enum ErrorFormat {
}
interface Props {
errorMessage: string
errorMessage?: string
errorFormat: ErrorFormat
isInitialFetch: boolean
isInitialFetch?: boolean
loading: RemoteDataState
hasResults: boolean
queries: DashboardQuery[]
queries?: DashboardQuery[]
fallbackNote?: string
}
@ -41,7 +41,10 @@ export default class EmptyQueryView extends PureComponent<Props> {
errorFormat,
} = this.props
if (loading === RemoteDataState.NotStarted || !queries.length) {
if (
loading === RemoteDataState.NotStarted ||
(queries && !queries.length)
) {
return (
<EmptyGraphMessage
message={emptyGraphCopy}

View File

@ -31,12 +31,12 @@ import {
interface Props {
giraffeResult: FromFluxResult
files: string[]
files?: string[]
loading: RemoteDataState
properties: QueryViewProperties | CheckViewProperties
timeZone: TimeZone
statuses: StatusRow[][]
timeRange: TimeRange | null
statuses?: StatusRow[][]
timeRange?: TimeRange | null
checkType?: CheckType
checkThresholds?: Threshold[]
theme: Theme

View File

@ -45,7 +45,7 @@ interface Props {
children: (config: Config) => JSX.Element
fluxGroupKeyUnion: string[]
loading: RemoteDataState
timeRange: TimeRange | null
timeRange?: TimeRange | null
table: Table
timeZone: TimeZone
viewProperties: XYViewProperties

View File

@ -15,15 +15,15 @@ import {getActiveTimeMachine} from 'src/timeMachine/selectors'
import {VIS_GRAPHICS} from 'src/timeMachine/constants/visGraphics'
// Types
import {View, NewView, AppState, ViewType} from 'src/types'
import {AppState, ViewType} from 'src/types'
import {ComponentStatus} from 'src/clockface'
interface DispatchProps {
onUpdateType: typeof setType
onUpdateType: typeof setType | ((type: ViewType) => void)
}
interface StateProps {
view: View | NewView
viewType: ViewType
}
type Props = DispatchProps & StateProps
@ -75,22 +75,22 @@ class ViewTypeDropdown extends PureComponent<Props> {
}
private get dropdownStatus(): ComponentStatus {
const {view} = this.props
const {viewType} = this.props
if (view.properties.type === 'check') {
if (viewType === 'check') {
return ComponentStatus.Disabled
}
return ComponentStatus.Valid
}
private get selectedView(): ViewType {
const {view} = this.props
const {viewType} = this.props
if (view.properties.type === 'check') {
if (viewType === 'check') {
return 'xy'
}
return view.properties.type
return viewType
}
private getVewTypeGraphic = (viewType: ViewType): JSX.Element => {
@ -107,10 +107,12 @@ class ViewTypeDropdown extends PureComponent<Props> {
}
}
export {ViewTypeDropdown}
const mstp = (state: AppState): StateProps => {
const {view} = getActiveTimeMachine(state)
return {view}
return {viewType: view.properties.type}
}
const mdtp: DispatchProps = {