Merge pull request #2746 from influxdata/saving-tickscript

Saving tickscript
pull/2752/head
Alex Paxton 2018-02-02 11:37:31 -08:00 committed by GitHub
commit 058df60e0e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 346 additions and 86 deletions

View File

@ -1,11 +1,12 @@
## v1.4.1.0 [unreleased]
### Features
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. [#2722](https://github.com/influxdata/chronograf/pull/2722): Add auto refresh widget to hosts list page
### UI Improvements
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
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

View File

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

View File

@ -1,16 +1,16 @@
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">
<li
className={areLogsVisible ? null : 'active'}
onClick={onToggleLogsVisbility}
onClick={onToggleLogsVisibility}
>
Editor
</li>
<li
className={areLogsVisible ? 'active' : null}
onClick={onToggleLogsVisbility}
onClick={onToggleLogsVisibility}
>
Editor + Logs
</li>
@ -20,7 +20,7 @@ const {bool, func} = PropTypes
LogsToggle.propTypes = {
areLogsVisible: bool,
onToggleLogsVisbility: func.isRequired,
onToggleLogsVisibility: func.isRequired,
}
export default LogsToggle

View File

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

View File

@ -1,22 +1,31 @@
import React, {PropTypes} from 'react'
const TickscriptEditorConsole = ({validation}) =>
<div className="tickscript-console">
<div className="tickscript-console--output">
{validation
? <p>
{validation}
</p>
: <p className="tickscript-console--default">
Save your TICKscript to validate it
</p>}
</div>
</div>
const TickscriptEditorConsole = ({consoleMessage, unsavedChanges}) => {
let consoleOutput = 'TICKscript is valid'
let consoleClass = 'tickscript-console--valid'
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 = {
validation: string,
consoleMessage: string,
unsavedChanges: bool,
}
export default TickscriptEditorConsole

View File

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

View File

@ -24,11 +24,12 @@ class TickscriptPage extends Component {
dbrps: [],
type: 'stream',
},
validation: '',
consoleMessage: '',
isEditingID: true,
logs: [],
areLogsEnabled: false,
failStr: '',
unsavedChanges: false,
}
}
@ -172,9 +173,10 @@ class TickscriptPage extends Component {
} else {
response = await createTask(kapacitor, task, router, sourceID)
}
if (response && response.code === 500) {
return this.setState({validation: response.message})
if (response.code) {
this.setState({unsavedChanges: true, consoleMessage: response.message})
} else {
this.setState({unsavedChanges: false, consoleMessage: ''})
}
} catch (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 => {
this.setState({task: {...this.state.task, tickscript}})
this.setState({
task: {...this.state.task, tickscript},
unsavedChanges: true,
})
}
handleSelectDbrps = dbrps => {
this.setState({task: {...this.state.task, dbrps}})
this.setState({task: {...this.state.task, dbrps}, unsavedChanges: true})
}
handleChangeType = type => () => {
this.setState({task: {...this.state.task, type}})
this.setState({task: {...this.state.task, type}, unsavedChanges: true})
}
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})
}
render() {
const {source} = this.props
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
const {
task,
logs,
areLogsVisible,
areLogsEnabled,
unsavedChanges,
consoleMessage,
} = this.state
return (
<Tickscript
task={task}
logs={logs}
source={source}
validation={validation}
consoleMessage={consoleMessage}
onSave={this.handleSave}
unsavedChanges={unsavedChanges}
onExit={this.handleExit}
isNewTickscript={!this._isEditing()}
onSelectDbrps={this.handleSelectDbrps}
onChangeScript={this.handleChangeScript}
@ -220,7 +242,7 @@ class TickscriptPage extends Component {
onChangeID={this.handleChangeID}
areLogsVisible={areLogsVisible}
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
@import 'components/ceo-display-options';
@import 'components/confirm-button';
@import 'components/confirm-buttons';
@import 'components/code-mirror-theme';
@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 {
flex: 1 0 0;
position: relative;
}
.tickscript-controls,
.tickscript-console,
.tickscript-editor {
padding: 0;
margin: 0;
width: 100%;
position: relative;
}
.tickscript-controls,
.tickscript-console {
height: $tickscript-console-height;
.tickscript-console,
.tickscript-controls {
padding: 0 60px;
display: flex;
}
.tickscript-controls {
display: flex;
align-items: center;
height: $tickscript-controls-height;
justify-content: space-between;
padding: 0 60px;
background-color: $g3-castle;
}
.tickscript-controls--name {
@ -42,29 +40,42 @@ $tickscript-console-height: 60px;
> * {margin-left: 8px;}
}
.tickscript-console--output {
padding: 0 60px;
font-family: $code-font;
font-weight: 600;
display: flex;
align-items: center;
background-color: $g2-kevlar;
border-bottom: 2px solid $g3-castle;
position: relative;
height: 100%;
width: 100%;
border-radius: $radius $radius 0 0;
.tickscript-console {
align-items: flex-start;
height: $tickscript-controls-height * 2.25;
border-top: 2px solid $g3-castle;
background-color: $g0-obsidian;
overflow-y: scroll;
@include custom-scrollbar($g0-obsidian,$g4-onyx);
> 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 {
color: $g10-wolf;
font-style: italic;
color: $g13-mist;
}
.tickscript-console--valid {
color: $c-rainforest;
}
.tickscript-console--error {
color: $c-dreamsicle;
}
.tickscript-editor {
height: calc(100% - #{$tickscript-console-height * 2});
height: calc(100% - #{$tickscript-controls-height * 3.25});
}
/*