feat: adding basic structure for notebooks (#18108)

pull/18130/head
Alex Boatwright 2020-05-15 17:18:25 -07:00 committed by GitHub
parent 1199136bbd
commit 89fe6834b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 436 additions and 3 deletions

View File

@ -79,6 +79,7 @@
"@types/text-encoding": "^0.0.32",
"@types/uuid": "^3.4.3",
"@types/webpack": "^4.4.35",
"@types/webpack-env": "^1.15.2",
"@typescript-eslint/eslint-plugin": "^2.6.0",
"@typescript-eslint/parser": "^2.6.0",
"acorn": "^6.0.6",

View File

@ -0,0 +1,28 @@
import React, {FC, useContext} from 'react'
import {Button, ComponentColor} from '@influxdata/clockface'
import {NotebookContext} from 'src/notebooks/context/notebook'
import {PIPE_DEFINITIONS} from 'src/notebooks'
const AddButtons: FC = () => {
const {addPipe} = useContext(NotebookContext)
const pipes = Object.entries(PIPE_DEFINITIONS).map(([type, def]) => {
return (
<Button
key={def.type}
text={def.button}
onClick={() => {
addPipe({
...def.empty,
type,
})
}}
color={ComponentColor.Secondary}
/>
)
})
return <div className="notebook--actions">{pipes}</div>
}
export default AddButtons

View File

@ -0,0 +1,13 @@
import React, {FC} from 'react'
import {Page} from '@influxdata/clockface'
const Header: FC = () => {
return (
<>
<Page.Title title="Notebooks" />
</>
)
}
export default Header

View File

@ -2,13 +2,26 @@ import React, {FC} from 'react'
import {Page} from '@influxdata/clockface'
import {NotebookProvider} from 'src/notebooks/context/notebook'
import Header from 'src/notebooks/components/Header'
import PipeList from 'src/notebooks/components/PipeList'
import AddButtons from 'src/notebooks/components/AddButtons'
// NOTE: uncommon, but using this to scope the project
// within the page and not bleed it's dependancies outside
// of the feature flag
import 'src/notebooks/style.scss'
const NotebookPage: FC = () => {
return (
<NotebookProvider>
<Page titleTag="Notebook">
<Page.Header fullWidth={false} />
<Page.Contents fullWidth={false} scrollable={true} />
<Page.Header fullWidth={false}>
<Header />
</Page.Header>
<Page.Contents fullWidth={false} scrollable={true}>
<PipeList />
<AddButtons />
</Page.Contents>
</Page>
</NotebookProvider>
)

View File

@ -0,0 +1,62 @@
// Libraries
import React, {FC, ReactChildren} from 'react'
// Components
import {
FlexBox,
ComponentSize,
AlignItems,
JustifyContent,
} from '@influxdata/clockface'
import usePanelState from 'src/notebooks/hooks/usePanelState'
import RemoveButton from 'src/notebooks/components/RemoveButton'
interface Props {
children: ReactChildren | JSX.Element | JSX.Element[]
title: string
controlsLeft?: JSX.Element | JSX.Element[]
controlsRight?: JSX.Element | JSX.Element[]
onRemove?: () => void
}
const Panel: FC<Props> = ({
children,
title,
controlsLeft,
controlsRight,
onRemove,
}) => {
const [className, showChildren, toggle] = usePanelState()
return (
<div className={className}>
<div className="notebook-panel--header">
<FlexBox
className="notebook-panel--header-left"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexStart}
>
<div className="notebook-panel--toggle" onClick={toggle} />
<div className="notebook-panel--title">{title}</div>
{showChildren && controlsLeft}
</FlexBox>
<FlexBox
className="notebook-panel--header-right"
alignItems={AlignItems.Center}
margin={ComponentSize.Small}
justifyContent={JustifyContent.FlexEnd}
>
{showChildren && controlsRight}
<RemoveButton onRemove={onRemove} />
</FlexBox>
</div>
<div className="notebook-panel--body">{showChildren && children}</div>
</div>
)
}
export {Panel}
export default Panel

View File

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

View File

@ -0,0 +1,14 @@
import React, {FC, useContext} from 'react'
import Pipe from 'src/notebooks/components/Pipe'
import {NotebookContext} from 'src/notebooks/context/notebook'
const PipeList: FC = () => {
const {id, pipes} = useContext(NotebookContext)
const _pipes = pipes.map((_, index) => (
<Pipe index={index} key={`pipe-${id}-${index}`} />
))
return <>{_pipes}</>
}
export default PipeList

View File

@ -0,0 +1,19 @@
// Libraries
import React, {FC} from 'react'
// Components
import {SquareButton, IconFont} from '@influxdata/clockface'
interface Props {
onRemove?: () => void
}
const RemoveButton: FC<Props> = ({onRemove}) => {
if (!onRemove) {
return null
}
return <SquareButton icon={IconFont.Remove} onClick={onRemove} />
}
export default RemoveButton

View File

@ -12,7 +12,7 @@ export interface NotebookContextType {
removePipe: (idx: number) => void
}
export const DEFAULT_CONTEXT = {
export const DEFAULT_CONTEXT: NotebookContextType = {
id: 'new',
pipes: [],
addPipe: () => {},

View File

@ -0,0 +1,31 @@
import {useState} from 'react'
import classnames from 'classnames'
type PanelState = 'hidden' | 'small' | 'large'
interface ProgressMap {
[key: PanelState]: PanelState
}
const PANEL_PROGRESS: ProgressMap = {
hidden: 'small',
small: 'large',
large: 'hidden',
}
function usePanelState() {
const [panelState, setPanelState] = useState<PanelState>('small')
const className = classnames('notebook-panel', {
[`notebook-panel__${panelState}`]: panelState,
})
const showChildren = panelState === 'small' || panelState === 'large'
const toggle = (): void => {
setPanelState(PANEL_PROGRESS[panelState])
}
return [className, showChildren, toggle]
}
export default usePanelState

41
ui/src/notebooks/index.ts Normal file
View File

@ -0,0 +1,41 @@
import {FunctionComponent, ComponentClass} from 'react'
export interface PipeProp {
index: number
}
// NOTE: keep this interface as small as possible and
// don't take extending it lightly. this should only
// define what ALL pipe types require to be included
// on the page.
export interface TypeRegistration {
type: string // a unique string that identifies a pipe
component: FunctionComponent<PipeProp> | ComponentClass<PipeProp> // the view component for rendering the interface
button: string // a human readable string for appending the type
empty: any // the default state for an add
}
export interface TypeLookup {
[key: string]: TypeRegistration
}
export const PIPE_DEFINITIONS: TypeLookup = {}
export function register(definition: TypeRegistration) {
if (PIPE_DEFINITIONS.hasOwnProperty(definition.type)) {
throw new Error(
`Pipe of type [${definition.type}] has already been registered`
)
}
PIPE_DEFINITIONS[definition.type] = {
...definition,
}
}
// NOTE: this loads in all the modules under the current directory
// to make it easier to add new types
const context = require.context('./pipes', true, /index\.(ts|tsx)$/)
context.keys().forEach(key => {
context(key)
})

View File

@ -0,0 +1,12 @@
import {register} from 'src/notebooks'
import ExampleView from './view'
import './style.scss'
register({
type: 'example',
component: ExampleView,
button: 'Example Adding',
empty: {
text: 'Example Text',
},
})

View File

@ -0,0 +1,5 @@
.notebook-example {
h1 {
color: #f00;
}
}

View File

@ -0,0 +1,28 @@
import React, {FC, useContext} from 'react'
import {PipeProp} from 'src/notebooks'
import {NotebookContext} from 'src/notebooks/context/notebook'
import Panel from 'src/notebooks/components/Panel'
const TITLE = 'Example Pipe'
const ExampleView: FC<PipeProp> = ({index}) => {
const {pipes, removePipe} = useContext(NotebookContext)
const pipe = pipes[index]
if (index) {
return (
<Panel onRemove={() => removePipe(index)} title={TITLE}>
<h1>{pipe.text}</h1>
</Panel>
)
}
return (
<Panel title={TITLE}>
<h1>{pipe.text}</h1>
</Panel>
)
}
export default ExampleView

149
ui/src/notebooks/style.scss Normal file
View File

@ -0,0 +1,149 @@
@import "@influxdata/clockface/dist/variables.scss";
$notebook-header-height: 46px;
$notebook-size-toggle: 16px;
.notebook-panel {
background-color: $g2-kevlar;
border-radius: $cf-radius;
display: flex;
flex-direction: column;
align-items: stretch;
margin-bottom: $cf-marg-b;
transition: height 0.25s ease;
}
.notebook-panel--header {
background-color: $g3-castle;
border-radius: $cf-radius;
padding: 0 $cf-marg-b;
height: $notebook-header-height;
flex: 0 0 $notebook-header-height;
display: flex;
align-items: center;
justify-content: space-between;
}
.notebook-panel--header-left,
.notebook-panel--header-right {
flex: 1 0 $notebook-header-height;
}
.notebook-panel--title {
user-select: none;
font-size: 14px;
font-weight: $cf-font-weight--medium;
margin-left: $cf-marg-b;
margin-right: $cf-marg-c !important;
}
.notebook-panel--toggle {
width: $cf-form-sm-height;
height: $cf-form-sm-height;
position: relative;
&:after {
content: '';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: $notebook-size-toggle;
transition: height 0.25s cubic-bezier(0.25, 1, 0.5, 1), border-color 0.25s ease;
border: $cf-border solid $g8-storm;
border-radius: $cf-radius / 2;
}
&:hover {
cursor: pointer;
&:after {
border-color: $g13-mist;
}
}
}
.notebook-panel--body {
padding: $cf-marg-b;
flex: 1 0 0;
position: relative;
}
// Special styling for query builder inside notebook panel
.notebook-panel--body .query-builder {
top: $cf-marg-b;
right: $cf-marg-b;
bottom: $cf-marg-b;
left: $cf-marg-b;
padding-top: 0;
}
/*
Notebook Panel Modes
------------------------------------------------------------------------------
*/
.notebook-panel__hidden {
height: $notebook-header-height;
.notebook-panel--body {
display: none;
}
.notebook-panel--toggle:after {
height: $cf-border;
}
}
.notebook-panel__small {
height: 320px;
.notebook-panel--toggle:after {
height: $notebook-size-toggle / 2;
}
}
.notebook-panel__large {
height: 60vh;
min-height: 640px;
.notebook-panel--toggle:after {
height: $notebook-size-toggle;
}
}
.notebook-panel--visualization {
width: 100%;
height: 100%;
display: flex;
align-items: stretch;
flex-direction: row;
}
.notebook-panel--view {
flex: 1 0 0;
position: relative;
}
.notebook-header--buttons {
display: inline-flex;
flex: 0 0 auto;
flex-wrap: wrap;
> * {
margin-left: 4px;
}
}
.notebook--actions {
margin-top: $cf-marg-c;
display: inline-flex;
flex: 0 0 auto;
flex-wrap: wrap;
justify-content: center;
width: 100%;
> * {
margin-left: 4px;
}
}