feat: keep notebook meta out of the pipes (#18163)
parent
d1275aaff7
commit
27fa70a4d7
|
@ -1,18 +1,16 @@
|
|||
import {FC, createElement, useContext} from 'react'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import {FC, createElement} from 'react'
|
||||
|
||||
import {PIPE_DEFINITIONS, PipeProp} from 'src/notebooks'
|
||||
|
||||
const Pipe: FC<PipeProp> = props => {
|
||||
const {index} = props
|
||||
const {pipes} = useContext(NotebookContext)
|
||||
const {data} = props
|
||||
|
||||
if (!PIPE_DEFINITIONS.hasOwnProperty(pipes[index].type)) {
|
||||
throw new Error(`Pipe type [${pipes[index].type}] not registered`)
|
||||
if (!PIPE_DEFINITIONS.hasOwnProperty(data.type)) {
|
||||
throw new Error(`Pipe type [${data.type}] not registered`)
|
||||
return null
|
||||
}
|
||||
|
||||
return createElement(PIPE_DEFINITIONS[pipes[index].type].component, props)
|
||||
return createElement(PIPE_DEFINITIONS[data.type].component, props)
|
||||
}
|
||||
|
||||
export default Pipe
|
||||
|
|
|
@ -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 {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import NotebookPanel from 'src/notebooks/components/panel/NotebookPanel'
|
||||
|
||||
const PipeList: FC = () => {
|
||||
const {id, pipes, removePipe, movePipe} = useContext(NotebookContext)
|
||||
const _pipes = pipes.map((_, index) => {
|
||||
const remove = () => removePipe(index)
|
||||
const moveUp = () => movePipe(index, index - 1)
|
||||
const moveDown = () => movePipe(index, index + 1)
|
||||
const {id, pipes, updatePipe} = useContext(NotebookContext)
|
||||
const _pipes = pipes.map((pipe, index) => {
|
||||
const panel: FC<PipeContextProps> = props => {
|
||||
const _props = {
|
||||
...props,
|
||||
index,
|
||||
}
|
||||
|
||||
return createElement(NotebookPanel, _props)
|
||||
}
|
||||
const onUpdate = (data: PipeData) => {
|
||||
updatePipe(index, data)
|
||||
}
|
||||
|
||||
return (
|
||||
<Pipe
|
||||
index={index}
|
||||
remove={remove}
|
||||
moveUp={moveUp}
|
||||
moveDown={moveDown}
|
||||
key={`pipe-${id}-${index}`}
|
||||
data={pipe}
|
||||
onUpdate={onUpdate}
|
||||
Context={panel}
|
||||
/>
|
||||
)
|
||||
})
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
// Libraries
|
||||
import React, {FC, ReactChildren, useState} from 'react'
|
||||
import React, {FC, useContext} from 'react'
|
||||
import classnames from 'classnames'
|
||||
|
||||
// Components
|
||||
|
@ -9,43 +9,32 @@ import {
|
|||
AlignItems,
|
||||
JustifyContent,
|
||||
} from '@influxdata/clockface'
|
||||
import {PipeContextProps} from 'src/notebooks'
|
||||
import {NotebookContext} from 'src/notebooks/context/notebook'
|
||||
import RemovePanelButton from 'src/notebooks/components/panel/RemovePanelButton'
|
||||
import PanelVisibilityToggle from 'src/notebooks/components/panel/PanelVisibilityToggle'
|
||||
import MovePanelButton from 'src/notebooks/components/panel/MovePanelButton'
|
||||
import NotebookPanelTitle from 'src/notebooks/components/panel/NotebookPanelTitle'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
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 interface Props extends PipeContextProps {
|
||||
index: number
|
||||
}
|
||||
|
||||
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> = ({
|
||||
children,
|
||||
title,
|
||||
previousPanelTitle,
|
||||
onTitleChange,
|
||||
controlsLeft,
|
||||
controlsRight,
|
||||
onRemove,
|
||||
onMoveUp,
|
||||
onMoveDown,
|
||||
}) => {
|
||||
const [panelVisibility, setPanelVisibility] = useState<
|
||||
NotebookPanelVisibility
|
||||
>('visible')
|
||||
const moveUp = canBeMovedUp ? () => movePipe(index, index - 1) : null
|
||||
const moveDown = canBeMovedDown ? () => movePipe(index, index + 1) : null
|
||||
const remove = canBeRemoved ? () => removePipe(index) : null
|
||||
|
||||
const isVisible = meta[index].visible
|
||||
|
||||
const panelClassName = classnames('notebook-panel', {
|
||||
[`notebook-panel__${panelVisibility}`]: panelVisibility,
|
||||
[`notebook-panel__visible`]: isVisible,
|
||||
[`notebook-panel__hidden`]: !isVisible,
|
||||
})
|
||||
|
||||
return (
|
||||
|
@ -57,12 +46,7 @@ const NotebookPanel: FC<Props> = ({
|
|||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexStart}
|
||||
>
|
||||
<NotebookPanelTitle
|
||||
title={title}
|
||||
onTitleChange={onTitleChange}
|
||||
previousPanelTitle={previousPanelTitle}
|
||||
/>
|
||||
{controlsLeft}
|
||||
<NotebookPanelTitle index={index} />
|
||||
</FlexBox>
|
||||
<FlexBox
|
||||
className="notebook-panel--header-right"
|
||||
|
@ -70,19 +54,13 @@ const NotebookPanel: FC<Props> = ({
|
|||
margin={ComponentSize.Small}
|
||||
justifyContent={JustifyContent.FlexEnd}
|
||||
>
|
||||
{controlsRight}
|
||||
<MovePanelButton direction="up" onClick={onMoveUp} />
|
||||
<MovePanelButton direction="down" onClick={onMoveDown} />
|
||||
<PanelVisibilityToggle
|
||||
onToggle={setPanelVisibility}
|
||||
visibility={panelVisibility}
|
||||
/>
|
||||
<RemovePanelButton onRemove={onRemove} />
|
||||
<MovePanelButton direction="up" onClick={moveUp} />
|
||||
<MovePanelButton direction="down" onClick={moveDown} />
|
||||
<PanelVisibilityToggle index={index} />
|
||||
<RemovePanelButton onRemove={remove} />
|
||||
</FlexBox>
|
||||
</div>
|
||||
<div className="notebook-panel--body">
|
||||
{panelVisibility === 'visible' && children}
|
||||
</div>
|
||||
<div className="notebook-panel--body">{isVisible && children}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,55 +1,41 @@
|
|||
// Libraries
|
||||
import React, {FC, ChangeEvent} from 'react'
|
||||
|
||||
// Components
|
||||
import {Icon, IconFont} from '@influxdata/clockface'
|
||||
import React, {FC, ChangeEvent, useContext} from 'react'
|
||||
import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
onTitleChange?: (title: string) => void
|
||||
previousPanelTitle?: string
|
||||
index: number
|
||||
}
|
||||
|
||||
const NotebookPanelTitle: FC<Props> = ({
|
||||
title,
|
||||
onTitleChange,
|
||||
previousPanelTitle,
|
||||
}) => {
|
||||
const NotebookPanelTitle: FC<Props> = ({index}) => {
|
||||
const {meta, updateMeta} = useContext(NotebookContext)
|
||||
const title = meta[index].title
|
||||
const onTitleChange = (value: string) => {
|
||||
updateMeta(index, {
|
||||
title: value,
|
||||
} as PipeMeta)
|
||||
}
|
||||
|
||||
let sourceName
|
||||
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>
|
||||
)
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const trimmedValue = e.target.value.replace(' ', '_')
|
||||
onTitleChange(trimmedValue)
|
||||
}
|
||||
|
||||
if (onTitleChange) {
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>): void => {
|
||||
const trimmedValue = e.target.value.replace(' ', '_')
|
||||
onTitleChange(trimmedValue)
|
||||
}
|
||||
|
||||
titleElement = (
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={onChange}
|
||||
placeholder="Enter an ID"
|
||||
className="notebook-panel--editable-title"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
maxLength={30}
|
||||
/>
|
||||
)
|
||||
}
|
||||
titleElement = (
|
||||
<input
|
||||
type="text"
|
||||
value={title}
|
||||
onChange={onChange}
|
||||
placeholder="Enter an ID"
|
||||
className="notebook-panel--editable-title"
|
||||
autoComplete="off"
|
||||
autoCorrect="off"
|
||||
spellCheck={false}
|
||||
maxLength={30}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
@ -1,26 +1,23 @@
|
|||
// Libraries
|
||||
import React, {FC} from 'react'
|
||||
import React, {FC, useContext} from 'react'
|
||||
|
||||
// Components
|
||||
import {SquareButton, IconFont} from '@influxdata/clockface'
|
||||
import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook'
|
||||
|
||||
// Types
|
||||
import {NotebookPanelVisibility} from 'src/notebooks/components/panel/NotebookPanel'
|
||||
|
||||
interface Props {
|
||||
visibility: NotebookPanelVisibility
|
||||
onToggle: (visibility: NotebookPanelVisibility) => void
|
||||
export interface Props {
|
||||
index: number
|
||||
}
|
||||
|
||||
const PanelVisibilityToggle: FC<Props> = ({visibility, onToggle}) => {
|
||||
const icon = visibility === 'visible' ? IconFont.EyeOpen : IconFont.EyeClosed
|
||||
const PanelVisibilityToggle: FC<Props> = ({index}) => {
|
||||
const {meta, updateMeta} = useContext(NotebookContext)
|
||||
|
||||
const icon = meta[index].visible ? IconFont.EyeOpen : IconFont.EyeClosed
|
||||
|
||||
const handleClick = (): void => {
|
||||
if (visibility === 'hidden') {
|
||||
onToggle('visible')
|
||||
} else if (visibility === 'visible') {
|
||||
onToggle('hidden')
|
||||
}
|
||||
updateMeta(index, {
|
||||
visible: !meta[index].visible,
|
||||
} as PipeMeta)
|
||||
}
|
||||
|
||||
return <SquareButton icon={icon} onClick={handleClick} />
|
||||
|
|
|
@ -1,14 +1,18 @@
|
|||
import React, {FC, useState} from 'react'
|
||||
import {PipeData} from 'src/notebooks'
|
||||
|
||||
// TODO make this polymorphic to mimic the self registration
|
||||
// of pipe stages
|
||||
export type Pipe = any
|
||||
export interface PipeMeta {
|
||||
title: string
|
||||
visible: boolean
|
||||
}
|
||||
|
||||
export interface NotebookContextType {
|
||||
id: string
|
||||
pipes: Pipe[]
|
||||
addPipe: (pipe: Pipe) => void
|
||||
updatePipe: (idx: number, pipe: Pipe) => void
|
||||
pipes: PipeData[]
|
||||
meta: PipeMeta[] // data only used for the view layer for Notebooks
|
||||
addPipe: (pipe: PipeData) => void
|
||||
updatePipe: (idx: number, pipe: PipeData) => void
|
||||
updateMeta: (idx: number, pipe: PipeMeta) => void
|
||||
movePipe: (currentIdx: number, newIdx: number) => void
|
||||
removePipe: (idx: number) => void
|
||||
}
|
||||
|
@ -16,8 +20,10 @@ export interface NotebookContextType {
|
|||
export const DEFAULT_CONTEXT: NotebookContextType = {
|
||||
id: 'new',
|
||||
pipes: [],
|
||||
meta: [],
|
||||
addPipe: () => {},
|
||||
updatePipe: () => {},
|
||||
updateMeta: () => {},
|
||||
movePipe: () => {},
|
||||
removePipe: () => {},
|
||||
}
|
||||
|
@ -36,18 +42,30 @@ export const NotebookContext = React.createContext<NotebookContextType>(
|
|||
DEFAULT_CONTEXT
|
||||
)
|
||||
|
||||
let GENERATOR_INDEX = 0
|
||||
|
||||
export const NotebookProvider: FC = ({children}) => {
|
||||
const [id] = useState(DEFAULT_CONTEXT.id)
|
||||
const [pipes, setPipes] = useState(DEFAULT_CONTEXT.pipes)
|
||||
const [meta, setMeta] = useState(DEFAULT_CONTEXT.meta)
|
||||
|
||||
function addPipe(pipe: Pipe) {
|
||||
setPipes(pipes => {
|
||||
pipes.push(pipe)
|
||||
return pipes.slice()
|
||||
})
|
||||
function addPipe(pipe: PipeData) {
|
||||
const add = data => {
|
||||
return pipes => {
|
||||
pipes.push(data)
|
||||
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 => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
|
@ -57,27 +75,41 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
})
|
||||
}
|
||||
|
||||
function movePipe(currentIdx: number, newIdx: number) {
|
||||
setPipes(pipes => {
|
||||
const idx = ((newIdx % pipes.length) + pipes.length) % pipes.length
|
||||
|
||||
if (idx === currentIdx) {
|
||||
return pipes
|
||||
function updateMeta(idx: number, pipe: PipeMeta) {
|
||||
setMeta(pipes => {
|
||||
pipes[idx] = {
|
||||
...pipes[idx],
|
||||
...pipe,
|
||||
}
|
||||
|
||||
const pipe = pipes.splice(currentIdx, 1)
|
||||
|
||||
pipes.splice(idx, 0, pipe[0])
|
||||
|
||||
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) {
|
||||
setPipes(pipes => {
|
||||
const remove = pipes => {
|
||||
pipes.splice(idx, 1)
|
||||
return pipes.slice()
|
||||
})
|
||||
}
|
||||
setPipes(remove)
|
||||
setMeta(remove)
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -85,7 +117,9 @@ export const NotebookProvider: FC = ({children}) => {
|
|||
value={{
|
||||
id,
|
||||
pipes,
|
||||
meta,
|
||||
updatePipe,
|
||||
updateMeta,
|
||||
movePipe,
|
||||
addPipe,
|
||||
removePipe,
|
||||
|
|
|
@ -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 {
|
||||
index: number
|
||||
remove: () => void
|
||||
moveUp: () => void
|
||||
moveDown: () => void
|
||||
data: PipeData
|
||||
onUpdate: (data: PipeData) => void
|
||||
Context:
|
||||
| FunctionComponent<PipeContextProps>
|
||||
| ComponentClass<PipeContextProps>
|
||||
}
|
||||
|
||||
// NOTE: keep this interface as small as possible and
|
||||
|
|
|
@ -1,5 +1,18 @@
|
|||
.notebook-example {
|
||||
.pipe-example {
|
||||
.notebook-panel--header {
|
||||
transition: opacity 0.2s;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.notebook-panel--header {
|
||||
opacity: 1;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #f00;
|
||||
margin: 0;
|
||||
padding: 1em 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,11 @@
|
|||
import React, {FC, useContext} from 'react'
|
||||
import React, {FC} from 'react'
|
||||
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 (
|
||||
<NotebookPanel
|
||||
id={`pipe${index}`}
|
||||
onMoveUp={canBeMovedUp && moveUp}
|
||||
onMoveDown={canBeMovedDown && moveDown}
|
||||
onRemove={canBeRemoved && remove}
|
||||
title={TITLE}
|
||||
>
|
||||
<h1>{pipe.text}</h1>
|
||||
</NotebookPanel>
|
||||
<Context>
|
||||
<h1>{data.text}</h1>
|
||||
</Context>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue