Add ability to create notes on a dashboard
parent
2bd3031383
commit
759891e37f
|
@ -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
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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))
|
||||
}
|
|
@ -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>
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
|
@ -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%;
|
||||
}
|
|
@ -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))
|
|
@ -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
|
|
@ -0,0 +1,9 @@
|
|||
@import "src/style/modules";
|
||||
|
||||
.note-editor-text {
|
||||
overflow: hidden;
|
||||
|
||||
.react-codemirror2 {
|
||||
padding: 10px;
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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" />
|
||||
}
|
||||
|
||||
|
|
|
@ -33,6 +33,8 @@ const properties: GaugeView = {
|
|||
type: ViewType.Gauge,
|
||||
prefix: '',
|
||||
suffix: '',
|
||||
note: '',
|
||||
showNoteWhenEmpty: false,
|
||||
decimalPlaces: {
|
||||
digits: 10,
|
||||
isEnforced: false,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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
|
|
@ -85,7 +85,6 @@ class Cells extends Component<Props & WithRouterProps, State> {
|
|||
<CellComponent
|
||||
cell={cell}
|
||||
onZoom={onZoom}
|
||||
isEditable={true}
|
||||
autoRefresh={autoRefresh}
|
||||
manualRefresh={manualRefresh}
|
||||
timeRange={timeRange}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -92,3 +92,4 @@ div.CodeMirror-selected,
|
|||
@import 'ThemeTickscript';
|
||||
@import 'ThemeInfluxQL';
|
||||
@import 'ThemeHints';
|
||||
@import 'ThemeMarkdown';
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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: '//',
|
||||
},
|
||||
}
|
||||
|
|
|
@ -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}`,
|
||||
})
|
||||
|
|
|
@ -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: '',
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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',
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
92
ui/yarn.lock
92
ui/yarn.lock
|
@ -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
96
view.go
|
@ -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() {}
|
||||
|
||||
/////////////////////////////
|
||||
|
|
|
@ -45,7 +45,9 @@ func TestView_MarshalJSON(t *testing.T) {
|
|||
"type": "xy",
|
||||
"colors": null,
|
||||
"legend": {},
|
||||
"geom": ""
|
||||
"geom": "",
|
||||
"note": "",
|
||||
"showNoteWhenEmpty": false
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
|
Loading…
Reference in New Issue