Merge pull request #2746 from influxdata/saving-tickscript

Saving tickscript
pull/10616/head
Alex Paxton 2018-02-02 11:37:31 -08:00 committed by GitHub
commit 44339a5b25
11 changed files with 346 additions and 86 deletions

View File

@ -1,11 +1,12 @@
## v1.4.1.0 [unreleased] ## v1.4.1.0 [unreleased]
### Features ### Features
1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule 1. [#2409](https://github.com/influxdata/chronograf/pull/2409): Allow adding multiple event handlers to a rule
1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations" 1. [#2709](https://github.com/influxdata/chronograf/pull/2709): Add "send test alert" button to test kapacitor alert configurations
1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to specified kapacitor config panel from rule builder alert handlers 1. [#2708](https://github.com/influxdata/chronograf/pull/2708): Link to specified kapacitor config panel from rule builder alert handlers
1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page 1. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
### UI Improvements ### UI Improvements
1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections 1. [#2698](https://github.com/influxdata/chronograf/pull/2698): Improve clarity of terminology surrounding InfluxDB & Kapacitor connections
1. [#2746](https://github.com/influxdata/chronograf/pull/2746): Separate saving TICKscript from exiting editor page
### Bug Fixes ### Bug Fixes
1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected 1. [#2684](https://github.com/influxdata/chronograf/pull/2684): Fix TICKscript Sensu alerts when no group by tags selected
1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file 1. [#2735](https://github.com/influxdata/chronograf/pull/2735): Remove cli options from systemd service file

View File

@ -195,16 +195,10 @@ export const updateRuleStatus = (rule, status) => dispatch => {
}) })
} }
export const createTask = ( export const createTask = (kapacitor, task) => async dispatch => {
kapacitor,
task,
router,
sourceID
) => async dispatch => {
try { try {
const {data} = await createTaskAJAX(kapacitor, task) const {data} = await createTaskAJAX(kapacitor, task)
router.push(`/sources/${sourceID}/alert-rules`) dispatch(publishNotification('success', 'TICKscript successfully created'))
dispatch(publishNotification('success', 'You made a TICKscript!'))
return data return data
} catch (error) { } catch (error) {
if (!error) { if (!error) {
@ -220,20 +214,17 @@ export const updateTask = (
kapacitor, kapacitor,
task, task,
ruleID, ruleID,
router,
sourceID sourceID
) => async dispatch => { ) => async dispatch => {
try { try {
const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID) const {data} = await updateTaskAJAX(kapacitor, task, ruleID, sourceID)
router.push(`/sources/${sourceID}/alert-rules`) dispatch(publishNotification('success', 'TICKscript saved'))
dispatch(publishNotification('success', 'TICKscript updated successully'))
return data return data
} catch (error) { } catch (error) {
if (!error) { if (!error) {
dispatch(errorThrown('Could not communicate with server')) dispatch(errorThrown('Could not communicate with server'))
return return
} }
return error.data return error.data
} }
} }

View File

@ -1,16 +1,16 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
const LogsToggle = ({areLogsVisible, onToggleLogsVisbility}) => const LogsToggle = ({areLogsVisible, onToggleLogsVisibility}) =>
<ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle"> <ul className="nav nav-tablist nav-tablist-sm nav-tablist-malachite logs-toggle">
<li <li
className={areLogsVisible ? null : 'active'} className={areLogsVisible ? null : 'active'}
onClick={onToggleLogsVisbility} onClick={onToggleLogsVisibility}
> >
Editor Editor
</li> </li>
<li <li
className={areLogsVisible ? 'active' : null} className={areLogsVisible ? 'active' : null}
onClick={onToggleLogsVisbility} onClick={onToggleLogsVisibility}
> >
Editor + Logs Editor + Logs
</li> </li>
@ -20,7 +20,7 @@ const {bool, func} = PropTypes
LogsToggle.propTypes = { LogsToggle.propTypes = {
areLogsVisible: bool, areLogsVisible: bool,
onToggleLogsVisbility: func.isRequired, onToggleLogsVisibility: func.isRequired,
} }
export default LogsToggle export default LogsToggle

View File

@ -8,25 +8,29 @@ import LogsTable from 'src/kapacitor/components/LogsTable'
const Tickscript = ({ const Tickscript = ({
onSave, onSave,
onExit,
task, task,
logs, logs,
validation, consoleMessage,
onSelectDbrps, onSelectDbrps,
onChangeScript, onChangeScript,
onChangeType, onChangeType,
onChangeID, onChangeID,
unsavedChanges,
isNewTickscript, isNewTickscript,
areLogsVisible, areLogsVisible,
areLogsEnabled, areLogsEnabled,
onToggleLogsVisbility, onToggleLogsVisibility,
}) => }) =>
<div className="page"> <div className="page">
<TickscriptHeader <TickscriptHeader
task={task} task={task}
onSave={onSave} onSave={onSave}
onExit={onExit}
unsavedChanges={unsavedChanges}
areLogsVisible={areLogsVisible} areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled} areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility} onToggleLogsVisibility={onToggleLogsVisibility}
isNewTickscript={isNewTickscript} isNewTickscript={isNewTickscript}
/> />
<div className="page-contents--split"> <div className="page-contents--split">
@ -38,11 +42,14 @@ const Tickscript = ({
onChangeID={onChangeID} onChangeID={onChangeID}
task={task} task={task}
/> />
<TickscriptEditorConsole validation={validation} />
<TickscriptEditor <TickscriptEditor
script={task.tickscript} script={task.tickscript}
onChangeScript={onChangeScript} onChangeScript={onChangeScript}
/> />
<TickscriptEditorConsole
consoleMessage={consoleMessage}
unsavedChanges={unsavedChanges}
/>
</div> </div>
{areLogsVisible ? <LogsTable logs={logs} /> : null} {areLogsVisible ? <LogsTable logs={logs} /> : null}
</div> </div>
@ -53,12 +60,13 @@ const {arrayOf, bool, func, shape, string} = PropTypes
Tickscript.propTypes = { Tickscript.propTypes = {
logs: arrayOf(shape()).isRequired, logs: arrayOf(shape()).isRequired,
onSave: func.isRequired, onSave: func.isRequired,
onExit: func.isRequired,
source: shape({ source: shape({
id: string, id: string,
}), }),
areLogsVisible: bool, areLogsVisible: bool,
areLogsEnabled: bool, areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired, onToggleLogsVisibility: func.isRequired,
task: shape({ task: shape({
id: string, id: string,
script: string, script: string,
@ -66,10 +74,11 @@ Tickscript.propTypes = {
}).isRequired, }).isRequired,
onChangeScript: func.isRequired, onChangeScript: func.isRequired,
onSelectDbrps: func.isRequired, onSelectDbrps: func.isRequired,
validation: string, consoleMessage: string,
onChangeType: func.isRequired, onChangeType: func.isRequired,
onChangeID: func.isRequired, onChangeID: func.isRequired,
isNewTickscript: bool.isRequired, isNewTickscript: bool.isRequired,
unsavedChanges: bool,
} }
export default Tickscript export default Tickscript

View File

@ -1,22 +1,31 @@
import React, {PropTypes} from 'react' import React, {PropTypes} from 'react'
const TickscriptEditorConsole = ({validation}) => const TickscriptEditorConsole = ({consoleMessage, unsavedChanges}) => {
<div className="tickscript-console"> let consoleOutput = 'TICKscript is valid'
<div className="tickscript-console--output"> let consoleClass = 'tickscript-console--valid'
{validation
? <p>
{validation}
</p>
: <p className="tickscript-console--default">
Save your TICKscript to validate it
</p>}
</div>
</div>
const {string} = PropTypes if (consoleMessage) {
consoleOutput = consoleMessage
consoleClass = 'tickscript-console--error'
} else if (unsavedChanges) {
consoleOutput = 'You have unsaved changes, save to validate TICKscript'
consoleClass = 'tickscript-console--default'
}
return (
<div className="tickscript-console">
<p className={consoleClass}>
{consoleOutput}
</p>
</div>
)
}
const {bool, string} = PropTypes
TickscriptEditorConsole.propTypes = { TickscriptEditorConsole.propTypes = {
validation: string, consoleMessage: string,
unsavedChanges: bool,
} }
export default TickscriptEditorConsole export default TickscriptEditorConsole

View File

@ -2,14 +2,17 @@ import React, {PropTypes} from 'react'
import SourceIndicator from 'shared/components/SourceIndicator' import SourceIndicator from 'shared/components/SourceIndicator'
import LogsToggle from 'src/kapacitor/components/LogsToggle' import LogsToggle from 'src/kapacitor/components/LogsToggle'
import ConfirmButton from 'src/shared/components/ConfirmButton'
const TickscriptHeader = ({ const TickscriptHeader = ({
task: {id}, task: {id},
onSave, onSave,
onExit,
unsavedChanges,
areLogsVisible, areLogsVisible,
areLogsEnabled, areLogsEnabled,
isNewTickscript, isNewTickscript,
onToggleLogsVisbility, onToggleLogsVisibility,
}) => }) =>
<div className="page-header full-width"> <div className="page-header full-width">
<div className="page-header__container"> <div className="page-header__container">
@ -20,18 +23,40 @@ const TickscriptHeader = ({
<LogsToggle <LogsToggle
areLogsVisible={areLogsVisible} areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled} areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility} onToggleLogsVisibility={onToggleLogsVisibility}
/>} />}
<div className="page-header__right"> <div className="page-header__right">
<SourceIndicator /> <SourceIndicator />
<button {isNewTickscript
className="btn btn-success btn-sm" ? <button
title={id ? '' : 'ID your TICKscript to save'} className="btn btn-success btn-sm"
onClick={onSave} title="Name your TICKscript to save"
disabled={!id} onClick={onSave}
> disabled={!id}
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'} >
</button> Save New TICKscript
</button>
: <button
className="btn btn-success btn-sm"
title="You have unsaved changes"
onClick={onSave}
disabled={!unsavedChanges}
>
Save Changes
</button>}
{unsavedChanges
? <ConfirmButton
text="Exit"
confirmText="Discard unsaved changes?"
confirmAction={onExit}
/>
: <button
className="btn btn-default btn-sm"
title="Return to Alert Rules"
onClick={onExit}
>
Exit
</button>}
</div> </div>
</div> </div>
</div> </div>
@ -41,9 +66,10 @@ const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptHeader.propTypes = { TickscriptHeader.propTypes = {
isNewTickscript: bool, isNewTickscript: bool,
onSave: func, onSave: func,
onExit: func.isRequired,
areLogsVisible: bool, areLogsVisible: bool,
areLogsEnabled: bool, areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired, onToggleLogsVisibility: func.isRequired,
task: shape({ task: shape({
dbrps: arrayOf( dbrps: arrayOf(
shape({ shape({
@ -52,6 +78,7 @@ TickscriptHeader.propTypes = {
}) })
), ),
}), }),
unsavedChanges: bool,
} }
export default TickscriptHeader export default TickscriptHeader

View File

@ -24,11 +24,12 @@ class TickscriptPage extends Component {
dbrps: [], dbrps: [],
type: 'stream', type: 'stream',
}, },
validation: '', consoleMessage: '',
isEditingID: true, isEditingID: true,
logs: [], logs: [],
areLogsEnabled: false, areLogsEnabled: false,
failStr: '', failStr: '',
unsavedChanges: false,
} }
} }
@ -172,9 +173,10 @@ class TickscriptPage extends Component {
} else { } else {
response = await createTask(kapacitor, task, router, sourceID) response = await createTask(kapacitor, task, router, sourceID)
} }
if (response.code) {
if (response && response.code === 500) { this.setState({unsavedChanges: true, consoleMessage: response.message})
return this.setState({validation: response.message}) } else {
this.setState({unsavedChanges: false, consoleMessage: ''})
} }
} catch (error) { } catch (error) {
console.error(error) console.error(error)
@ -182,37 +184,57 @@ class TickscriptPage extends Component {
} }
} }
handleExit = () => {
const {source: {id: sourceID}, router} = this.props
return router.push(`/sources/${sourceID}/alert-rules`)
}
handleChangeScript = tickscript => { handleChangeScript = tickscript => {
this.setState({task: {...this.state.task, tickscript}}) this.setState({
task: {...this.state.task, tickscript},
unsavedChanges: true,
})
} }
handleSelectDbrps = dbrps => { handleSelectDbrps = dbrps => {
this.setState({task: {...this.state.task, dbrps}}) this.setState({task: {...this.state.task, dbrps}, unsavedChanges: true})
} }
handleChangeType = type => () => { handleChangeType = type => () => {
this.setState({task: {...this.state.task, type}}) this.setState({task: {...this.state.task, type}, unsavedChanges: true})
} }
handleChangeID = e => { handleChangeID = e => {
this.setState({task: {...this.state.task, id: e.target.value}}) this.setState({
task: {...this.state.task, id: e.target.value},
unsavedChanges: true,
})
} }
handleToggleLogsVisbility = () => { handleToggleLogsVisibility = () => {
this.setState({areLogsVisible: !this.state.areLogsVisible}) this.setState({areLogsVisible: !this.state.areLogsVisible})
} }
render() { render() {
const {source} = this.props const {source} = this.props
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state const {
task,
logs,
areLogsVisible,
areLogsEnabled,
unsavedChanges,
consoleMessage,
} = this.state
return ( return (
<Tickscript <Tickscript
task={task} task={task}
logs={logs} logs={logs}
source={source} source={source}
validation={validation} consoleMessage={consoleMessage}
onSave={this.handleSave} onSave={this.handleSave}
unsavedChanges={unsavedChanges}
onExit={this.handleExit}
isNewTickscript={!this._isEditing()} isNewTickscript={!this._isEditing()}
onSelectDbrps={this.handleSelectDbrps} onSelectDbrps={this.handleSelectDbrps}
onChangeScript={this.handleChangeScript} onChangeScript={this.handleChangeScript}
@ -220,7 +242,7 @@ class TickscriptPage extends Component {
onChangeID={this.handleChangeID} onChangeID={this.handleChangeID}
areLogsVisible={areLogsVisible} areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled} areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={this.handleToggleLogsVisbility} onToggleLogsVisibility={this.handleToggleLogsVisibility}
/> />
) )
} }

View File

@ -0,0 +1,110 @@
import React, {Component, PropTypes} from 'react'
import OnClickOutside from 'shared/components/OnClickOutside'
class ConfirmButton extends Component {
constructor(props) {
super(props)
this.state = {
expanded: false,
}
}
handleButtonClick = () => {
if (this.props.disabled) {
return
}
this.setState({expanded: !this.state.expanded})
}
handleConfirmClick = () => {
this.setState({expanded: false})
this.props.confirmAction()
}
handleClickOutside = () => {
this.setState({expanded: false})
}
calculatePosition = () => {
if (!this.buttonDiv || !this.tooltipDiv) {
return ''
}
const windowWidth = window.innerWidth
const buttonRect = this.buttonDiv.getBoundingClientRect()
const tooltipRect = this.tooltipDiv.getBoundingClientRect()
const rightGap = windowWidth - buttonRect.right
if (tooltipRect.width / 2 > rightGap) {
return 'left'
}
return 'bottom'
}
render() {
const {
text,
confirmText,
type,
size,
square,
icon,
disabled,
customClass,
} = this.props
const {expanded} = this.state
const customClassString = customClass ? ` ${customClass}` : ''
const squareString = square ? ' btn-square' : ''
const expandedString = expanded ? ' active' : ''
const disabledString = disabled ? ' disabled' : ''
const classname = `confirm-button btn ${type} ${size}${customClassString}${squareString}${expandedString}${disabledString}`
return (
<div
className={classname}
onClick={this.handleButtonClick}
ref={r => (this.buttonDiv = r)}
>
{icon && <span className={`icon ${icon}`} />}
{text && text}
<div className={`confirm-button--tooltip ${this.calculatePosition()}`}>
<div
className="confirm-button--confirmation"
onClick={this.handleConfirmClick}
ref={r => (this.tooltipDiv = r)}
>
{confirmText}
</div>
</div>
</div>
)
}
}
const {bool, func, string} = PropTypes
ConfirmButton.defaultProps = {
confirmText: 'Confirm',
type: 'btn-default',
size: 'btn-sm',
square: false,
}
ConfirmButton.propTypes = {
text: string,
confirmText: string,
confirmAction: func.isRequired,
type: string,
size: string,
square: bool,
icon: string,
disabled: bool,
customClass: string,
}
export default OnClickOutside(ConfirmButton)

View File

@ -28,6 +28,7 @@
// Components // Components
@import 'components/ceo-display-options'; @import 'components/ceo-display-options';
@import 'components/confirm-button';
@import 'components/confirm-buttons'; @import 'components/confirm-buttons';
@import 'components/code-mirror-theme'; @import 'components/code-mirror-theme';
@import 'components/color-dropdown'; @import 'components/color-dropdown';

View File

@ -0,0 +1,79 @@
/*
Confirm Button
----------------------------------------------------------------------------
This button requires a second click to confirm the action
*/
.confirm-button {
.confirm-button--tooltip {
visibility: hidden;
transition: all;
position: absolute;
&.bottom {
top: calc(100% + 4px);
left: 50%;
transform: translateX(-50%);
}
&.left {
top: 50%;
right: calc(100% + 4px);
transform: translateY(-50%);
}
}
}
.confirm-button--confirmation {
border-radius: 3px;
background-color: $c-curacao;
opacity: 0;
padding: 0 7px;
color: $g20-white;
font-size: 13px;
font-weight: 600;
text-align: center;
transition: opacity 0.25s ease, background-color 0.25s ease;
&:after {
content: '';
border: 8px solid transparent;
position: absolute;
transition: border-color 0.25s ease;
z-index: 100;
}
&:hover {
background-color: $c-dreamsicle;
cursor: pointer;
}
}
.confirm-button--tooltip.bottom .confirm-button--confirmation:after {
bottom: 100%;
left: 50%;
border-bottom-color: $c-curacao;
transform: translateX(-50%);
}
.confirm-button--tooltip.bottom .confirm-button--confirmation:hover:after {
border-bottom-color: $c-dreamsicle;
}
.confirm-button--tooltip.left .confirm-button--confirmation:after {
left: 100%;
top: 50%;
border-left-color: $c-curacao;
transform: translateY(-50%);
}
.confirm-button--tooltip.left .confirm-button--confirmation:hover:after {
border-left-color: $c-dreamsicle;
}
.confirm-button.active {
z-index: 999;
.confirm-button--tooltip {
visibility: visible;
}
.confirm-button--confirmation {
opacity: 1;
}
}

View File

@ -3,28 +3,26 @@
---------------------------------------------------------------------------- ----------------------------------------------------------------------------
*/ */
$tickscript-console-height: 60px; $tickscript-controls-height: 60px;
.tickscript { .tickscript {
flex: 1 0 0; flex: 1 0 0;
position: relative;
} }
.tickscript-controls, .tickscript-controls,
.tickscript-console, .tickscript-console,
.tickscript-editor { .tickscript-editor {
padding: 0;
margin: 0;
width: 100%; width: 100%;
position: relative;
} }
.tickscript-controls, .tickscript-console,
.tickscript-console { .tickscript-controls {
height: $tickscript-console-height; padding: 0 60px;
display: flex;
} }
.tickscript-controls { .tickscript-controls {
display: flex;
align-items: center; align-items: center;
height: $tickscript-controls-height;
justify-content: space-between; justify-content: space-between;
padding: 0 60px;
background-color: $g3-castle; background-color: $g3-castle;
} }
.tickscript-controls--name { .tickscript-controls--name {
@ -42,29 +40,42 @@ $tickscript-console-height: 60px;
> * {margin-left: 8px;} > * {margin-left: 8px;}
} }
.tickscript-console--output { .tickscript-console {
padding: 0 60px; align-items: flex-start;
font-family: $code-font; height: $tickscript-controls-height * 2.25;
font-weight: 600; border-top: 2px solid $g3-castle;
display: flex; background-color: $g0-obsidian;
align-items: center; overflow-y: scroll;
background-color: $g2-kevlar; @include custom-scrollbar($g0-obsidian,$g4-onyx);
border-bottom: 2px solid $g3-castle;
position: relative;
height: 100%;
width: 100%;
border-radius: $radius $radius 0 0;
> p { > p {
margin: 0; position: relative;
padding-left: 16px;
font-family: $code-font;
margin: 11px 0;
font-weight: 700;
word-wrap: break-word;
word-break: break-word;
&:before {
content: '>';
position: absolute;
top: 0;
left: 0;
}
} }
} }
.tickscript-console--default { .tickscript-console--default {
color: $g10-wolf; color: $g13-mist;
font-style: italic; }
.tickscript-console--valid {
color: $c-rainforest;
}
.tickscript-console--error {
color: $c-dreamsicle;
} }
.tickscript-editor { .tickscript-editor {
height: calc(100% - #{$tickscript-console-height * 2}); height: calc(100% - #{$tickscript-controls-height * 3.25});
} }
/* /*