feat: add visualizations to notebooks (#18348)
parent
0caee29f24
commit
1ccac90a8c
|
@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
})
|
|
@ -0,0 +1,10 @@
|
|||
.notebook-visualization {
|
||||
min-height: 250px;
|
||||
|
||||
.empty-graph-error {
|
||||
height: 200px;
|
||||
}
|
||||
.time-machine-tables {
|
||||
height: 200px;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue