diff --git a/ui/src/notebooks/components/NotebookPipe.tsx b/ui/src/notebooks/components/NotebookPipe.tsx index 64039239a2..ff5eaefe9e 100644 --- a/ui/src/notebooks/components/NotebookPipe.tsx +++ b/ui/src/notebooks/components/NotebookPipe.tsx @@ -15,6 +15,7 @@ const NotebookPipe: FC = ({ data, onUpdate, results, + loading, }) => { const panel: FC = useMemo( () => props => { @@ -33,7 +34,13 @@ const NotebookPipe: FC = ({ } return ( - + ) } diff --git a/ui/src/notebooks/components/PipeList.tsx b/ui/src/notebooks/components/PipeList.tsx index e6cc7dff1e..4588deb5e6 100644 --- a/ui/src/notebooks/components/PipeList.tsx +++ b/ui/src/notebooks/components/PipeList.tsx @@ -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} /> ) }) diff --git a/ui/src/notebooks/components/header/Submit.tsx b/ui/src/notebooks/components/header/Submit.tsx index 0e8d7045e2..f27965bfd9 100644 --- a/ui/src/notebooks/components/header/Submit.tsx +++ b/ui/src/notebooks/components/header/Submit.tsx @@ -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) + }) }) - }) }) } diff --git a/ui/src/notebooks/components/panel/NotebookPanel.tsx b/ui/src/notebooks/components/panel/NotebookPanel.tsx index a8fd9a2357..d10fde8e02 100644 --- a/ui/src/notebooks/components/panel/NotebookPanel.tsx +++ b/ui/src/notebooks/components/panel/NotebookPanel.tsx @@ -92,7 +92,7 @@ const NotebookPanel: FC = ({index, children, controls}) => { useEffect(() => { updateMeta(index, {panelRef} as PipeMeta) - }) + }, []) const panelClassName = classnames('notebook-panel', { [`notebook-panel__visible`]: isVisible, diff --git a/ui/src/notebooks/context/notebook.tsx b/ui/src/notebooks/context/notebook.tsx index 09fc73ea37..efe893dedb 100644 --- a/ui/src/notebooks/context/notebook.tsx +++ b/ui/src/notebooks/context/notebook.tsx @@ -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( diff --git a/ui/src/notebooks/context/query.tsx b/ui/src/notebooks/context/query.tsx index 62d51dba7f..0ccaba77f8 100644 --- a/ui/src/notebooks/context/query.tsx +++ b/ui/src/notebooks/context/query.tsx @@ -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 = ({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 = ({children, variables, org}) => { return { raw: raw.csv, parsed: parse(raw.csv), + error: null, } }) } diff --git a/ui/src/notebooks/index.ts b/ui/src/notebooks/index.ts index 472548d1cd..099d56ca2e 100644 --- a/ui/src/notebooks/index.ts +++ b/ui/src/notebooks/index.ts @@ -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 diff --git a/ui/src/notebooks/pipes/Visualization/index.ts b/ui/src/notebooks/pipes/Visualization/index.ts new file mode 100644 index 0000000000..42b9b97225 --- /dev/null +++ b/ui/src/notebooks/pipes/Visualization/index.ts @@ -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', + }, + }, +}) diff --git a/ui/src/notebooks/pipes/Visualization/style.scss b/ui/src/notebooks/pipes/Visualization/style.scss new file mode 100644 index 0000000000..0220544410 --- /dev/null +++ b/ui/src/notebooks/pipes/Visualization/style.scss @@ -0,0 +1,10 @@ +.notebook-visualization { + min-height: 250px; + + .empty-graph-error { + height: 200px; + } + .time-machine-tables { + height: 200px; + } +} diff --git a/ui/src/notebooks/pipes/Visualization/view.tsx b/ui/src/notebooks/pipes/Visualization/view.tsx new file mode 100644 index 0000000000..29eac77e84 --- /dev/null +++ b/ui/src/notebooks/pipes/Visualization/view.tsx @@ -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 = ({ + 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 = ( + + ) + + return ( + +
+ + + +
+
+ ) +} + +export default Visualization diff --git a/ui/src/shared/components/EmptyQueryView.tsx b/ui/src/shared/components/EmptyQueryView.tsx index 9b22a93b5d..8ae63b02b6 100644 --- a/ui/src/shared/components/EmptyQueryView.tsx +++ b/ui/src/shared/components/EmptyQueryView.tsx @@ -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 { errorFormat, } = this.props - if (loading === RemoteDataState.NotStarted || !queries.length) { + if ( + loading === RemoteDataState.NotStarted || + (queries && !queries.length) + ) { return ( JSX.Element fluxGroupKeyUnion: string[] loading: RemoteDataState - timeRange: TimeRange | null + timeRange?: TimeRange | null table: Table timeZone: TimeZone viewProperties: XYViewProperties diff --git a/ui/src/timeMachine/components/view_options/ViewTypeDropdown.tsx b/ui/src/timeMachine/components/view_options/ViewTypeDropdown.tsx index 510613c31f..8d36582b2a 100644 --- a/ui/src/timeMachine/components/view_options/ViewTypeDropdown.tsx +++ b/ui/src/timeMachine/components/view_options/ViewTypeDropdown.tsx @@ -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 { } 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 { } } +export {ViewTypeDropdown} + const mstp = (state: AppState): StateProps => { const {view} = getActiveTimeMachine(state) - return {view} + return {viewType: view.properties.type} } const mdtp: DispatchProps = {