feat: keep notebook meta out of the pipes (#18163)

pull/18186/head^2
Alex Boatwright 2020-05-20 12:26:23 -07:00 committed by GitHub
parent d1275aaff7
commit 27fa70a4d7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 177 additions and 173 deletions

View File

@ -1,18 +1,16 @@
import {FC, createElement, useContext} from 'react' import {FC, createElement} from 'react'
import {NotebookContext} from 'src/notebooks/context/notebook'
import {PIPE_DEFINITIONS, PipeProp} from 'src/notebooks' import {PIPE_DEFINITIONS, PipeProp} from 'src/notebooks'
const Pipe: FC<PipeProp> = props => { const Pipe: FC<PipeProp> = props => {
const {index} = props const {data} = props
const {pipes} = useContext(NotebookContext)
if (!PIPE_DEFINITIONS.hasOwnProperty(pipes[index].type)) { if (!PIPE_DEFINITIONS.hasOwnProperty(data.type)) {
throw new Error(`Pipe type [${pipes[index].type}] not registered`) throw new Error(`Pipe type [${data.type}] not registered`)
return null return null
} }
return createElement(PIPE_DEFINITIONS[pipes[index].type].component, props) return createElement(PIPE_DEFINITIONS[data.type].component, props)
} }
export default Pipe export default Pipe

View File

@ -1,21 +1,30 @@
import React, {FC, useContext} from 'react' import React, {FC, useContext, createElement} from 'react'
import {PipeContextProps, PipeData} from 'src/notebooks'
import Pipe from 'src/notebooks/components/Pipe' import Pipe from 'src/notebooks/components/Pipe'
import {NotebookContext} from 'src/notebooks/context/notebook' import {NotebookContext} from 'src/notebooks/context/notebook'
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
const PipeList: FC = () => { const PipeList: FC = () => {
const {id, pipes, removePipe, movePipe} = useContext(NotebookContext) const {id, pipes, updatePipe} = useContext(NotebookContext)
const _pipes = pipes.map((_, index) => { const _pipes = pipes.map((pipe, index) => {
const remove = () => removePipe(index) const panel: FC<PipeContextProps> = props => {
const moveUp = () => movePipe(index, index - 1) const _props = {
const moveDown = () => movePipe(index, index + 1) ...props,
index,
}
return createElement(NotebookPanel, _props)
}
const onUpdate = (data: PipeData) => {
updatePipe(index, data)
}
return ( return (
<Pipe <Pipe
index={index}
remove={remove}
moveUp={moveUp}
moveDown={moveDown}
key={`pipe-${id}-${index}`} key={`pipe-${id}-${index}`}
data={pipe}
onUpdate={onUpdate}
Context={panel}
/> />
) )
}) })

View File

@ -1,5 +1,5 @@
// Libraries // Libraries
import React, {FC, ReactChildren, useState} from 'react' import React, {FC, useContext} from 'react'
import classnames from 'classnames' import classnames from 'classnames'
// Components // Components
@ -9,43 +9,32 @@ import {
AlignItems, AlignItems,
JustifyContent, JustifyContent,
} from '@influxdata/clockface' } from '@influxdata/clockface'
import {PipeContextProps} from 'src/notebooks'
import {NotebookContext} from 'src/notebooks/context/notebook'
import RemovePanelButton from 'src/notebooks/components/panel/RemovePanelButton' import RemovePanelButton from 'src/notebooks/components/panel/RemovePanelButton'
import PanelVisibilityToggle from 'src/notebooks/components/panel/PanelVisibilityToggle' import PanelVisibilityToggle from 'src/notebooks/components/panel/PanelVisibilityToggle'
import MovePanelButton from 'src/notebooks/components/panel/MovePanelButton' import MovePanelButton from 'src/notebooks/components/panel/MovePanelButton'
import NotebookPanelTitle from 'src/notebooks/components/panel/NotebookPanelTitle' import NotebookPanelTitle from 'src/notebooks/components/panel/NotebookPanelTitle'
interface Props { export interface Props extends PipeContextProps {
id: string index: number
children: ReactChildren | JSX.Element | JSX.Element[]
title: string
onTitleChange?: (title: string) => void
previousPanelTitle?: string
controlsLeft?: JSX.Element | JSX.Element[]
controlsRight?: JSX.Element | JSX.Element[]
onRemove?: () => void
onMoveUp?: () => void
onMoveDown?: () => void
} }
export type NotebookPanelVisibility = 'hidden' | 'visible' const NotebookPanel: FC<Props> = ({index, children}) => {
const {pipes, removePipe, movePipe, meta} = useContext(NotebookContext)
const canBeMovedUp = index > 0
const canBeMovedDown = index < pipes.length - 1
const canBeRemoved = index !== 0
const NotebookPanel: FC<Props> = ({ const moveUp = canBeMovedUp ? () => movePipe(index, index - 1) : null
children, const moveDown = canBeMovedDown ? () => movePipe(index, index + 1) : null
title, const remove = canBeRemoved ? () => removePipe(index) : null
previousPanelTitle,
onTitleChange, const isVisible = meta[index].visible
controlsLeft,
controlsRight,
onRemove,
onMoveUp,
onMoveDown,
}) => {
const [panelVisibility, setPanelVisibility] = useState<
NotebookPanelVisibility
>('visible')
const panelClassName = classnames('notebook-panel', { const panelClassName = classnames('notebook-panel', {
[`notebook-panel__${panelVisibility}`]: panelVisibility, [`notebook-panel__visible`]: isVisible,
[`notebook-panel__hidden`]: !isVisible,
}) })
return ( return (
@ -57,12 +46,7 @@ const NotebookPanel: FC<Props> = ({
margin={ComponentSize.Small} margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexStart} justifyContent={JustifyContent.FlexStart}
> >
<NotebookPanelTitle <NotebookPanelTitle index={index} />
title={title}
onTitleChange={onTitleChange}
previousPanelTitle={previousPanelTitle}
/>
{controlsLeft}
</FlexBox> </FlexBox>
<FlexBox <FlexBox
className="notebook-panel--header-right" className="notebook-panel--header-right"
@ -70,19 +54,13 @@ const NotebookPanel: FC<Props> = ({
margin={ComponentSize.Small} margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexEnd} justifyContent={JustifyContent.FlexEnd}
> >
{controlsRight} <MovePanelButton direction="up" onClick={moveUp} />
<MovePanelButton direction="up" onClick={onMoveUp} /> <MovePanelButton direction="down" onClick={moveDown} />
<MovePanelButton direction="down" onClick={onMoveDown} /> <PanelVisibilityToggle index={index} />
<PanelVisibilityToggle <RemovePanelButton onRemove={remove} />
onToggle={setPanelVisibility}
visibility={panelVisibility}
/>
<RemovePanelButton onRemove={onRemove} />
</FlexBox> </FlexBox>
</div> </div>
<div className="notebook-panel--body"> <div className="notebook-panel--body">{isVisible && children}</div>
{panelVisibility === 'visible' && children}
</div>
</div> </div>
) )
} }

View File

@ -1,36 +1,23 @@
// Libraries // Libraries
import React, {FC, ChangeEvent} from 'react' import React, {FC, ChangeEvent, useContext} from 'react'
import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook'
// Components
import {Icon, IconFont} from '@influxdata/clockface'
interface Props { interface Props {
title: string index: number
onTitleChange?: (title: string) => void
previousPanelTitle?: string
} }
const NotebookPanelTitle: FC<Props> = ({ const NotebookPanelTitle: FC<Props> = ({index}) => {
title, const {meta, updateMeta} = useContext(NotebookContext)
onTitleChange, const title = meta[index].title
previousPanelTitle, const onTitleChange = (value: string) => {
}) => { updateMeta(index, {
title: value,
} as PipeMeta)
}
let sourceName let sourceName
let titleElement = <div className="notebook-panel--title">{title}</div> let titleElement = <div className="notebook-panel--title">{title}</div>
if (previousPanelTitle) {
sourceName = (
<div className="notebook-panel--data-source">
{previousPanelTitle}
<Icon
glyph={IconFont.CaretRight}
className="notebook-panel--data-caret"
/>
</div>
)
}
if (onTitleChange) {
const onChange = (e: ChangeEvent<HTMLInputElement>): void => { const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
const trimmedValue = e.target.value.replace(' ', '_') const trimmedValue = e.target.value.replace(' ', '_')
onTitleChange(trimmedValue) onTitleChange(trimmedValue)
@ -49,7 +36,6 @@ const NotebookPanelTitle: FC<Props> = ({
maxLength={30} maxLength={30}
/> />
) )
}
return ( return (
<> <>

View File

@ -1,26 +1,23 @@
// Libraries // Libraries
import React, {FC} from 'react' import React, {FC, useContext} from 'react'
// Components // Components
import {SquareButton, IconFont} from '@influxdata/clockface' import {SquareButton, IconFont} from '@influxdata/clockface'
import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook'
// Types export interface Props {
import {NotebookPanelVisibility} from 'src/notebooks/components/panel/NotebookPanel' index: number
interface Props {
visibility: NotebookPanelVisibility
onToggle: (visibility: NotebookPanelVisibility) => void
} }
const PanelVisibilityToggle: FC<Props> = ({visibility, onToggle}) => { const PanelVisibilityToggle: FC<Props> = ({index}) => {
const icon = visibility === 'visible' ? IconFont.EyeOpen : IconFont.EyeClosed const {meta, updateMeta} = useContext(NotebookContext)
const icon = meta[index].visible ? IconFont.EyeOpen : IconFont.EyeClosed
const handleClick = (): void => { const handleClick = (): void => {
if (visibility === 'hidden') { updateMeta(index, {
onToggle('visible') visible: !meta[index].visible,
} else if (visibility === 'visible') { } as PipeMeta)
onToggle('hidden')
}
} }
return <SquareButton icon={icon} onClick={handleClick} /> return <SquareButton icon={icon} onClick={handleClick} />

View File

@ -1,14 +1,18 @@
import React, {FC, useState} from 'react' import React, {FC, useState} from 'react'
import {PipeData} from 'src/notebooks'
// TODO make this polymorphic to mimic the self registration export interface PipeMeta {
// of pipe stages title: string
export type Pipe = any visible: boolean
}
export interface NotebookContextType { export interface NotebookContextType {
id: string id: string
pipes: Pipe[] pipes: PipeData[]
addPipe: (pipe: Pipe) => void meta: PipeMeta[] // data only used for the view layer for Notebooks
updatePipe: (idx: number, pipe: Pipe) => void addPipe: (pipe: PipeData) => void
updatePipe: (idx: number, pipe: PipeData) => void
updateMeta: (idx: number, pipe: PipeMeta) => void
movePipe: (currentIdx: number, newIdx: number) => void movePipe: (currentIdx: number, newIdx: number) => void
removePipe: (idx: number) => void removePipe: (idx: number) => void
} }
@ -16,8 +20,10 @@ export interface NotebookContextType {
export const DEFAULT_CONTEXT: NotebookContextType = { export const DEFAULT_CONTEXT: NotebookContextType = {
id: 'new', id: 'new',
pipes: [], pipes: [],
meta: [],
addPipe: () => {}, addPipe: () => {},
updatePipe: () => {}, updatePipe: () => {},
updateMeta: () => {},
movePipe: () => {}, movePipe: () => {},
removePipe: () => {}, removePipe: () => {},
} }
@ -36,18 +42,30 @@ export const NotebookContext = React.createContext<NotebookContextType>(
DEFAULT_CONTEXT DEFAULT_CONTEXT
) )
let GENERATOR_INDEX = 0
export const NotebookProvider: FC = ({children}) => { export const NotebookProvider: FC = ({children}) => {
const [id] = useState(DEFAULT_CONTEXT.id) const [id] = useState(DEFAULT_CONTEXT.id)
const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes) const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes)
const [meta, setMeta] = useState(DEFAULT_CONTEXT.meta)
function addPipe(pipe: Pipe) { function addPipe(pipe: PipeData) {
setPipes(pipes => { const add = data => {
pipes.push(pipe) return pipes => {
pipes.push(data)
return pipes.slice() return pipes.slice()
}
}
setPipes(add(pipe))
setMeta(
add({
title: `Notebook_${++GENERATOR_INDEX}`,
visible: true,
}) })
)
} }
function updatePipe(idx: number, pipe: Pipe) { function updatePipe(idx: number, pipe: PipeData) {
setPipes(pipes => { setPipes(pipes => {
pipes[idx] = { pipes[idx] = {
...pipes[idx], ...pipes[idx],
@ -57,27 +75,41 @@ export const NotebookProvider: FC = ({children}) => {
}) })
} }
function movePipe(currentIdx: number, newIdx: number) { function updateMeta(idx: number, pipe: PipeMeta) {
setPipes(pipes => { setMeta(pipes => {
const idx = ((newIdx % pipes.length) + pipes.length) % pipes.length pipes[idx] = {
...pipes[idx],
if (idx === currentIdx) { ...pipe,
return pipes
} }
const pipe = pipes.splice(currentIdx, 1)
pipes.splice(idx, 0, pipe[0])
return pipes.slice() 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) { function removePipe(idx: number) {
setPipes(pipes => { const remove = pipes => {
pipes.splice(idx, 1) pipes.splice(idx, 1)
return pipes.slice() return pipes.slice()
}) }
setPipes(remove)
setMeta(remove)
} }
return ( return (
@ -85,7 +117,9 @@ export const NotebookProvider: FC = ({children}) => {
value={{ value={{
id, id,
pipes, pipes,
meta,
updatePipe, updatePipe,
updateMeta,
movePipe, movePipe,
addPipe, addPipe,
removePipe, removePipe,

View File

@ -1,10 +1,17 @@
import {FunctionComponent, ComponentClass} from 'react' import {FunctionComponent, ComponentClass, ReactNode} from 'react'
export interface PipeContextProps {
children?: ReactNode
}
export type PipeData = any
export interface PipeProp { export interface PipeProp {
index: number data: PipeData
remove: () => void onUpdate: (data: PipeData) => void
moveUp: () => void Context:
moveDown: () => void | FunctionComponent<PipeContextProps>
| ComponentClass<PipeContextProps>
} }
// NOTE: keep this interface as small as possible and // NOTE: keep this interface as small as possible and

View File

@ -1,5 +1,18 @@
.notebook-example { .pipe-example {
.notebook-panel--header {
transition: opacity 0.2s;
opacity: 0;
}
&:hover {
.notebook-panel--header {
opacity: 1;
}
}
h1 { h1 {
color: #f00; margin: 0;
padding: 1em 0;
} }
} }

View File

@ -1,29 +1,11 @@
import React, {FC, useContext} from 'react' import React, {FC} from 'react'
import {PipeProp} from 'src/notebooks' import {PipeProp} from 'src/notebooks'
import {NotebookContext} from 'src/notebooks/context/notebook'
import {NotebookPanel} from 'src/notebooks/components/Notebook'
const TITLE = 'Example Pipe'
const ExampleView: FC<PipeProp> = ({index, remove, moveUp, moveDown}) => {
const {pipes} = useContext(NotebookContext)
const pipe = pipes[index]
const canBeMovedUp = index > 0
const canBeMovedDown = index < pipes.length - 1
const canBeRemoved = index !== 0
const ExampleView: FC<PipeProp> = ({data, Context}) => {
return ( return (
<NotebookPanel <Context>
id={`pipe${index}`} <h1>{data.text}</h1>
onMoveUp={canBeMovedUp && moveUp} </Context>
onMoveDown={canBeMovedDown && moveDown}
onRemove={canBeRemoved && remove}
title={TITLE}
>
<h1>{pipe.text}</h1>
</NotebookPanel>
) )
} }