Add ability to create notes on a dashboard

pull/10616/head
Christopher Henn 2018-11-27 17:03:13 -08:00 committed by Chris Henn
parent 2bd3031383
commit 759891e37f
40 changed files with 1246 additions and 160 deletions

View File

@ -84,7 +84,9 @@ func TestService_handleGetViews(t *testing.T) {
"type": "xy",
"colors": null,
"legend": {},
"geom": ""
"geom": "",
"note": "",
"showNoteWhenEmpty": false
}
},
{
@ -330,7 +332,9 @@ func TestService_handlePostViews(t *testing.T) {
"type": "xy",
"colors": null,
"legend": {},
"geom": ""
"geom": "",
"note": "",
"showNoteWhenEmpty": false
}
}
`,
@ -530,7 +534,9 @@ func TestService_handlePatchView(t *testing.T) {
"type": "xy",
"colors": null,
"legend": {},
"geom": ""
"geom": "",
"note": "",
"showNoteWhenEmpty": false
}
}
`,

View File

@ -140,7 +140,7 @@
"react-dnd-html5-backend": "^2.6.0",
"react-dom": "^16.3.1",
"react-grid-layout": "^0.16.6",
"react-markdown": "^3.6.0",
"react-markdown": "^4.0.3",
"react-redux": "^5.0.7",
"react-resize-detector": "^2.3.0",
"react-router": "^3.0.2",

View File

@ -0,0 +1,124 @@
// Libraries
import {get, isUndefined} from 'lodash'
// Actions
import {createCellWithView} from 'src/dashboards/actions/v2'
import {updateView} from 'src/dashboards/actions/v2/views'
// Utils
import {createView} from 'src/shared/utils/view'
// Types
import {GetState} from 'src/types/v2'
import {NoteEditorMode, MarkdownView, ViewType} from 'src/types/v2/dashboards'
import {NoteEditorState} from 'src/dashboards/reducers/v2/notes'
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: '',
},
},
})
interface CloseNoteEditorAction {
type: 'CLOSE_NOTE_EDITOR'
}
export const closeNoteEditor = (): CloseNoteEditorAction => ({
type: 'CLOSE_NOTE_EDITOR',
})
interface SetIsPreviewingAction {
type: 'SET_IS_PREVIEWING'
payload: {isPreviewing: boolean}
}
export const setIsPreviewing = (
isPreviewing: boolean
): SetIsPreviewingAction => ({
type: 'SET_IS_PREVIEWING',
payload: {isPreviewing},
})
interface ToggleShowNoteWhenEmptyAction {
type: 'TOGGLE_SHOW_NOTE_WHEN_EMPTY'
}
export const toggleShowNoteWhenEmpty = (): ToggleShowNoteWhenEmptyAction => ({
type: 'TOGGLE_SHOW_NOTE_WHEN_EMPTY',
})
interface SetNoteAction {
type: 'SET_NOTE'
payload: {note: string}
}
export const setNote = (note: string): SetNoteAction => ({
type: 'SET_NOTE',
payload: {note},
})
export const createNoteCell = (dashboardID: string) => async (
dispatch,
getState: GetState
) => {
const dashboard = getState().dashboards.find(d => d.id === dashboardID)
if (!dashboard) {
throw new Error(`could not find dashboard with id "${dashboardID}"`)
}
const {note} = getState().noteEditor
const view = createView<MarkdownView>(ViewType.Markdown)
view.properties.note = note
return dispatch(createCellWithView(dashboard, view))
}
export const updateViewNote = () => async (dispatch, getState: GetState) => {
const state = getState()
const {note, showNoteWhenEmpty, viewID} = state.noteEditor
const view: any = get(state, `views.${viewID}.view`)
if (!view) {
throw new Error(`could not find view with id "${viewID}"`)
}
if (isUndefined(view.properties.note)) {
throw new Error(
`view type "${view.properties.type}" does not support notes`
)
}
const updatedView = {
...view,
properties: {...view.properties, note, showNoteWhenEmpty},
}
return dispatch(updateView(view.links.self, updatedView))
}

View File

@ -65,6 +65,8 @@ class DashboardComponent extends PureComponent<Props> {
) : (
<DashboardEmpty dashboard={dashboard} />
)}
{/* This element is used as a portal container for note tooltips in cell headers */}
<div className="cell-header-note-tooltip-container" />
</div>
</FancyScrollbar>
)

View File

@ -1,5 +1,6 @@
// Libraries
import React, {Component} from 'react'
import {connect} from 'react-redux'
// Components
import {Page} from 'src/pageLayout'
@ -9,13 +10,16 @@ import GraphTips from 'src/shared/components/graph_tips/GraphTips'
import RenameDashboard from 'src/dashboards/components/rename_dashboard/RenameDashboard'
import {Button, ButtonShape, ComponentColor, IconFont} from 'src/clockface'
// Actions
import {addNote} from 'src/dashboards/actions/v2/notes'
// Types
import * as AppActions from 'src/types/actions/app'
import * as QueriesModels from 'src/types/queries'
import {Dashboard} from 'src/api'
import {DashboardSwitcherLinks} from 'src/types/v2/dashboards'
interface Props {
interface OwnProps {
activeDashboard: string
dashboard: Dashboard
timeRange: QueriesModels.TimeRange
@ -32,6 +36,12 @@ interface Props {
isHidden: boolean
}
interface DispatchProps {
onAddNote: typeof addNote
}
type Props = OwnProps & DispatchProps
class DashboardHeader extends Component<Props> {
public static defaultProps: Partial<Props> = {
zoomedTimeRange: {
@ -49,6 +59,7 @@ class DashboardHeader extends Component<Props> {
timeRange: {upper, lower},
zoomedTimeRange: {upper: zoomedUpper, lower: zoomedLower},
isHidden,
onAddNote,
} = this.props
return (
@ -57,6 +68,11 @@ class DashboardHeader extends Component<Props> {
<Page.Header.Right>
<GraphTips />
{this.addCellButton}
<Button
icon={IconFont.TextBlock}
text="Add Note"
onClick={onAddNote}
/>
<AutoRefreshDropdown
onChoose={handleChooseAutoRefresh}
onManualRefresh={onManualRefresh}
@ -90,10 +106,10 @@ class DashboardHeader extends Component<Props> {
if (dashboard) {
return (
<Button
shape={ButtonShape.Square}
icon={IconFont.AddCell}
color={ComponentColor.Primary}
onClick={onAddCell}
text="Add Cell"
titleText="Add cell to dashboard"
/>
)
@ -113,4 +129,11 @@ class DashboardHeader extends Component<Props> {
}
}
export default DashboardHeader
const mdtp = {
onAddNote: addNote,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(DashboardHeader)

View File

@ -13,6 +13,7 @@ import ManualRefresh from 'src/shared/components/ManualRefresh'
import VEO from 'src/dashboards/components/VEO'
import {OverlayTechnology} from 'src/clockface'
import {HoverTimeProvider} from 'src/dashboards/utils/hoverTime'
import NoteEditorContainer from 'src/dashboards/components/NoteEditorContainer'
// Actions
import * as dashboardActions from 'src/dashboards/actions/v2'
@ -222,6 +223,7 @@ class DashboardPage extends Component<Props, State> {
/>
</OverlayTechnology>
</HoverTimeProvider>
<NoteEditorContainer />
</Page>
)
}

View File

@ -0,0 +1,42 @@
@import "src/style/modules";
.note-editor {
height: 100%;
display: flex;
flex-direction: column;
.note-editor-text, .note-editor-preview {
flex: 1 1 0;
border: 2px solid $g6-smoke;
border-radius: 4px;
}
.note-editor-preview {
padding: 15px;
}
}
.note-editor--controls {
margin-bottom: 20px;
display: flex;
justify-content: space-between;
align-items: center;
&.centered {
justify-content: center;
}
}
.note-editor--toggle {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
font-weight: 600;
color: $g13-mist;
.slide-toggle {
margin-left: 10px;
margin-top: 2px;
}
}

View File

@ -0,0 +1,112 @@
// Libraries
import React, {SFC} from 'react'
import {connect} from 'react-redux'
// Components
import {Radio, SlideToggle, ComponentSize} from 'src/clockface'
import NoteEditorText from 'src/dashboards/components/NoteEditorText'
import NoteEditorPreview from 'src/dashboards/components/NoteEditorPreview'
// Actions
import {
setIsPreviewing,
toggleShowNoteWhenEmpty,
setNote,
} from 'src/dashboards/actions/v2/notes'
// Styles
import 'src/dashboards/components/NoteEditor.scss'
// Types
import {AppState} from 'src/types/v2'
interface StateProps {
note: string
isPreviewing: boolean
toggleVisible: boolean
showNoteWhenEmpty: boolean
}
interface DispatchProps {
onSetIsPreviewing: typeof setIsPreviewing
onToggleShowNoteWhenEmpty: typeof toggleShowNoteWhenEmpty
onSetNote: typeof setNote
}
interface OwnProps {}
type Props = StateProps & DispatchProps & OwnProps
const NoteEditor: SFC<Props> = props => {
const {
note,
isPreviewing,
toggleVisible,
showNoteWhenEmpty,
onSetIsPreviewing,
onToggleShowNoteWhenEmpty,
onSetNote,
} = props
return (
<div className="note-editor">
<div
className={`note-editor--controls ${toggleVisible ? '' : 'centered'}`}
>
<Radio>
<Radio.Button
value={false}
active={!isPreviewing}
onClick={onSetIsPreviewing}
>
Compose
</Radio.Button>
<Radio.Button
value={true}
active={isPreviewing}
onClick={onSetIsPreviewing}
>
Preview
</Radio.Button>
</Radio>
{toggleVisible && (
<label className="note-editor--toggle">
Show note when query returns no data
<SlideToggle
active={showNoteWhenEmpty}
size={ComponentSize.ExtraSmall}
onChange={onToggleShowNoteWhenEmpty}
/>
</label>
)}
</div>
{isPreviewing ? (
<NoteEditorPreview note={note} />
) : (
<NoteEditorText note={note} onChangeNote={onSetNote} />
)}
</div>
)
}
const mstp = (state: AppState) => {
const {
note,
isPreviewing,
toggleVisible,
showNoteWhenEmpty,
} = state.noteEditor
return {note, isPreviewing, toggleVisible, showNoteWhenEmpty}
}
const mdtp = {
onSetIsPreviewing: setIsPreviewing,
onToggleShowNoteWhenEmpty: toggleShowNoteWhenEmpty,
onSetNote: setNote,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(NoteEditor)

View File

@ -0,0 +1,10 @@
.note-editor-container .overlay--container {
height: 80%;
max-height: 600px;
display: flex;
flex-direction: column;
}
.note-editor-container .overlay--body {
height: 100%;
}

View File

@ -0,0 +1,157 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {withRouter, WithRouterProps} from 'react-router'
// Components
import NoteEditor from 'src/dashboards/components/NoteEditor'
import {
OverlayBody,
OverlayHeading,
OverlayTechnology,
OverlayContainer,
Button,
ComponentColor,
ComponentStatus,
} from 'src/clockface'
// Actions
import {
closeNoteEditor,
createNoteCell,
updateViewNote,
} from 'src/dashboards/actions/v2/notes'
import {notify} from 'src/shared/actions/notifications'
// Utils
import {savingNoteFailed} from 'src/shared/copy/v2/notifications'
// Styles
import 'src/dashboards/components/NoteEditorContainer.scss'
// Types
import {RemoteDataState} from 'src/types'
import {AppState} from 'src/types/v2'
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>
onNotify: typeof notify
}
interface OwnProps {}
type Props = StateProps & DispatchProps & OwnProps & WithRouterProps
interface State {
savingStatus: RemoteDataState
}
class NoteEditorContainer extends PureComponent<Props, State> {
public state: State = {savingStatus: RemoteDataState.NotStarted}
public render() {
const {onHide, overlayVisible} = this.props
return (
<div className="note-editor-container">
<OverlayTechnology visible={overlayVisible}>
<OverlayContainer>
<OverlayHeading title={this.overlayTitle}>
<div className="create-source-overlay--heading-buttons">
<Button text="Cancel" onClick={onHide} />
<Button
text="Save"
color={ComponentColor.Success}
status={this.saveButtonStatus}
onClick={this.handleSave}
/>
</div>
</OverlayHeading>
<OverlayBody>
<NoteEditor />
</OverlayBody>
</OverlayContainer>
</OverlayTechnology>
</div>
)
}
private get overlayTitle(): string {
const {mode} = this.props
let overlayTitle: string
if (mode === NoteEditorMode.Editing) {
overlayTitle = 'Edit Note'
} else {
overlayTitle = 'Add Note'
}
return overlayTitle
}
private get saveButtonStatus(): ComponentStatus {
const {savingStatus} = this.state
if (savingStatus === RemoteDataState.Loading) {
return ComponentStatus.Loading
}
return ComponentStatus.Default
}
private handleSave = async () => {
const {
viewID,
onCreateNoteCell,
onUpdateViewNote,
onHide,
onNotify,
} = this.props
const dashboardID = this.props.params.dashboardID
this.setState({savingStatus: RemoteDataState.Loading})
try {
if (viewID) {
await onUpdateViewNote()
} else {
await onCreateNoteCell(dashboardID)
}
this.setState({savingStatus: RemoteDataState.NotStarted}, onHide)
} catch (error) {
onNotify(savingNoteFailed(error.message))
console.error(error)
this.setState({savingStatus: RemoteDataState.Error})
}
}
}
const mstp = (state: AppState) => {
const {mode, overlayVisible, viewID} = state.noteEditor
return {mode, overlayVisible, viewID}
}
const mdtp = {
onHide: closeNoteEditor,
onNotify: notify,
onCreateNoteCell: createNoteCell as any,
onUpdateViewNote: updateViewNote as any,
}
export default connect<StateProps, DispatchProps, OwnProps>(
mstp,
mdtp
)(withRouter<StateProps & DispatchProps & OwnProps>(NoteEditorContainer))

View File

@ -0,0 +1,20 @@
import React, {SFC} from 'react'
import ReactMarkdown from 'react-markdown'
import FancyScrollbar from 'src/shared/components/fancy_scrollbar/FancyScrollbar'
interface Props {
note: string
}
const NoteEditorPreview: SFC<Props> = props => {
return (
<div className="note-editor-preview markdown-format">
<FancyScrollbar>
<ReactMarkdown source={props.note} escapeHtml={true} />
</FancyScrollbar>
</div>
)
}
export default NoteEditorPreview

View File

@ -0,0 +1,9 @@
@import "src/style/modules";
.note-editor-text {
overflow: hidden;
.react-codemirror2 {
padding: 10px;
}
}

View File

@ -0,0 +1,53 @@
// Libraries
import React, {PureComponent} from 'react'
import {Controlled as ReactCodeMirror} from 'react-codemirror2'
// Utils
import {humanizeNote} from 'src/dashboards/utils/notes'
// Styles
import 'src/dashboards/components/NoteEditorText.scss'
const OPTIONS = {
mode: 'markdown',
theme: 'markdown',
tabIndex: 1,
readonly: false,
lineNumbers: false,
autoRefresh: true,
completeSingle: false,
lineWrapping: true,
}
const noOp = () => {}
interface Props {
note: string
onChangeNote: (value: string) => void
}
class NoteEditorText extends PureComponent<Props, {}> {
public render() {
const {note} = this.props
return (
<div className="note-editor-text">
<ReactCodeMirror
autoCursor={true}
value={humanizeNote(note)}
options={OPTIONS}
onBeforeChange={this.handleChange}
onTouchStart={noOp}
/>
</div>
)
}
private handleChange = (_, __, note: string) => {
const {onChangeNote} = this.props
onChangeNote(note)
}
}
export default NoteEditorText

View File

@ -0,0 +1,66 @@
import {Action} from 'src/dashboards/actions/v2/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,
})
const noteEditorReducer = (
state: NoteEditorState = initialState(),
action: Action
) => {
switch (action.type) {
case 'OPEN_NOTE_EDITOR': {
const {initialState} = action.payload
return {
...state,
...initialState,
overlayVisible: true,
isPreviewing: false,
}
}
case 'CLOSE_NOTE_EDITOR': {
return {...state, overlayVisible: false}
}
case 'SET_IS_PREVIEWING': {
const {isPreviewing} = action.payload
return {...state, isPreviewing}
}
case 'TOGGLE_SHOW_NOTE_WHEN_EMPTY': {
const {showNoteWhenEmpty} = state
return {...state, showNoteWhenEmpty: !showNoteWhenEmpty}
}
case 'SET_NOTE': {
const {note} = action.payload
return {...state, note}
}
}
return state
}
export default noteEditorReducer

View File

@ -2,6 +2,7 @@ import {
modeFlux,
modeTickscript,
modeInfluxQL,
modeMarkdown,
} from 'src/shared/constants/codeMirrorModes'
import 'codemirror/addon/hint/show-hint'
@ -326,3 +327,4 @@ function indentFunction(states, meta) {
CodeMirror.defineSimpleMode('flux', modeFlux)
CodeMirror.defineSimpleMode('tickscript', modeTickscript)
CodeMirror.defineSimpleMode('influxQL', modeInfluxQL)
CodeMirror.defineSimpleMode('markdown', modeMarkdown)

View File

@ -3,6 +3,7 @@ import React, {PureComponent} from 'react'
// Components
import EmptyGraphMessage from 'src/shared/components/EmptyGraphMessage'
import Markdown from 'src/shared/components/views/Markdown'
// Constants
import {emptyGraphCopy} from 'src/shared/copy/cell'
@ -17,11 +18,19 @@ interface Props {
loading: RemoteDataState
tables: FluxTable[]
queries: DashboardQuery[]
fallbackNote?: string
}
export default class EmptyQueryView extends PureComponent<Props> {
public render() {
const {error, isInitialFetch, loading, tables, queries} = this.props
const {
error,
isInitialFetch,
loading,
tables,
queries,
fallbackNote,
} = this.props
if (!queries.length) {
return <EmptyGraphMessage message={emptyGraphCopy} />
@ -35,7 +44,13 @@ export default class EmptyQueryView extends PureComponent<Props> {
return <EmptyGraphMessage message="Loading..." />
}
if (!tables.some(d => !!d.data.length)) {
const hasNoResults = !tables.some(d => !!d.data.length)
if (hasNoResults && fallbackNote) {
return <Markdown text={fallbackNote} />
}
if (hasNoResults) {
return <EmptyGraphMessage message="No Results" />
}

View File

@ -33,6 +33,8 @@ const properties: GaugeView = {
type: ViewType.Gauge,
prefix: '',
suffix: '',
note: '',
showNoteWhenEmpty: false,
decimalPlaces: {
digits: 10,
isEnforced: false,

View File

@ -55,6 +55,7 @@ class RefreshingView extends PureComponent<Props> {
loading={loading}
isInitialFetch={isInitialFetch}
queries={this.queries}
fallbackNote={this.fallbackNote}
>
<QueryViewSwitcher
tables={tables}
@ -85,6 +86,12 @@ class RefreshingView extends PureComponent<Props> {
return queries
}
private get fallbackNote(): string {
const {note, showNoteWhenEmpty} = this.props.properties
return showNoteWhenEmpty ? note : null
}
}
export default RefreshingView

View File

@ -71,6 +71,23 @@ $cell--header-size: 36px;
transform: translateY(-50%);
width: 100%;
pointer-events: none;
.cell--header-note & {
margin-left: 25px;
}
}
.cell-header-note {
position: absolute;
top: 7px;
left: 10px;
z-index: 10;
cursor: default;
color: $g14-chromium;
&:hover {
color: $g20-white;
}
}
.cell--header-bar {

View File

@ -1,7 +1,7 @@
// Libraries
import React, {Component, ComponentClass} from 'react'
import {connect} from 'react-redux'
import _ from 'lodash'
import {get} from 'lodash'
// Components
import CellHeader from 'src/shared/components/cells/CellHeader'
@ -14,13 +14,13 @@ import {readView} from 'src/dashboards/actions/v2/views'
// Types
import {RemoteDataState, TimeRange} from 'src/types'
import {Cell, View, AppState} from 'src/types/v2'
import {Cell, View, AppState, ViewType} from 'src/types/v2'
// Styles
import './Cell.scss'
interface StateProps {
view: View
view: View | null
viewStatus: RemoteDataState
}
@ -37,7 +37,6 @@ interface PassedProps {
onCloneCell: (cell: Cell) => void
onEditCell: () => void
onZoom: (range: TimeRange) => void
isEditable: boolean
}
type Props = StateProps & DispatchProps & PassedProps
@ -53,35 +52,51 @@ class CellComponent extends Component<Props> {
}
public render() {
const {isEditable, onEditCell, onDeleteCell, onCloneCell, cell} = this.props
const {onEditCell, onDeleteCell, onCloneCell, cell, view} = this.props
return (
<>
<CellHeader name={this.viewName} isEditable={isEditable} />
<CellContext
visible={isEditable}
cell={cell}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
onEditCell={onEditCell}
onCSVDownload={this.handleCSVDownload}
/>
<CellHeader name={this.viewName} note={this.viewNote} />
{view && (
<CellContext
cell={cell}
view={view}
onDeleteCell={onDeleteCell}
onCloneCell={onCloneCell}
onEditCell={onEditCell}
onCSVDownload={this.handleCSVDownload}
/>
)}
<div className="cell--view">{this.view}</div>
</>
)
}
// private get queries(): DashboardQuery[] {
// const {view} = this.props
// return _.get(view, ['properties.queries'], [])
// }
private get viewName(): string {
const {view} = this.props
const viewName = view ? view.name : ''
return viewName
if (view && view.properties.type !== ViewType.Markdown) {
return view.name
}
return ''
}
private get viewNote(): string {
const {view} = this.props
if (!view) {
return ''
}
const isMarkdownView = view.properties.type === ViewType.Markdown
const showNoteWhenEmpty = get(view, 'properties.showNoteWhenEmpty')
if (isMarkdownView || showNoteWhenEmpty) {
return ''
}
return get(view, 'properties.note', '')
}
private get view(): JSX.Element {
@ -112,16 +127,7 @@ class CellComponent extends Component<Props> {
}
private handleCSVDownload = (): void => {
// TODO: get data from link
// const {cellData, cell} = this.props
// const joinedName = cell.name.split(' ').join('_')
// const {data} = timeSeriesToTableGraph(cellData)
// try {
// download(dataToCSV(data), `${joinedName}.csv`, 'text/plain')
// } catch (error) {
// notify(csvDownloadFailed())
// console.error(error)
// }
throw new Error('csv download not implemented')
}
}

View File

@ -1,53 +1,76 @@
// Libraries
import React, {PureComponent} from 'react'
import {connect} from 'react-redux'
import {get} from 'lodash'
// Components
import {Context, IconFont, ComponentColor} from 'src/clockface'
// Types
import {Cell} from 'src/types/v2'
import {ErrorHandling} from 'src/shared/decorators/errors'
interface Props {
visible: boolean
// Actions
import {openNoteEditor} from 'src/dashboards/actions/v2/notes'
// Types
import {Cell, View, ViewType} from 'src/types/v2'
import {NoteEditorMode} from 'src/types/v2/dashboards'
interface OwnProps {
cell: Cell
view: View
onDeleteCell: (cell: Cell) => void
onCloneCell: (cell: Cell) => void
onCSVDownload: () => void
onEditCell: () => void
}
interface DispatchProps {
onOpenNoteEditor: typeof openNoteEditor
}
type Props = DispatchProps & OwnProps
@ErrorHandling
class CellContext extends PureComponent<Props> {
public render() {
const {onEditCell, onCSVDownload, visible} = this.props
return (
<Context className="cell--context">
<Context.Menu icon={IconFont.Pencil}>{this.editMenuItems}</Context.Menu>
<Context.Menu
icon={IconFont.Duplicate}
color={ComponentColor.Secondary}
>
<Context.Item label="Clone" action={this.handleCloneCell} />
</Context.Menu>
<Context.Menu icon={IconFont.Trash} color={ComponentColor.Danger}>
<Context.Item label="Delete" action={this.handleDeleteCell} />
</Context.Menu>
</Context>
)
}
if (visible) {
return (
<Context className="cell--context">
<Context.Menu icon={IconFont.Pencil}>
<Context.Item label="Configure" action={onEditCell} />
<Context.Item
label="Download CSV"
action={onCSVDownload}
disabled={true}
/>
</Context.Menu>
<Context.Menu
icon={IconFont.Duplicate}
color={ComponentColor.Secondary}
>
<Context.Item label="Clone" action={this.handleCloneCell} />
</Context.Menu>
<Context.Menu icon={IconFont.Trash} color={ComponentColor.Danger}>
<Context.Item label="Delete" action={this.handleDeleteCell} />
</Context.Menu>
</Context>
)
private get editMenuItems(): JSX.Element[] | JSX.Element {
const {view, onEditCell, onCSVDownload} = this.props
if (view.properties.type === ViewType.Markdown) {
return <Context.Item label="Edit Note" action={this.handleEditNote} />
}
return null
const hasNote = !!get(view, 'properties.note')
return [
<Context.Item key="configure" label="Configure" action={onEditCell} />,
<Context.Item
key="note"
label={hasNote ? 'Edit Note' : 'Add Note'}
action={this.handleEditNote}
/>,
<Context.Item
key="download"
label="Download CSV"
action={onCSVDownload}
disabled={true}
/>,
]
}
private handleDeleteCell = () => {
@ -61,6 +84,34 @@ class CellContext extends PureComponent<Props> {
onCloneCell(cell)
}
private handleEditNote = () => {
const {onOpenNoteEditor, view} = 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)
}
}
export default CellContext
const mdtp = {
onOpenNoteEditor: openNoteEditor,
}
export default connect<{}, DispatchProps, OwnProps>(
null,
mdtp
)(CellContext)

View File

@ -1,42 +1,27 @@
// Libraries
import React, {PureComponent} from 'react'
import React, {SFC} from 'react'
import classnames from 'classnames'
import {ErrorHandling} from 'src/shared/decorators/errors'
// Components
import CellHeaderNote from 'src/shared/components/cells/CellHeaderNote'
interface Props {
name: string
isEditable: boolean
note: string
}
@ErrorHandling
class CellHeader extends PureComponent<Props> {
public render() {
const {isEditable, name} = this.props
const CellHeader: SFC<Props> = ({name, note}) => {
const className = classnames('cell--header cell--draggable', {
'cell--header-note': !!note,
})
if (isEditable) {
return (
<div className="cell--header cell--draggable">
<label className={this.cellNameClass}>{name}</label>
<div className="cell--header-bar" />
</div>
)
}
return (
<div className="cell--header">
<label className="cell--name">{name}</label>
</div>
)
}
private get cellNameClass(): string {
const {name} = this.props
const isNameBlank = !!name.trim()
return classnames('cell--name', {'cell--name__blank': isNameBlank})
}
return (
<div className={className}>
<label className="cell--name">{name}</label>
<div className="cell--header-bar" />
{note && <CellHeaderNote note={note} />}
</div>
)
}
export default CellHeader

View File

@ -0,0 +1,79 @@
// Libraries
import React, {PureComponent, CSSProperties} from 'react'
// Components
import CellHeaderNoteTooltip from 'src/shared/components/cells/CellHeaderNoteTooltip'
const MAX_TOOLTIP_WIDTH = 400
const MAX_TOOLTIP_HEIGHT = 200
interface Props {
note: string
}
interface State {
isShowingTooltip: boolean
domRect?: DOMRect
}
class CellHeaderNote extends PureComponent<Props, State> {
public state: State = {isShowingTooltip: false}
public render() {
const {note} = this.props
const {isShowingTooltip} = this.state
return (
<div
className="cell-header-note"
onMouseEnter={this.handleMouseEnter}
onMouseLeave={this.handleMouseLeave}
>
<span className="icon chat" />
{isShowingTooltip && (
<CellHeaderNoteTooltip
note={note}
containerStyle={this.tooltipStyle}
maxWidth={MAX_TOOLTIP_WIDTH}
maxHeight={MAX_TOOLTIP_HEIGHT}
/>
)}
</div>
)
}
private get tooltipStyle(): CSSProperties {
const {x, y, width, height} = this.state.domRect
const overflowsBottom = y + MAX_TOOLTIP_HEIGHT > window.innerHeight
const overflowsRight = x + MAX_TOOLTIP_WIDTH > window.innerWidth
const style: CSSProperties = {}
if (overflowsBottom) {
style.bottom = `${window.innerHeight - y - height}px`
} else {
style.top = `${y}px`
}
if (overflowsRight) {
style.right = `${window.innerWidth - x}px`
} else {
style.left = `${x + width}px`
}
return style
}
private handleMouseEnter = e => {
this.setState({
isShowingTooltip: true,
domRect: e.target.getBoundingClientRect(),
})
}
private handleMouseLeave = () => {
this.setState({isShowingTooltip: false})
}
}
export default CellHeaderNote

View File

@ -0,0 +1,15 @@
@import "src/style/modules";
.cell-header-note-tooltip {
position: fixed;
z-index: 3;
padding: 0 5px;
}
.cell-header-note-tooltip--content {
padding: 10px 15px;
border-radius: 4px;
@include gradient-v($g0-obsidian, $g1-raven);
font-size: 13px;
overflow: scroll;
}

View File

@ -0,0 +1,41 @@
// Libraries
import React, {SFC, CSSProperties} from 'react'
import {createPortal} from 'react-dom'
import ReactMarkdown from 'react-markdown'
// Styles
import 'src/shared/components/cells/CellHeaderNoteTooltip.scss'
interface Props {
note: string
containerStyle: CSSProperties
maxWidth: number
maxHeight: number
}
const CellHeaderNoteTooltip: SFC<Props> = props => {
const {note, containerStyle, maxWidth, maxHeight} = props
const style = {
maxWidth: `${maxWidth}px`,
maxHeight: `${maxHeight}px`,
}
const content = (
<div className="cell-header-note-tooltip" style={containerStyle}>
<div
className="cell-header-note-tooltip--content markdown-format"
style={style}
>
<ReactMarkdown source={note} />
</div>
</div>
)
return createPortal(
content,
document.querySelector('.cell-header-note-tooltip-container')
)
}
export default CellHeaderNoteTooltip

View File

@ -85,7 +85,6 @@ class Cells extends Component<Props & WithRouterProps, State> {
<CellComponent
cell={cell}
onZoom={onZoom}
isEditable={true}
autoRefresh={autoRefresh}
manualRefresh={manualRefresh}
timeRange={timeRange}

View File

@ -5,9 +5,6 @@ import React, {Component} from 'react'
import Markdown from 'src/shared/components/views/Markdown'
import RefreshingView from 'src/shared/components/RefreshingView'
// Constants
import {text} from 'src/shared/components/views/gettingsStarted'
// Types
import {TimeRange} from 'src/types'
import {View, ViewType, ViewShape} from 'src/types/v2'
@ -37,7 +34,7 @@ class ViewComponent extends Component<Props> {
case ViewType.LogViewer:
return this.emptyGraph
case ViewType.Markdown:
return <Markdown text={text} />
return <Markdown text={view.properties.note} />
default:
return (
<RefreshingView

View File

@ -92,3 +92,4 @@ div.CodeMirror-selected,
@import 'ThemeTickscript';
@import 'ThemeInfluxQL';
@import 'ThemeHints';
@import 'ThemeMarkdown';

View File

@ -0,0 +1,59 @@
/*
CodeMirror "Markdown" Theme
------------------------------------------------------------------------------
Intended for use with the Markdown CodeMirror Mode
*/
.cm-s-markdown {
color: $g15-platinum;
&:hover {
cursor: text;
}
.cm-italic {
font-style: italic;
}
.cm-bold {
font-weight: 900;
}
.cm-strikethrough {
text-decoration: none;
opacity: 0.5;
}
.cm-heading {
color: $c-honeydew;
}
.cm-image {
color: $c-comet;
}
.cm-link {
color: $c-pool;
text-decoration: none;
}
.cm-blockquote {
color: $g18-cloud;
background-color: $g3-castle;
}
.CodeMirror-scrollbar-filler {
background-color: transparent;
}
.CodeMirror-vscrollbar,
.CodeMirror-hscrollbar {
@include custom-scrollbar-round($g2-kevlar, $c-pool);
}
}
.cm-s-markdown.CodeMirror-empty {
font-style: italic;
color: $g9-mountain;
}

View File

@ -4,10 +4,7 @@
*/
.markdown-cell {
position: absolute !important;
height: calc(
100% - 20px
) !important; // Prevent it from covering the cell resizer
height: calc(100% - 20px) !important;
}
.markdown-cell--contents {
@ -26,7 +23,6 @@
position: absolute;
bottom: 0;
left: 0;
height: 20px;
width: 100%;
@include gradient-v(rgba($g3-castle, 0), $g3-castle);
z-index: 1;

View File

@ -1,5 +1,5 @@
// Libraries
import React, {Component} from 'react'
import React, {PureComponent} from 'react'
import ReactMarkdown from 'react-markdown'
// Components
@ -16,7 +16,7 @@ interface Props {
}
@ErrorHandling
class Markdown extends Component<Props> {
class Markdown extends PureComponent<Props> {
public render() {
const {text} = this.props

View File

@ -251,3 +251,51 @@ export const modeInfluxQL = {
lineComment: '//',
},
}
export const modeMarkdown = {
start: [
{
regex: /[*](\s|\w)+[*]/,
token: 'italic',
},
{
regex: /[*][*](\s|\w)+[*][*]/,
token: 'bold',
},
{
regex: /[~][~](\s|\w)+[~][~]/,
token: 'strikethrough',
},
{
regex: /\#+\s.+(?=$)/gm,
token: 'heading',
},
{
regex: /\>.+(?=$)/gm,
token: 'blockquote',
},
{
regex: /\[.+\]\(.+\)/,
token: 'link',
},
{
regex: /[!]\[.+\]\(.+\)/,
token: 'image',
},
],
comment: [
{
regex: /.*?\*\//,
token: 'comment',
next: 'start',
},
{
regex: /.*/,
token: 'comment',
},
],
meta: {
dontIndentStates: ['comment'],
lineComment: '//',
},
}

View File

@ -56,3 +56,9 @@ export const getTelegrafConfigFailed = (): Notification => ({
...defaultErrorNotification,
message: 'Failed to get telegraf config',
})
export const savingNoteFailed = (error): Notification => ({
...defaultErrorNotification,
duration: FIVE_SECONDS,
message: `Failed to save note: ${error}`,
})

View File

@ -37,6 +37,8 @@ function defaultLineViewProperties() {
queries: defaultViewQueries(),
colors: [],
legend: {},
note: '',
showNoteWhenEmpty: false,
axes: {
x: {
bounds: ['', ''] as [string, string],
@ -72,6 +74,8 @@ function defaultGaugeViewProperties() {
colors: DEFAULT_GAUGE_COLORS,
prefix: '',
suffix: '',
note: '',
showNoteWhenEmpty: false,
decimalPlaces: {
isEnforced: true,
digits: 2,
@ -137,6 +141,8 @@ const NEW_VIEW_CREATORS = {
digits: 2,
},
timeFormat: 'YYYY-MM-DD HH:mm:ss',
note: '',
showNoteWhenEmpty: false,
},
}),
[ViewType.Markdown]: (): NewView<MarkdownView> => ({
@ -144,7 +150,7 @@ const NEW_VIEW_CREATORS = {
properties: {
type: ViewType.Markdown,
shape: ViewShape.ChronografV2,
text: '',
note: '',
},
}),
}

View File

@ -20,6 +20,7 @@ import logsReducer from 'src/logs/reducers'
import timeMachinesReducer from 'src/shared/reducers/v2/timeMachines'
import orgsReducer from 'src/organizations/reducers/orgs'
import onboardingReducer from 'src/onboarding/reducers/'
import noteEditorReducer from 'src/dashboards/reducers/v2/notes'
// Types
import {LocalStorage} from 'src/types/localStorage'
@ -43,6 +44,7 @@ const rootReducer = combineReducers<ReducerState>({
orgs: orgsReducer,
me: meReducer,
onboarding: onboardingReducer,
noteEditor: noteEditorReducer,
})
const composeEnhancers =

View File

@ -63,11 +63,6 @@ export interface DecimalPlaces {
digits: number
}
export interface MarkDownProperties {
type: ViewType.Markdown
text: string
}
export interface View<T extends ViewProperties = ViewProperties> {
id: string
name: string
@ -121,6 +116,8 @@ export interface XYView {
axes: Axes
colors: Color[]
legend: Legend
note: string
showNoteWhenEmpty: boolean
}
export interface LinePlusSingleStatView {
@ -133,6 +130,8 @@ export interface LinePlusSingleStatView {
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
note: string
showNoteWhenEmpty: boolean
}
export interface SingleStatView {
@ -143,6 +142,8 @@ export interface SingleStatView {
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
note: string
showNoteWhenEmpty: boolean
}
export interface GaugeView {
@ -153,6 +154,8 @@ export interface GaugeView {
prefix: string
suffix: string
decimalPlaces: DecimalPlaces
note: string
showNoteWhenEmpty: boolean
}
export interface TableView {
@ -164,12 +167,14 @@ export interface TableView {
fieldOptions: FieldOption[]
decimalPlaces: DecimalPlaces
timeFormat: string
note: string
showNoteWhenEmpty: boolean
}
export interface MarkdownView {
type: ViewType.Markdown
shape: ViewShape.ChronografV2
text: string
note: string
}
export interface LogViewerView {
@ -250,3 +255,8 @@ export interface DashboardSwitcherLinks {
export interface ViewParams {
type: ViewType
}
export enum NoteEditorMode {
Adding = 'adding',
Editing = 'editing',
}

View File

@ -31,6 +31,7 @@ import {MeState} from 'src/shared/reducers/v2/me'
import {OverlayState} from 'src/types/v2/overlay'
import {SourcesState} from 'src/sources/reducers'
import {OnboardingState} from 'src/onboarding/reducers'
import {NoteEditorState} from 'src/dashboards/reducers/v2/notes'
export interface AppState {
VERSION: string
@ -49,6 +50,7 @@ export interface AppState {
orgs: Organization[]
me: MeState
onboarding: OnboardingState
noteEditor: NoteEditorState
}
export type GetState = () => AppState

View File

@ -1019,6 +1019,11 @@ argparse@^1.0.7:
dependencies:
sprintf-js "~1.0.2"
arity-n@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/arity-n/-/arity-n-1.0.4.tgz#d9e76b11733e08569c0847ae7b39b2860b30b745"
integrity sha1-2edrEXM+CFacCEeuezmyhgswt0U=
arr-diff@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-2.0.0.tgz#8f3b827f955a8bd669697e4a4256ac3ceae356cf"
@ -1802,6 +1807,11 @@ cheerio@^1.0.0-rc.2:
lodash "^4.15.0"
parse5 "^3.0.1"
chickencurry@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/chickencurry/-/chickencurry-1.1.1.tgz#02655f2b26b3bc2ee1ae1e5316886de38eb79738"
integrity sha1-AmVfKyazvC7hrh5TFoht4463lzg=
chokidar@^2.0.3:
version "2.0.4"
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.0.4.tgz#356ff4e2b0e8e43e322d18a372460bbcf3accd26"
@ -2056,6 +2066,13 @@ component-emitter@^1.2.1:
resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6"
integrity sha1-E3kY1teCg/ffemt8WmPhQOaUJeY=
compose-function@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/compose-function/-/compose-function-2.0.0.tgz#e642fa7e1da21529720031476776fc24691ac0b0"
integrity sha1-5kL6fh2iFSlyADFHZ3b8JGkawLA=
dependencies:
arity-n "^1.0.4"
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -3859,6 +3876,17 @@ html-encoding-sniffer@^1.0.1, html-encoding-sniffer@^1.0.2:
dependencies:
whatwg-encoding "^1.0.1"
html-to-react@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/html-to-react/-/html-to-react-1.3.3.tgz#e41666a735f9997ed2372dcd21d8b0e42b334467"
integrity sha512-4Qi5/t8oBr6c1t1kBJKyxEeJu0lb7ctvq29oFZioiUHH0Wz88VWGwoXuH26HDt9v64bDHA4NMPNTH8bVrcaJWA==
dependencies:
domhandler "^2.3.0"
escape-string-regexp "^1.0.5"
htmlparser2 "^3.8.3"
ramda "^0.25.0"
underscore.string.fp "^1.0.4"
htmlnano@^0.1.9:
version "0.1.10"
resolved "https://registry.yarnpkg.com/htmlnano/-/htmlnano-0.1.10.tgz#a0a548eb4c76ae2cf2423ec7a25c881734d3dea6"
@ -3871,6 +3899,18 @@ htmlnano@^0.1.9:
svgo "^1.0.5"
terser "^3.8.1"
htmlparser2@^3.8.3:
version "3.10.0"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.10.0.tgz#5f5e422dcf6119c0d983ed36260ce9ded0bee464"
integrity sha512-J1nEUGv+MkXS0weHNWVKJJ+UrLfePxRWpN3C9bEi9fLxL2+ggW94DQvgYVXsaT30PGwYRIZKNZXuyMhp3Di4bQ==
dependencies:
domelementtype "^1.3.0"
domhandler "^2.3.0"
domutils "^1.5.1"
entities "^1.1.1"
inherits "^2.0.1"
readable-stream "^3.0.6"
htmlparser2@^3.9.0, htmlparser2@^3.9.1, htmlparser2@^3.9.2:
version "3.9.2"
resolved "https://registry.yarnpkg.com/htmlparser2/-/htmlparser2-3.9.2.tgz#1bdf87acca0f3f9e53fa4fcceb0f4b4cbb00b338"
@ -7065,6 +7105,11 @@ railroad-diagrams@^1.0.0:
resolved "https://registry.yarnpkg.com/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz#eb7e6267548ddedfb899c1b90e57374559cddb7e"
integrity sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=
ramda@^0.25.0:
version "0.25.0"
resolved "https://registry.yarnpkg.com/ramda/-/ramda-0.25.0.tgz#8fdf68231cffa90bc2f9460390a0cb74a29b29a9"
integrity sha512-GXpfrYVPwx3K7RQ6aYT8KPS8XViSXUVJT1ONhoKPE9VAleW42YE+U+8VEyGWt41EnEQW7gwecYJriTI0pKoecQ==
randexp@0.4.6:
version "0.4.6"
resolved "https://registry.yarnpkg.com/randexp/-/randexp-0.4.6.tgz#e986ad5e5e31dae13ddd6f7b3019aa7c87f60ca3"
@ -7221,11 +7266,12 @@ react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362"
integrity sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==
react-markdown@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-3.6.0.tgz#29f6aaab5270c8ef0a5e234093a873ec3e01722b"
integrity sha512-TV0wQDHHPCEeKJHWXFfEAKJ8uSEsJ9LgrMERkXx05WV/3q6Ig+59KDNaTmjcoqlCpE/sH5PqqLMh4t0QWKrJ8Q==
react-markdown@^4.0.3:
version "4.0.3"
resolved "https://registry.yarnpkg.com/react-markdown/-/react-markdown-4.0.3.tgz#8851f9265d0322bb5d60ab2766b3ab48cdbeb890"
integrity sha512-CIc3eLVpW5XocM1MCid2rS0vs9skhvdL/slAkY/a3Cr9y72b0J/25GiD70fGmStjuxsd5ROdm4ZYfiYYxPPyGA==
dependencies:
html-to-react "^1.3.3"
mdast-add-list-metadata "1.0.1"
prop-types "^15.6.1"
remark-parse "^5.0.0"
@ -7352,6 +7398,15 @@ readable-stream@^2.0.1, readable-stream@^2.0.2, readable-stream@^2.0.6, readable
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
readable-stream@^3.0.6:
version "3.0.6"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.0.6.tgz#351302e4c68b5abd6a2ed55376a7f9a25be3057a"
integrity sha512-9E1oLoOWfhSXHGv6QlwXJim7uNzd9EVlWK+21tCU9Ju/kR0/p2AZYPz4qSchgO8PlLIH4FpZYfzwS+rEksZjIg==
dependencies:
inherits "^2.0.3"
string_decoder "^1.1.1"
util-deprecate "^1.0.1"
readdirp@^2.0.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-2.2.1.tgz#0e87622a3325aa33e892285caf8b4e846529a525"
@ -7705,6 +7760,11 @@ ret@~0.1.10:
resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc"
integrity sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==
reverse-arguments@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/reverse-arguments/-/reverse-arguments-1.0.0.tgz#c28095a3a921ac715d61834ddece9027992667cd"
integrity sha1-woCVo6khrHFdYYNN3s6QJ5kmZ80=
rgb-regex@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/rgb-regex/-/rgb-regex-1.0.1.tgz#c0e0d6882df0e23be254a475e8edd41915feaeb1"
@ -8272,6 +8332,13 @@ string_decoder@^1.0.0, string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
string_decoder@^1.1.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.2.0.tgz#fe86e738b19544afe70469243b2a1ee9240eae8d"
integrity sha512-6YqyX6ZWEYguAxgZzHGL7SsCeGx3V2TtOTqZz1xSTSWnqsbWwbptafNyvf/ACquZUXV3DANr5BDIwNYe1mN42w==
dependencies:
safe-buffer "~5.1.0"
strip-ansi@^3.0.0, strip-ansi@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-3.0.1.tgz#6a385fb8853d952d5ff05d0e8aaf94278dc63dcf"
@ -8727,6 +8794,21 @@ uglify-js@^3.1.4:
commander "~2.17.1"
source-map "~0.6.1"
underscore.string.fp@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/underscore.string.fp/-/underscore.string.fp-1.0.4.tgz#054b3f1843bcae561286c87de5e8879b4fc98364"
integrity sha1-BUs/GEO8rlYShsh95eiHm0/Jg2Q=
dependencies:
chickencurry "1.1.1"
compose-function "^2.0.0"
reverse-arguments "1.0.0"
underscore.string "3.0.3"
underscore.string@3.0.3:
version "3.0.3"
resolved "https://registry.yarnpkg.com/underscore.string/-/underscore.string-3.0.3.tgz#4617b8c1a250cf6e5064fbbb363d0fa96cf14552"
integrity sha1-Rhe4waJQz25QZPu7Nj0PqWzxRVI=
underscore@~1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.4.4.tgz#61a6a32010622afa07963bf325203cf12239d604"
@ -8880,7 +8962,7 @@ use@^3.1.0:
resolved "https://registry.yarnpkg.com/use/-/use-3.1.1.tgz#d50c8cac79a19fbc20f2911f56eb973f4e10070f"
integrity sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==
util-deprecate@~1.0.1:
util-deprecate@^1.0.1, util-deprecate@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=

96
view.go
View File

@ -129,6 +129,12 @@ func UnmarshalViewPropertiesJSON(b []byte) (ViewProperties, error) {
return nil, err
}
vis = tv
case "markdown":
var mv MarkdownViewProperties
if err := json.Unmarshal(v.B, &mv); err != nil {
return nil, err
}
vis = mv
case "log-viewer": // happens in log viewer stays in log viewer.
var lv LogViewProperties
if err := json.Unmarshal(v.B, &lv); err != nil {
@ -199,6 +205,14 @@ func MarshalViewPropertiesJSON(v ViewProperties) ([]byte, error) {
Shape: "chronograf-v2",
LinePlusSingleStatProperties: vis,
}
case MarkdownViewProperties:
s = struct {
Shape string `json:"shape"`
MarkdownViewProperties
}{
Shape: "chronograf-v2",
MarkdownViewProperties: vis,
}
case LogViewProperties:
s = struct {
Shape string `json:"shape"`
@ -281,55 +295,70 @@ func (u ViewUpdate) MarshalJSON() ([]byte, error) {
// LinePlusSingleStatProperties represents options for line plus single stat view in Chronograf
type LinePlusSingleStatProperties struct {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
ViewColors []ViewColor `json:"colors"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
ViewColors []ViewColor `json:"colors"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Note string `json:"note"`
ShowNoteWhenEmpty bool `json:"showNoteWhenEmpty"`
}
// XYViewProperties represents options for line, bar, step, or stacked view in Chronograf
type XYViewProperties struct {
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
Geom string `json:"geom"` // Either "line", "step", "stacked", or "bar"
ViewColors []ViewColor `json:"colors"`
Queries []DashboardQuery `json:"queries"`
Axes map[string]Axis `json:"axes"`
Type string `json:"type"`
Legend Legend `json:"legend"`
Geom string `json:"geom"` // Either "line", "step", "stacked", or "bar"
ViewColors []ViewColor `json:"colors"`
Note string `json:"note"`
ShowNoteWhenEmpty bool `json:"showNoteWhenEmpty"`
}
// SingleStatViewProperties represents options for single stat view in Chronograf
type SingleStatViewProperties struct {
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Note string `json:"note"`
ShowNoteWhenEmpty bool `json:"showNoteWhenEmpty"`
}
// GaugeViewProperties represents options for gauge view in Chronograf
type GaugeViewProperties struct {
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
Prefix string `json:"prefix"`
Suffix string `json:"suffix"`
ViewColors []ViewColor `json:"colors"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Note string `json:"note"`
ShowNoteWhenEmpty bool `json:"showNoteWhenEmpty"`
}
// TableViewProperties represents options for table view in Chronograf
type TableViewProperties struct {
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
ViewColors []ViewColor `json:"colors"`
TableOptions TableOptions `json:"tableOptions"`
FieldOptions []RenamableField `json:"fieldOptions"`
TimeFormat string `json:"timeFormat"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Type string `json:"type"`
Queries []DashboardQuery `json:"queries"`
ViewColors []ViewColor `json:"colors"`
TableOptions TableOptions `json:"tableOptions"`
FieldOptions []RenamableField `json:"fieldOptions"`
TimeFormat string `json:"timeFormat"`
DecimalPlaces DecimalPlaces `json:"decimalPlaces"`
Note string `json:"note"`
ShowNoteWhenEmpty bool `json:"showNoteWhenEmpty"`
}
type MarkdownViewProperties struct {
Type string `json:"type"`
Note string `json:"note"`
}
// LogViewProperties represents options for log viewer in Chronograf.
@ -357,6 +386,7 @@ func (LinePlusSingleStatProperties) viewProperties() {}
func (SingleStatViewProperties) viewProperties() {}
func (GaugeViewProperties) viewProperties() {}
func (TableViewProperties) viewProperties() {}
func (MarkdownViewProperties) viewProperties() {}
func (LogViewProperties) viewProperties() {}
/////////////////////////////

View File

@ -45,7 +45,9 @@ func TestView_MarshalJSON(t *testing.T) {
"type": "xy",
"colors": null,
"legend": {},
"geom": ""
"geom": "",
"note": "",
"showNoteWhenEmpty": false
}
}
`,