feat: adding basic structure for notebooks (#18108)
parent
1199136bbd
commit
89fe6834b0
|
@ -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",
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -12,7 +12,7 @@ export interface NotebookContextType {
|
|||
removePipe: (idx: number) => void
|
||||
}
|
||||
|
||||
export const DEFAULT_CONTEXT = {
|
||||
export const DEFAULT_CONTEXT: NotebookContextType = {
|
||||
id: 'new',
|
||||
pipes: [],
|
||||
addPipe: () => {},
|
||||
|
|
|
@ -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
|
|
@ -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)
|
||||
})
|
|
@ -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',
|
||||
},
|
||||
})
|
|
@ -0,0 +1,5 @@
|
|||
.notebook-example {
|
||||
h1 {
|
||||
color: #f00;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue