diff --git a/ui/src/dashboards/selectors/index.test.ts b/ui/src/dashboards/selectors/index.test.ts index d36d2ac51a..3633abbdf9 100644 --- a/ui/src/dashboards/selectors/index.test.ts +++ b/ui/src/dashboards/selectors/index.test.ts @@ -82,6 +82,7 @@ describe('Dashboards.Selector', () => { autoRefresh: 0, showTemplateControlBar: false, navBarState: 'expanded', + notebookMiniMapState: 'expanded', timeZone: 'Local' as TimeZone, theme: 'dark', }, @@ -103,6 +104,7 @@ describe('Dashboards.Selector', () => { autoRefresh: 0, showTemplateControlBar: false, navBarState: 'expanded', + notebookMiniMapState: 'expanded', timeZone: 'UTC' as TimeZone, theme: 'dark', }, diff --git a/ui/src/mockState.tsx b/ui/src/mockState.tsx index e952852417..ba9dfad145 100644 --- a/ui/src/mockState.tsx +++ b/ui/src/mockState.tsx @@ -23,6 +23,7 @@ export const localState: LocalStorage = { navBarState: 'expanded', timeZone: 'Local' as TimeZone, theme: 'dark', + notebookMiniMapState: 'expanded', }, }, flags: { diff --git a/ui/src/notebooks/NotebookVariables.scss b/ui/src/notebooks/NotebookVariables.scss new file mode 100644 index 0000000000..2389534840 --- /dev/null +++ b/ui/src/notebooks/NotebookVariables.scss @@ -0,0 +1,9 @@ +@import '@influxdata/clockface/dist/variables.scss'; + +$notebook-header-height: 46px; + +$notebook-panel--gutter: $cf-marg-d; +$notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%); + +$notebook-divider-height: ($cf-marg-a * 2) + $cf-border; +$notebook-divider-color: $g2-kevlar; diff --git a/ui/src/notebooks/components/Notebook.tsx b/ui/src/notebooks/components/Notebook.tsx index 8a936c6dda..d7c2b0378b 100644 --- a/ui/src/notebooks/components/Notebook.tsx +++ b/ui/src/notebooks/components/Notebook.tsx @@ -2,8 +2,10 @@ import React, {FC} from 'react' import {Page} from '@influxdata/clockface' import {NotebookProvider} from 'src/notebooks/context/notebook' +import {ScrollProvider} from 'src/notebooks/context/scroll' import Header from 'src/notebooks/components/header' import PipeList from 'src/notebooks/components/PipeList' +import MiniMap from 'src/notebooks/components/minimap/MiniMap' // NOTE: uncommon, but using this to scope the project // within the page and not bleed it's dependancies outside @@ -13,10 +15,21 @@ import 'src/notebooks/style.scss' const NotebookPage: FC = () => { return ( - -
- - + + +
+ +
+ + +
+
+ + ) } diff --git a/ui/src/notebooks/components/PipeList.tsx b/ui/src/notebooks/components/PipeList.tsx index 222b447c5f..8fba4bd6a9 100644 --- a/ui/src/notebooks/components/PipeList.tsx +++ b/ui/src/notebooks/components/PipeList.tsx @@ -3,37 +3,42 @@ import React, {FC, useContext, useCallback} from 'react' // Contexts import {NotebookContext} from 'src/notebooks/context/notebook' +import {ScrollContext} from 'src/notebooks/context/scroll' // Components import NotebookPipe from 'src/notebooks/components/NotebookPipe' import EmptyPipeList from 'src/notebooks/components/EmptyPipeList' -import {Page} from '@influxdata/clockface' +import {DapperScrollbars} from '@influxdata/clockface' const PipeList: FC = () => { const {id, pipes, updatePipe} = useContext(NotebookContext) + const {scrollPosition} = useContext(ScrollContext) const update = useCallback(updatePipe, [id]) - const scrollable = !!pipes.length - - let _pipes: JSX.Element | JSX.Element[] = - - if (pipes.length) { - _pipes = pipes.map((_, index) => { - return ( - - ) - }) + if (!pipes.length) { + return } + const _pipes = pipes.map((_, index) => { + return ( + + ) + }) + return ( - + {_pipes} - + ) } diff --git a/ui/src/notebooks/components/header/index.tsx b/ui/src/notebooks/components/header/index.tsx index 3e3cbd6cd7..8edf64a00a 100644 --- a/ui/src/notebooks/components/header/index.tsx +++ b/ui/src/notebooks/components/header/index.tsx @@ -3,6 +3,7 @@ import React, {FC} from 'react' import {Page} from '@influxdata/clockface' import AddButtons from 'src/notebooks/components/AddButtons' import Buttons from 'src/notebooks/components/header/Buttons' +import MiniMapToggle from 'src/notebooks/components/minimap/MiniMapToggle' const FULL_WIDTH = true @@ -17,6 +18,7 @@ const Header: FC = () => ( + diff --git a/ui/src/notebooks/components/minimap/MiniMap.scss b/ui/src/notebooks/components/minimap/MiniMap.scss new file mode 100644 index 0000000000..15f30834bb --- /dev/null +++ b/ui/src/notebooks/components/minimap/MiniMap.scss @@ -0,0 +1,40 @@ +@import '@influxdata/clockface/dist/variables.scss'; +@import '~src/notebooks/NotebookVariables.scss'; + +.notebook-minimap { + flex: 0 0 200px; + border-right: $cf-border solid $notebook-divider-color; +} + +.notebook-minimap--list { + padding-left: $cf-marg-d; + padding-right: $cf-marg-b; +} + +.notebook-minimap--item { + font-size: 14px; + font-weight: $cf-font-weight--medium; + color: $g11-sidewalk; + transition: color 0.25s ease, background-color 0.25s ease; + margin-bottom: $cf-border; + height: $cf-marg-d; + line-height: $cf-marg-d; + padding: 0 $cf-marg-b; + border-radius: $cf-radius; + user-select: none; + + &:hover { + cursor: pointer; + } +} + +.notebook-minimap--item__hidden { + color: $g8-storm; + font-style: italic; +} + +.notebook-minimap--item:hover, +.notebook-minimap--item__focus { + color: $g18-cloud; + background-color: $g3-castle; +} diff --git a/ui/src/notebooks/components/minimap/MiniMap.tsx b/ui/src/notebooks/components/minimap/MiniMap.tsx new file mode 100644 index 0000000000..47aaffec7b --- /dev/null +++ b/ui/src/notebooks/components/minimap/MiniMap.tsx @@ -0,0 +1,70 @@ +// Libraries +import React, {FC, useContext} from 'react' +import {connect} from 'react-redux' + +// Contexts +import {NotebookContext, PipeMeta} from 'src/notebooks/context/notebook' +import {ScrollContext} from 'src/notebooks/context/scroll' + +// Components +import {DapperScrollbars} from '@influxdata/clockface' +import MiniMapItem from 'src/notebooks/components/minimap/MiniMapItem' + +// Types +import {AppState, NotebookMiniMapState} from 'src/types' + +// Styles +import 'src/notebooks/components/minimap/MiniMap.scss' + +interface StateProps { + notebookMiniMapState: NotebookMiniMapState +} + +const MiniMap: FC = ({notebookMiniMapState}) => { + const {meta, updateMeta} = useContext(NotebookContext) + const {scrollToPipe} = useContext(ScrollContext) + + if (notebookMiniMapState === 'collapsed') { + return null + } + + const handleClick = (idx: number): void => { + const {panelRef} = meta[idx] + scrollToPipe(panelRef) + updateMeta(idx, {focus: true} as PipeMeta) + } + + const pipes = meta.map((pipe, index) => ( + + )) + + return ( + +
{pipes}
+
+ ) +} + +const mstp = (state: AppState): StateProps => { + const { + app: { + persisted: {notebookMiniMapState}, + }, + } = state + + return { + notebookMiniMapState, + } +} + +export default connect( + mstp, + null +)(MiniMap) diff --git a/ui/src/notebooks/components/minimap/MiniMapItem.tsx b/ui/src/notebooks/components/minimap/MiniMapItem.tsx new file mode 100644 index 0000000000..349d82db40 --- /dev/null +++ b/ui/src/notebooks/components/minimap/MiniMapItem.tsx @@ -0,0 +1,30 @@ +// Libraries +import React, {FC} from 'react' +import classnames from 'classnames' + +interface Props { + title: string + focus: boolean + visible: boolean + index: number + onClick: (index: number) => void +} + +const MiniMapItem: FC = ({title, focus, onClick, index, visible}) => { + const className = classnames('notebook-minimap--item', { + 'notebook-minimap--item__focus': focus, + 'notebook-minimap--item__hidden': !visible, + }) + + const handleClick = (): void => { + onClick(index) + } + + return ( +
+ {title} +
+ ) +} + +export default MiniMapItem diff --git a/ui/src/notebooks/components/minimap/MiniMapToggle.tsx b/ui/src/notebooks/components/minimap/MiniMapToggle.tsx new file mode 100644 index 0000000000..9736b0e40e --- /dev/null +++ b/ui/src/notebooks/components/minimap/MiniMapToggle.tsx @@ -0,0 +1,65 @@ +// Libraries +import React, {FC} from 'react' +import {connect} from 'react-redux' + +// Components +import {SlideToggle, InputLabel} from '@influxdata/clockface' + +// Actions +import {setNotebookMiniMapState} from 'src/shared/actions/app' + +// Types +import {AppState, NotebookMiniMapState} from 'src/types' + +interface StateProps { + notebookMiniMapState: NotebookMiniMapState +} + +interface DispatchProps { + handleSetNotebookMiniMapState: typeof setNotebookMiniMapState +} + +type Props = StateProps & DispatchProps + +const MiniMapToggle: FC = ({ + notebookMiniMapState, + handleSetNotebookMiniMapState, +}) => { + const active = notebookMiniMapState === 'expanded' + + const handleChange = (): void => { + if (active) { + handleSetNotebookMiniMapState('collapsed') + } else { + handleSetNotebookMiniMapState('expanded') + } + } + + return ( + <> + + Minimap + + ) +} + +const mstp = (state: AppState): StateProps => { + const { + app: { + persisted: {notebookMiniMapState}, + }, + } = state + + return { + notebookMiniMapState, + } +} + +const mdtp: DispatchProps = { + handleSetNotebookMiniMapState: setNotebookMiniMapState, +} + +export default connect( + mstp, + mdtp +)(MiniMapToggle) diff --git a/ui/src/notebooks/components/panel/NotebookPanel.tsx b/ui/src/notebooks/components/panel/NotebookPanel.tsx index 70cecd4b0d..a8fd9a2357 100644 --- a/ui/src/notebooks/components/panel/NotebookPanel.tsx +++ b/ui/src/notebooks/components/panel/NotebookPanel.tsx @@ -1,5 +1,13 @@ // Libraries -import React, {FC, useContext, useCallback, ReactNode, MouseEvent} from 'react' +import React, { + FC, + useContext, + useCallback, + useEffect, + ReactNode, + MouseEvent, + useRef, +} from 'react' import classnames from 'classnames' // Components @@ -77,10 +85,15 @@ const NotebookPanelHeader: FC = ({index, controls}) => { const NotebookPanel: FC = ({index, children, controls}) => { const {meta, updateMeta} = useContext(NotebookContext) + const panelRef = useRef(null) const isVisible = meta[index].visible const isFocused = meta[index].focus + useEffect(() => { + updateMeta(index, {panelRef} as PipeMeta) + }) + const panelClassName = classnames('notebook-panel', { [`notebook-panel__visible`]: isVisible, [`notebook-panel__hidden`]: !isVisible, @@ -105,7 +118,7 @@ const NotebookPanel: FC = ({index, children, controls}) => { return ( -
+
{children}
diff --git a/ui/src/notebooks/context/notebook.tsx b/ui/src/notebooks/context/notebook.tsx index 6da564524e..3d99de9f5f 100644 --- a/ui/src/notebooks/context/notebook.tsx +++ b/ui/src/notebooks/context/notebook.tsx @@ -1,10 +1,11 @@ -import React, {FC, useState, useCallback} from 'react' +import React, {FC, useState, useCallback, RefObject} from 'react' import {PipeData} from 'src/notebooks' export interface PipeMeta { title: string visible: boolean focus: boolean + panelRef: RefObject } export interface NotebookContextType { diff --git a/ui/src/notebooks/context/scroll.tsx b/ui/src/notebooks/context/scroll.tsx new file mode 100644 index 0000000000..f037a0cd42 --- /dev/null +++ b/ui/src/notebooks/context/scroll.tsx @@ -0,0 +1,40 @@ +// Libraries +import React, {FC, useState, RefObject} from 'react' + +export interface ScrollContextType { + scrollPosition: number + scrollToPipe: (panelRef: RefObject) => void +} + +export const DEFAULT_CONTEXT: ScrollContextType = { + scrollPosition: 0, + scrollToPipe: () => {}, +} + +export const ScrollContext = React.createContext( + DEFAULT_CONTEXT +) + +export const ScrollProvider: FC = ({children}) => { + const [scrollPosition, setListScrollPosition] = useState( + DEFAULT_CONTEXT.scrollPosition + ) + + const scrollToPipe = (panelRef: RefObject) => { + if (panelRef && panelRef.current) { + const {offsetTop} = panelRef.current + setListScrollPosition(offsetTop) + } + } + + return ( + + {children} + + ) +} diff --git a/ui/src/notebooks/pipes/markdown/style.scss b/ui/src/notebooks/pipes/markdown/style.scss index c23f1af242..6500e46fb4 100644 --- a/ui/src/notebooks/pipes/markdown/style.scss +++ b/ui/src/notebooks/pipes/markdown/style.scss @@ -1,31 +1,19 @@ -@import "@influxdata/clockface/dist/variables.scss"; +@import '@influxdata/clockface/dist/variables.scss'; $notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%); -.notebook-panel--markdown, -.notebook-panel--markdown-editor { - font-size: 14px; - line-height: 16px; +.notebook-panel--markdown { + font-size: 14px; + line-height: 16px; padding: $cf-marg-b; border-radius: $cf-radius - 1px; - border-width: $cf-border; - border-style: solid; - transition: border-color 0.25s ease; -} - -.notebook-panel--markdown { - border-color: $g1-raven; - - .notebook-panel:hover & { - border-color: $notebook-panel--bg; - } + transition: border-color 0.25s ease; } .notebook-panel--body .markdown-editor--monaco { position: relative; .react-monaco-editor-container { - min-height: 100px; + min-height: 100px; } } - diff --git a/ui/src/notebooks/style.scss b/ui/src/notebooks/style.scss index 0ef9889528..f4515a9344 100644 --- a/ui/src/notebooks/style.scss +++ b/ui/src/notebooks/style.scss @@ -1,19 +1,18 @@ @import '@influxdata/clockface/dist/variables.scss'; - -$notebook-header-height: 46px; -$notebook-size-toggle: 16px; -$notebook-left-bar: 22px; -$notebook-panel--bg: mix($g1-raven, $g2-kevlar, 50%); - -$notebook-divider-height: ($cf-marg-a * 2) + $cf-border; +@import '~src/notebooks/NotebookVariables.scss'; .notebook { width: 100%; + height: 100%; display: flex; - flex-direction: column; + flex-direction: row; align-items: stretch; } +.notebook-page .cf-page-contents__fluid { + padding: 0; +} + .notebook--add-cell-label { user-select: none; margin: 0 $cf-marg-c 0 0; @@ -21,16 +20,26 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border; font-weight: $cf-font-weight--medium; } +.notebook-main, +.notebook-empty { + flex: 1 0 0; +} + +.notebook-empty { + padding: 0 $notebook-panel--gutter; +} + .notebook-panel { display: flex; flex-direction: column; align-items: stretch; border-radius: $cf-radius; + margin: 0 $notebook-panel--gutter; &:after { content: ''; height: $cf-border; - background-color: $g2-kevlar; + background-color: $notebook-divider-color; border-radius: $cf-border / 2; margin: $cf-marg-a 0; } @@ -121,37 +130,6 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border; top: -2px; } -.notebook-panel--toggle { - top: 0; - left: 0; - width: $notebook-left-bar; - background-color: $g2-kevlar; - border-radius: $cf-radius; - height: 100%; - position: absolute; - - &:after { - box-sizing: border-box; - 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 { border-radius: 0 0 $cf-radius $cf-radius; padding: $cf-marg-b; @@ -198,15 +176,6 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border; min-height: $notebook-header-height; } -/* - Notebook Action Bar aka Footer - ------------------------------------------------------------------------------ -*/ - -.notebook--actions { - margin-top: $cf-marg-c; -} - /* Visualization Panel ------------------------------------------------------------------------------ @@ -242,6 +211,6 @@ $notebook-divider-height: ($cf-marg-a * 2) + $cf-border; flex-wrap: wrap; > * { - margin-left: 4px; + margin-left: $cf-marg-a; } } diff --git a/ui/src/shared/actions/app.ts b/ui/src/shared/actions/app.ts index de754f9467..59db326fdd 100644 --- a/ui/src/shared/actions/app.ts +++ b/ui/src/shared/actions/app.ts @@ -4,12 +4,13 @@ import {notify} from 'src/shared/actions/notifications' import {presentationMode} from 'src/shared/copy/notifications' import {Dispatch} from 'redux' -import {TimeZone, Theme, NavBarState} from 'src/types' +import {TimeZone, Theme, NavBarState, NotebookMiniMapState} from 'src/types' export enum ActionTypes { EnablePresentationMode = 'ENABLE_PRESENTATION_MODE', DisablePresentationMode = 'DISABLE_PRESENTATION_MODE', SetNavBarState = 'SET_NAV_BAR_STATE', + SetNotebookMiniMapState = 'SET_NOTEBOOK_MINI_MAP_STATE', SetAutoRefresh = 'SET_AUTOREFRESH', SetTimeZone = 'SET_APP_TIME_ZONE', TemplateControlBarVisibilityToggled = 'TemplateControlBarVisibilityToggledAction', @@ -20,6 +21,7 @@ export type Action = | ReturnType | ReturnType | ReturnType + | ReturnType | ReturnType | ReturnType | ReturnType @@ -54,6 +56,14 @@ export const setNavBarState = (navBarState: NavBarState) => navBarState, } as const) +export const setNotebookMiniMapState = ( + notebookMiniMapState: NotebookMiniMapState +) => + ({ + type: ActionTypes.SetNotebookMiniMapState, + notebookMiniMapState, + } as const) + export const setAutoRefresh = (milliseconds: number) => ({ type: ActionTypes.SetAutoRefresh, diff --git a/ui/src/shared/reducers/app.test.ts b/ui/src/shared/reducers/app.test.ts index 5c71acdfc4..f7fd0a247a 100644 --- a/ui/src/shared/reducers/app.test.ts +++ b/ui/src/shared/reducers/app.test.ts @@ -4,6 +4,7 @@ import { disablePresentationMode, setTheme, setNavBarState, + setNotebookMiniMapState, setAutoRefresh, } from 'src/shared/actions/app' import {TimeZone} from 'src/types' @@ -18,6 +19,7 @@ describe('Shared.Reducers.appReducer', () => { autoRefresh: 0, showTemplateControlBar: false, navBarState: 'expanded', + notebookMiniMapState: 'expanded', timeZone: 'Local' as TimeZone, theme: 'dark', }, @@ -65,6 +67,28 @@ describe('Shared.Reducers.appReducer', () => { expect(reducedState.persisted.navBarState).toBe('expanded') }) + it('should handle SET_NOTEBOOK_MINI_MAP_STATE to collapsed', () => { + const reducedState = appReducer( + initialState, + setNotebookMiniMapState('collapsed') + ) + + expect(reducedState.persisted.notebookMiniMapState).toBe('collapsed') + }) + + it('should handle SET_NOTEBOOK_MINI_MAP_STATE to expanded', () => { + Object.assign(initialState, { + persisted: {notebookMiniMapState: 'collapsed'}, + }) + + const reducedState = appReducer( + initialState, + setNotebookMiniMapState('expanded') + ) + + expect(reducedState.persisted.notebookMiniMapState).toBe('expanded') + }) + it('should handle SET_AUTOREFRESH', () => { const expectedMs = 15000 diff --git a/ui/src/shared/reducers/app.ts b/ui/src/shared/reducers/app.ts index c6bfb6f94a..6a336aee96 100644 --- a/ui/src/shared/reducers/app.ts +++ b/ui/src/shared/reducers/app.ts @@ -3,7 +3,7 @@ import {combineReducers} from 'redux' // Types import {ActionTypes, Action} from 'src/shared/actions/app' import {AUTOREFRESH_DEFAULT_INTERVAL} from 'src/shared/constants' -import {TimeZone, NavBarState, Theme} from 'src/types' +import {TimeZone, NavBarState, Theme, NotebookMiniMapState} from 'src/types' export interface AppState { ephemeral: { @@ -15,6 +15,7 @@ export interface AppState { timeZone: TimeZone navBarState: NavBarState theme: Theme + notebookMiniMapState: NotebookMiniMapState } } @@ -28,6 +29,7 @@ const initialState: AppState = { showTemplateControlBar: false, timeZone: 'Local', navBarState: 'collapsed', + notebookMiniMapState: 'expanded', }, } @@ -82,6 +84,12 @@ const appPersistedReducer = ( return {...state, timeZone} } + case ActionTypes.SetNotebookMiniMapState: { + const notebookMiniMapState = action.notebookMiniMapState + + return {...state, notebookMiniMapState} + } + case 'SET_NAV_BAR_STATE': { const navBarState = action.navBarState return { diff --git a/ui/src/types/app.ts b/ui/src/types/app.ts index 3de1f43c96..c1fd5dbcf3 100644 --- a/ui/src/types/app.ts +++ b/ui/src/types/app.ts @@ -1,3 +1,4 @@ export type CurrentPage = 'dashboard' | 'not set' export type Theme = 'light' | 'dark' export type NavBarState = 'expanded' | 'collapsed' +export type NotebookMiniMapState = 'expanded' | 'collapsed'