Merge pull request #12703 from influxdata/refactor/add-note-dashboard

Refactor/add note dashboard
pull/12712/head
Iris Scholten 2019-03-18 16:44:11 -07:00 committed by GitHub
commit a6abcea690
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 201 additions and 196 deletions

View File

@ -9,6 +9,7 @@
### Bug Fixes
1. [12684](https://github.com/influxdata/influxdb/pull/12684): Fix mismatch in bucket row and header
1. [12703](https://github.com/influxdata/influxdb/pull/12703): Allows user to edit note on cell
### UI Improvements

View File

@ -7,42 +7,21 @@ import {updateView} from 'src/dashboards/actions/views'
// Utils
import {createView} from 'src/shared/utils/view'
import {getView} from 'src/dashboards/selectors'
// Types
import {GetState} from 'src/types/v2'
import {NoteEditorMode, MarkdownView, ViewType} from 'src/types/v2/dashboards'
import {NoteEditorState} from 'src/dashboards/reducers/notes'
import {Dispatch} from 'redux-thunk'
export type Action =
| OpenNoteEditorAction
| CloseNoteEditorAction
| SetIsPreviewingAction
| ToggleShowNoteWhenEmptyAction
| SetNoteAction
interface OpenNoteEditorAction {
type: 'OPEN_NOTE_EDITOR'
payload: {initialState: Partial<NoteEditorState>}
}
export const openNoteEditor = (
initialState: Partial<NoteEditorState>
): OpenNoteEditorAction => ({
type: 'OPEN_NOTE_EDITOR',
payload: {initialState},
})
export const addNote = (): OpenNoteEditorAction => ({
type: 'OPEN_NOTE_EDITOR',
payload: {
initialState: {
mode: NoteEditorMode.Adding,
viewID: null,
toggleVisible: false,
note: '',
},
},
})
| SetNoteStateAction
| ResetNoteStateAction
interface CloseNoteEditorAction {
type: 'CLOSE_NOTE_EDITOR'
@ -83,7 +62,7 @@ export const setNote = (note: string): SetNoteAction => ({
})
export const createNoteCell = (dashboardID: string) => async (
dispatch,
dispatch: Dispatch<Action>,
getState: GetState
) => {
const dashboard = getState().dashboards.find(d => d.id === dashboardID)
@ -100,13 +79,68 @@ export const createNoteCell = (dashboardID: string) => async (
return dispatch(createCellWithView(dashboard, view))
}
export const updateViewNote = () => async (dispatch, getState: GetState) => {
export interface ResetNoteStateAction {
type: 'RESET_NOTE_STATE'
}
export const resetNoteState = (): ResetNoteStateAction => ({
type: 'RESET_NOTE_STATE',
})
export interface SetNoteStateAction {
type: 'SET_NOTE_STATE'
payload: Partial<NoteEditorState>
}
export const setNoteState = (
noteState: Partial<NoteEditorState>
): SetNoteStateAction => ({
type: 'SET_NOTE_STATE',
payload: noteState,
})
export const loadNote = (id: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const {
views: {views},
} = getState()
const currentViewState = views[id]
if (!currentViewState) {
return
}
const view = currentViewState.view
const note: string = get(view, 'properties.note', '')
const showNoteWhenEmpty: boolean = get(
view,
'properties.showNoteWhenEmpty',
false
)
const initialState = {
viewID: view.id,
note,
showNoteWhenEmpty,
mode: NoteEditorMode.Editing,
}
dispatch(setNoteState(initialState))
}
export const updateViewNote = (id: string) => async (
dispatch: Dispatch<Action>,
getState: GetState
) => {
const state = getState()
const {note, showNoteWhenEmpty, viewID} = state.noteEditor
const view: any = get(state, `views.${viewID}.view`)
const {note, showNoteWhenEmpty} = state.noteEditor
const view: any = getView(state, id)
if (!view) {
throw new Error(`could not find view with id "${viewID}"`)
throw new Error(`could not find view with id "${id}"`)
}
if (isUndefined(view.properties.note)) {

View File

@ -24,6 +24,7 @@ interface Props {
setScrollTop: (e: MouseEvent<HTMLElement>) => void
onEditView: (cellID: string) => void
onAddCell: () => void
onEditNote: (id: string) => void
}
@ErrorHandling
@ -42,6 +43,7 @@ class DashboardComponent extends PureComponent<Props> {
inPresentationMode,
setScrollTop,
onAddCell,
onEditNote,
} = this.props
return (
@ -63,6 +65,7 @@ class DashboardComponent extends PureComponent<Props> {
onDeleteCell={onDeleteCell}
onPositionChange={onPositionChange}
onEditView={onEditView}
onEditNote={onEditNote}
/>
) : (
<DashboardEmpty onAddCell={onAddCell} />

View File

@ -1,6 +1,5 @@
// Libraries
import React, {Component} from 'react'
import {connect} from 'react-redux'
// Components
import {Page} from 'src/pageLayout'
@ -21,15 +20,16 @@ import {
DASHBOARD_NAME_MAX_LENGTH,
} from 'src/dashboards/constants/index'
// Actions
import {addNote} from 'src/dashboards/actions/notes'
// Types
import * as AppActions from 'src/types/actions/app'
import * as QueriesModels from 'src/types/queries'
import {Dashboard} from '@influxdata/influx'
interface OwnProps {
interface DefaultProps {
zoomedTimeRange: QueriesModels.TimeRange
}
interface Props extends DefaultProps {
activeDashboard: string
dashboard: Dashboard
timeRange: QueriesModels.TimeRange
@ -40,21 +40,15 @@ interface OwnProps {
handleClickPresentationButton: AppActions.DelayEnablePresentationModeDispatcher
onAddCell: () => void
showTemplateControlBar: boolean
zoomedTimeRange: QueriesModels.TimeRange
onRenameDashboard: (name: string) => Promise<void>
toggleVariablesControlBar: () => void
isShowingVariablesControlBar: boolean
isHidden: boolean
onAddNote: () => void
}
interface DispatchProps {
onAddNote: typeof addNote
}
type Props = OwnProps & DispatchProps
class DashboardHeader extends Component<Props> {
public static defaultProps: Partial<Props> = {
export default class DashboardHeader extends Component<Props> {
public static defaultProps: DefaultProps = {
zoomedTimeRange: {
upper: null,
lower: null,
@ -70,21 +64,36 @@ class DashboardHeader extends Component<Props> {
timeRange: {upper, lower},
zoomedTimeRange: {upper: zoomedUpper, lower: zoomedLower},
isHidden,
onAddNote,
toggleVariablesControlBar,
isShowingVariablesControlBar,
onAddCell,
onRenameDashboard,
activeDashboard,
} = this.props
return (
<Page.Header fullWidth={true} inPresentationMode={isHidden}>
<Page.Header.Left>{this.dashboardTitle}</Page.Header.Left>
<Page.Header.Left>
<RenamablePageTitle
maxLength={DASHBOARD_NAME_MAX_LENGTH}
onRename={onRenameDashboard}
name={activeDashboard}
placeholder={DEFAULT_DASHBOARD_NAME}
/>
</Page.Header.Left>
<Page.Header.Right>
<GraphTips />
{this.addCellButton}
<Button
icon={IconFont.AddCell}
color={ComponentColor.Primary}
onClick={onAddCell}
text="Add Cell"
titleText="Add cell to dashboard"
/>
<Button
icon={IconFont.TextBlock}
text="Add Note"
onClick={onAddNote}
onClick={this.handleAddNote}
/>
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
@ -118,49 +127,11 @@ class DashboardHeader extends Component<Props> {
)
}
private handleAddNote = () => {
this.props.onAddNote()
}
private handleClickPresentationButton = (): void => {
this.props.handleClickPresentationButton()
}
private get addCellButton(): JSX.Element {
const {dashboard, onAddCell} = this.props
if (dashboard) {
return (
<Button
icon={IconFont.AddCell}
color={ComponentColor.Primary}
onClick={onAddCell}
text="Add Cell"
titleText="Add cell to dashboard"
/>
)
}
}
private get dashboardTitle(): JSX.Element {
const {dashboard, activeDashboard, onRenameDashboard} = this.props
if (dashboard) {
return (
<RenamablePageTitle
maxLength={DASHBOARD_NAME_MAX_LENGTH}
onRename={onRenameDashboard}
name={activeDashboard}
placeholder={DEFAULT_DASHBOARD_NAME}
/>
)
}
return <Page.Title title={activeDashboard} />
}
}
const mdtp = {
onAddNote: addNote,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(DashboardHeader)

View File

@ -11,7 +11,6 @@ import DashboardHeader from 'src/dashboards/components/DashboardHeader'
import DashboardComponent from 'src/dashboards/components/Dashboard'
import ManualRefresh from 'src/shared/components/ManualRefresh'
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
import NoteEditorContainer from 'src/dashboards/components/NoteEditorContainer'
import VariablesControlBar from 'src/dashboards/components/variablesControlBar/VariablesControlBar'
// Actions
@ -164,6 +163,7 @@ class DashboardPage extends Component<Props, State> {
autoRefresh={autoRefresh}
isHidden={inPresentationMode}
onAddCell={this.handleAddCell}
onAddNote={this.showNoteOverlay}
onManualRefresh={onManualRefresh}
zoomedTimeRange={zoomedTimeRange}
onRenameDashboard={this.handleRenameDashboard}
@ -193,11 +193,11 @@ class DashboardPage extends Component<Props, State> {
onDeleteCell={this.handleDeleteDashboardCell}
onEditView={this.handleEditView}
onAddCell={this.handleAddCell}
onEditNote={this.showNoteOverlay}
/>
)}
{children}
</HoverTimeProvider>
<NoteEditorContainer />
</Page>
)
}
@ -240,6 +240,14 @@ class DashboardPage extends Component<Props, State> {
this.showVEO()
}
private showNoteOverlay = async (id?: string): Promise<void> => {
if (id) {
this.props.router.push(`${this.props.location.pathname}/notes/${id}/edit`)
} else {
this.props.router.push(`${this.props.location.pathname}/notes/new`)
}
}
private handleEditView = (cellID: string): void => {
this.showVEO(cellID)
}

View File

@ -28,7 +28,6 @@ import {AppState} from 'src/types/v2'
interface StateProps {
note: string
toggleVisible: boolean
showNoteWhenEmpty: boolean
}
@ -69,36 +68,25 @@ class NoteEditor extends PureComponent<Props> {
}
private get visibilityToggle(): JSX.Element {
const {
toggleVisible,
showNoteWhenEmpty,
onToggleShowNoteWhenEmpty,
} = this.props
const {showNoteWhenEmpty, onToggleShowNoteWhenEmpty} = this.props
if (toggleVisible) {
return (
<ComponentSpacer stackChildren={Stack.Columns} align={Alignment.Right}>
<SlideToggle.Label text="Show note when query returns no data" />
<SlideToggle
active={showNoteWhenEmpty}
size={ComponentSize.ExtraSmall}
onChange={onToggleShowNoteWhenEmpty}
/>
</ComponentSpacer>
)
}
return (
<ComponentSpacer stackChildren={Stack.Columns} align={Alignment.Right}>
<SlideToggle.Label text="Show note when query returns no data" />
<SlideToggle
active={showNoteWhenEmpty}
size={ComponentSize.ExtraSmall}
onChange={onToggleShowNoteWhenEmpty}
/>
</ComponentSpacer>
)
}
}
const mstp = (state: AppState) => {
const {
note,
isPreviewing,
toggleVisible,
showNoteWhenEmpty,
} = state.noteEditor
const {note, isPreviewing, showNoteWhenEmpty} = state.noteEditor
return {note, isPreviewing, toggleVisible, showNoteWhenEmpty}
return {note, isPreviewing, showNoteWhenEmpty}
}
const mdtp = {

View File

@ -10,9 +10,10 @@ import {Overlay} from 'src/clockface'
// Actions
import {
closeNoteEditor,
createNoteCell,
updateViewNote,
loadNote,
resetNoteState,
} from 'src/dashboards/actions/notes'
import {notify} from 'src/shared/actions/notifications'
@ -26,41 +27,43 @@ import {NoteEditorMode} from 'src/types/v2/dashboards'
interface StateProps {
mode: NoteEditorMode
overlayVisible: boolean
viewID: string
}
interface DispatchProps {
onHide: typeof closeNoteEditor
onCreateNoteCell: (dashboardID: string) => Promise<void>
onUpdateViewNote: () => Promise<void>
onCreateNoteCell: typeof createNoteCell
onUpdateViewNote: typeof updateViewNote
resetNote: typeof resetNoteState
onNotify: typeof notify
loadNote: typeof loadNote
}
interface OwnProps {}
interface RouterProps extends WithRouterProps {
params: {
dashboardID: string
cellID?: string
}
}
type Props = StateProps & DispatchProps & OwnProps & WithRouterProps
type Props = StateProps & DispatchProps & RouterProps
interface State {
savingStatus: RemoteDataState
}
class NoteEditorContainer extends PureComponent<Props, State> {
class NoteEditorOverlay extends PureComponent<Props, State> {
public state: State = {savingStatus: RemoteDataState.NotStarted}
public render() {
const {onHide, overlayVisible} = this.props
return (
<div className="note-editor-container">
<Overlay visible={overlayVisible}>
<Overlay visible={true}>
<Overlay.Container maxWidth={900}>
<Overlay.Heading title={this.overlayTitle} onDismiss={onHide} />
<Overlay.Heading title={this.overlayTitle} onDismiss={this.close} />
<Overlay.Body>
<NoteEditor />
</Overlay.Body>
<Overlay.Footer>
<Button text="Cancel" onClick={onHide} />
<Button text="Cancel" onClick={this.close} />
<Button
text="Save"
color={ComponentColor.Success}
@ -74,6 +77,17 @@ class NoteEditorContainer extends PureComponent<Props, State> {
)
}
componentDidMount() {
const {
params: {cellID},
} = this.props
if (cellID) {
this.props.loadNote(cellID)
} else {
this.props.resetNote()
}
}
private get overlayTitle(): string {
const {mode} = this.props
@ -100,47 +114,48 @@ class NoteEditorContainer extends PureComponent<Props, State> {
private handleSave = async () => {
const {
viewID,
params: {cellID, dashboardID},
onCreateNoteCell,
onUpdateViewNote,
onHide,
onNotify,
} = this.props
const dashboardID = this.props.params.dashboardID
this.setState({savingStatus: RemoteDataState.Loading})
try {
if (viewID) {
await onUpdateViewNote()
if (cellID) {
await onUpdateViewNote(cellID)
} else {
await onCreateNoteCell(dashboardID)
}
this.setState({savingStatus: RemoteDataState.NotStarted}, onHide)
this.close()
} catch (error) {
onNotify(savingNoteFailed(error.message))
console.error(error)
this.setState({savingStatus: RemoteDataState.Error})
}
}
private close = () => {
this.props.router.goBack()
}
}
const mstp = (state: AppState) => {
const {mode, overlayVisible, viewID} = state.noteEditor
const mstp = (state: AppState): StateProps => {
const {mode} = state.noteEditor
return {mode, overlayVisible, viewID}
return {mode}
}
const mdtp = {
onHide: closeNoteEditor,
onNotify: notify,
onCreateNoteCell: createNoteCell as any,
onUpdateViewNote: updateViewNote as any,
onCreateNoteCell: createNoteCell,
onUpdateViewNote: updateViewNote,
resetNote: resetNoteState,
loadNote,
}
export default connect<StateProps, DispatchProps, OwnProps>(
export default connect<StateProps, DispatchProps, {}>(
mstp,
mdtp
)(withRouter<StateProps & DispatchProps & OwnProps>(NoteEditorContainer))
)(withRouter<StateProps & DispatchProps>(NoteEditorOverlay))

View File

@ -2,20 +2,14 @@ import {Action} from 'src/dashboards/actions/notes'
import {NoteEditorMode} from 'src/types/v2/dashboards'
export interface NoteEditorState {
overlayVisible: boolean
mode: NoteEditorMode
viewID: string
toggleVisible: boolean
note: string
showNoteWhenEmpty: boolean
isPreviewing: boolean
}
const initialState = (): NoteEditorState => ({
overlayVisible: false,
mode: NoteEditorMode.Adding,
viewID: null,
toggleVisible: false,
note: '',
showNoteWhenEmpty: false,
isPreviewing: false,
@ -26,13 +20,15 @@ const noteEditorReducer = (
action: Action
) => {
switch (action.type) {
case 'OPEN_NOTE_EDITOR': {
const {initialState} = action.payload
case 'RESET_NOTE_STATE': {
return initialState()
}
case 'SET_NOTE_STATE': {
const initialState = action.payload
return {
...state,
...initialState,
overlayVisible: true,
isPreviewing: false,
}
}

View File

@ -48,6 +48,7 @@ import OrgTasksIndex from 'src/organizations/containers/OrgTasksIndex'
import TaskExportOverlay from 'src/organizations/components/TaskExportOverlay'
import TaskImportOverlay from 'src/organizations/components/TaskImportOverlay'
import VEO from 'src/dashboards/components/VEO'
import NoteEditorOverlay from 'src/dashboards/components/NoteEditorOverlay'
import OnboardingWizardPage from 'src/onboarding/containers/OnboardingWizardPage'
@ -182,6 +183,13 @@ class Root extends PureComponent {
<Route path="new" component={VEO} />
<Route path=":cellID/edit" component={VEO} />
</Route>
<Route path="notes">
<Route path="new" component={NoteEditorOverlay} />
<Route
path=":cellID/edit"
component={NoteEditorOverlay}
/>
</Route>
</Route>
<Route
path=":dashboardID/export"

View File

@ -31,6 +31,7 @@ interface OwnProps {
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onEditCell: () => void
onEditNote: (id: string) => void
onZoom: (range: TimeRange) => void
}
@ -39,7 +40,14 @@ type Props = StateProps & OwnProps
@ErrorHandling
class CellComponent extends Component<Props> {
public render() {
const {onEditCell, onDeleteCell, onCloneCell, cell, view} = this.props
const {
onEditCell,
onEditNote,
onDeleteCell,
onCloneCell,
cell,
view,
} = this.props
return (
<>
@ -51,6 +59,7 @@ class CellComponent extends Component<Props> {
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
onEditCell={onEditCell}
onEditNote={onEditNote}
onCSVDownload={this.handleCSVDownload}
/>
)}

View File

@ -1,37 +1,27 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {get} from 'lodash'
// Components
import {Context} from 'src/clockface'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Actions
import {openNoteEditor} from 'src/dashboards/actions/notes'
// Types
import {IconFont, ComponentColor} from '@influxdata/clockface'
import {Cell, View, ViewType} from 'src/types/v2'
import {NoteEditorMode} from 'src/types/v2/dashboards'
interface OwnProps {
interface Props {
cell: Cell
view: View
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onCSVDownload: () => void
onEditCell: () => void
onEditNote: (id: string) => void
}
interface DispatchProps {
onOpenNoteEditor: typeof openNoteEditor
}
type Props = DispatchProps & OwnProps
@ErrorHandling
class CellContext extends PureComponent<Props> {
export default class CellContext extends PureComponent<Props> {
public render() {
const {cell, onDeleteCell, onCloneCell} = this.props
@ -77,32 +67,11 @@ class CellContext extends PureComponent<Props> {
}
private handleEditNote = () => {
const {onOpenNoteEditor, view} = this.props
const {
view: {id},
onEditNote,
} = this.props
const note: string = get(view, 'properties.note', '')
const showNoteWhenEmpty: boolean = get(
view,
'properties.showNoteWhenEmpty',
false
)
const initialState = {
viewID: view.id,
toggleVisible: view.properties.type !== ViewType.Markdown,
note,
showNoteWhenEmpty,
mode: note === '' ? NoteEditorMode.Adding : NoteEditorMode.Editing,
}
onOpenNoteEditor(initialState)
onEditNote(id)
}
}
const mdtp = {
onOpenNoteEditor: openNoteEditor,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(CellContext)

View File

@ -39,6 +39,7 @@ interface Props {
onDeleteCell?: (cell: Cell) => void
onPositionChange?: (cells: Cell[]) => void
onEditView: (cellID: string) => void
onEditNote: (id: string) => void
}
interface State {
@ -64,6 +65,7 @@ class Cells extends Component<Props & WithRouterProps, State> {
timeRange,
autoRefresh,
manualRefresh,
onEditNote,
} = this.props
const {rowHeight} = this.state
@ -91,6 +93,7 @@ class Cells extends Component<Props & WithRouterProps, State> {
onCloneCell={onCloneCell}
onDeleteCell={onDeleteCell}
onEditCell={this.handleEditCell(cell)}
onEditNote={onEditNote}
/>
{this.cellBorder}
</div>