Merge pull request #2188 from influxdata/feature/tickscript-logging

Tickscript Logging
pull/2416/head^2
Hunter Trujillo 2017-11-28 19:37:46 -07:00 committed by GitHub
commit 37d96f7cc7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 879 additions and 112 deletions

View File

@ -17,6 +17,7 @@
1. [#2423](https://github.com/influxdata/chronograf/pull/2423): Gracefully scale Template Variables Manager overlay on smaller displays
### Features
1. [#2188](https://github.com/influxdata/chronograf/pull/2188): Add Kapacitor logs to the TICKscript editor
1. [#2384](https://github.com/influxdata/chronograf/pull/2384): Add filtering by name to Dashboard index page
1. [#2385](https://github.com/influxdata/chronograf/pull/2385): Add time shift feature to DataExplorer and Dashboards
1. [#2400](https://github.com/influxdata/chronograf/pull/2400): Allow override of generic oauth2 keys for email
@ -43,7 +44,7 @@
### UI Improvements
1. [#2111](https://github.com/influxdata/chronograf/pull/2111): Increase size of Cell Editor query tabs to reveal more of their query strings
1. [#2120](https://github.com/influxdata/chronograf/pull/2120): Improve appearance of Admin Page tabs on smaller screens
1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to Tickscript editor
1. [#2119](https://github.com/influxdata/chronograf/pull/2119): Add cancel button to TICKscript editor
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard naming & renaming interaction
1. [#2104](https://github.com/influxdata/chronograf/pull/2104): Redesign dashboard switching dropdown
@ -63,7 +64,7 @@
### Features
1. [#1885](https://github.com/influxdata/chronograf/pull/1885): Add `fill` options to data explorer and dashboard queries
1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKScript
1. [#1978](https://github.com/influxdata/chronograf/pull/1978): Support editing kapacitor TICKscript
1. [#1721](https://github.com/influxdata/chronograf/pull/1721): Introduce the TICKscript editor UI
1. [#1992](https://github.com/influxdata/chronograf/pull/1992): Add .csv download button to data explorer
1. [#2082](https://github.com/influxdata/chronograf/pull/2082): Add Data Explorer InfluxQL query and location query synchronization, so queries can be shared via a a URL

View File

@ -48,7 +48,7 @@
'arrow-parens': 0,
'comma-dangle': [2, 'always-multiline'],
'no-cond-assign': 2,
'no-console': ['error', {allow: ['error']}],
'no-console': ['error', {allow: ['error', 'warn']}],
'no-constant-condition': 2,
'no-control-regex': 2,
'no-debugger': 2,

View File

@ -100,3 +100,32 @@ export const updateTask = async (
throw error
}
}
export const getLogStream = kapacitor =>
fetch(`${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs`, {
method: 'GET',
headers: {'Content-Type': 'application/json'},
})
export const getLogStreamByRuleID = (kapacitor, ruleID) =>
fetch(
`${kapacitor.links.proxy}?path=/kapacitor/v1preview/logs?task=${ruleID}`,
{
method: 'GET',
headers: {'Content-Type': 'application/json'},
}
)
export const pingKapacitorVersion = async kapacitor => {
try {
const result = await AJAX({
method: 'GET',
url: `${kapacitor.links.proxy}?path=/kapacitor/v1preview/ping`,
})
const kapVersion = result.headers['x-kapacitor-version']
return kapVersion === '' ? null : kapVersion
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -0,0 +1,32 @@
import React, {PropTypes} from 'react'
const LogItemHTTP = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service">HTTP Request</div>
<div className="logs-table--http">
{logItem.method} {logItem.username}@{logItem.host} ({logItem.duration})
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemHTTP.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
method: string.isRequired,
username: string.isRequired,
host: string.isRequired,
duration: string.isRequired,
}),
}
export default LogItemHTTP

View File

@ -0,0 +1,32 @@
import React, {PropTypes} from 'react'
const LogItemHTTPError = ({logItem}) =>
<div className="logs-table--row" key={logItem.key}>
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service error">HTTP Server</div>
<div className="logs-table--blah">
<div className="logs-table--key-values error">
ERROR: {logItem.msg}
</div>
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemHTTPError.propTypes = {
logItem: shape({
key: string.isRequired,
lvl: string.isRequired,
ts: string.isRequired,
msg: string.isRequired,
}),
}
export default LogItemHTTPError

View File

@ -0,0 +1,34 @@
import React, {PropTypes} from 'react'
const LogItemInfluxDBDebug = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service debug">InfluxDB</div>
<div className="logs-table--blah">
<div className="logs-table--key-values debug">
DEBUG: {logItem.msg}
<br />
Cluster: {logItem.cluster}
</div>
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemInfluxDBDebug.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
msg: string.isRequired,
cluster: string.isRequired,
}),
}
export default LogItemInfluxDBDebug

View File

@ -0,0 +1,31 @@
import React, {PropTypes} from 'react'
const LogItemKapacitorDebug = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service debug">Kapacitor</div>
<div className="logs-table--blah">
<div className="logs-table--key-values debug">
DEBUG: {logItem.msg}
</div>
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemKapacitorDebug.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
msg: string.isRequired,
}),
}
export default LogItemKapacitorDebug

View File

@ -0,0 +1,31 @@
import React, {PropTypes} from 'react'
const LogItemKapacitorError = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service error">Kapacitor</div>
<div className="logs-table--blah">
<div className="logs-table--key-values error">
ERROR: {logItem.msg}
</div>
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemKapacitorError.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
msg: string.isRequired,
}),
}
export default LogItemKapacitorError

View File

@ -0,0 +1,51 @@
import React, {PropTypes} from 'react'
const renderKeysAndValues = object => {
if (!object) {
return <span className="logs-table--empty-cell">--</span>
}
const objKeys = Object.keys(object)
const objValues = Object.values(object)
const objElements = objKeys.map((objKey, i) =>
<div key={i} className="logs-table--key-value">
{objKey}: <span>{objValues[i]}</span>
</div>
)
return objElements
}
const LogItemKapacitorPoint = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service">Kapacitor Point</div>
<div className="logs-table--blah">
<div className="logs-table--key-values">
TAGS<br />
{renderKeysAndValues(logItem.tag)}
</div>
<div className="logs-table--key-values">
FIELDS<br />
{renderKeysAndValues(logItem.field)}
</div>
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemKapacitorPoint.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
tag: shape.isRequired,
field: shape.isRequired,
}),
}
export default LogItemKapacitorPoint

View File

@ -0,0 +1,28 @@
import React, {PropTypes} from 'react'
const LogItemSession = ({logItem}) =>
<div className="logs-table--row">
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--session">
{logItem.msg}
</div>
</div>
</div>
const {shape, string} = PropTypes
LogItemSession.propTypes = {
logItem: shape({
lvl: string.isRequired,
ts: string.isRequired,
msg: string.isRequired,
}),
}
export default LogItemSession

View File

@ -0,0 +1,38 @@
import React, {PropTypes} from 'react'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import LogsTableRow from 'src/kapacitor/components/LogsTableRow'
const LogsTable = ({logs}) =>
<div className="logs-table--container">
<div className="logs-table--header">
<h2 className="panel-title">Logs</h2>
</div>
<FancyScrollbar
className="logs-table--panel fancy-scroll--kapacitor"
autoHide={false}
>
<div className="logs-table">
{logs.length
? logs.map((log, i) =>
<LogsTableRow key={log.key} logItem={log} index={i} />
)
: <div className="page-spinner" />}
</div>
</FancyScrollbar>
</div>
const {arrayOf, shape, string} = PropTypes
LogsTable.propTypes = {
logs: arrayOf(
shape({
key: string.isRequired,
ts: string.isRequired,
lvl: string.isRequired,
msg: string.isRequired,
})
).isRequired,
}
export default LogsTable

View File

@ -0,0 +1,68 @@
import React, {PropTypes} from 'react'
import LogItemSession from 'src/kapacitor/components/LogItemSession'
import LogItemHTTP from 'src/kapacitor/components/LogItemHTTP'
import LogItemHTTPError from 'src/kapacitor/components/LogItemHTTPError'
import LogItemKapacitorPoint from 'src/kapacitor/components/LogItemKapacitorPoint'
import LogItemKapacitorError from 'src/kapacitor/components/LogItemKapacitorError'
import LogItemKapacitorDebug from 'src/kapacitor/components/LogItemKapacitorDebug'
import LogItemInfluxDBDebug from 'src/kapacitor/components/LogItemInfluxDBDebug'
const LogsTableRow = ({logItem, index}) => {
if (logItem.service === 'sessions') {
return <LogItemSession logItem={logItem} key={index} />
}
if (logItem.service === 'http' && logItem.msg === 'http request') {
return <LogItemHTTP logItem={logItem} key={index} />
}
if (logItem.service === 'kapacitor' && logItem.msg === 'point') {
return <LogItemKapacitorPoint logItem={logItem} key={index} />
}
if (logItem.service === 'httpd_server_errors' && logItem.lvl === 'error') {
return <LogItemHTTPError logItem={logItem} key={index} />
}
if (logItem.service === 'kapacitor' && logItem.lvl === 'error') {
return <LogItemKapacitorError logItem={logItem} key={index} />
}
if (logItem.service === 'kapacitor' && logItem.lvl === 'debug') {
return <LogItemKapacitorDebug logItem={logItem} key={index} />
}
if (logItem.service === 'influxdb' && logItem.lvl === 'debug') {
return <LogItemInfluxDBDebug logItem={logItem} key={index} />
}
return (
<div className="logs-table--row" key={index}>
<div className="logs-table--divider">
<div className={`logs-table--level ${logItem.lvl}`} />
<div className="logs-table--timestamp">
{logItem.ts}
</div>
</div>
<div className="logs-table--details">
<div className="logs-table--service">
{logItem.service || '--'}
</div>
<div className="logs-table--blah">
<div className="logs-table--key-values">
{logItem.msg || '--'}
</div>
</div>
</div>
</div>
)
}
const {number, shape, string} = PropTypes
LogsTableRow.propTypes = {
logItem: shape({
key: string.isRequired,
ts: string.isRequired,
lvl: string.isRequired,
msg: string.isRequired,
}).isRequired,
index: number,
}
export default LogsTableRow

View File

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

View File

@ -2,56 +2,63 @@ import React, {PropTypes} from 'react'
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
import TickscriptEditorControls from 'src/kapacitor/components/TickscriptEditorControls'
import TickscriptEditorConsole from 'src/kapacitor/components/TickscriptEditorConsole'
import LogsTable from 'src/kapacitor/components/LogsTable'
const Tickscript = ({
source,
onSave,
task,
logs,
validation,
onSelectDbrps,
onChangeScript,
onChangeType,
onChangeID,
isNewTickscript,
areLogsVisible,
areLogsEnabled,
onToggleLogsVisbility,
}) =>
<div className="page">
<TickscriptHeader
task={task}
source={source}
onSave={onSave}
onChangeID={onChangeID}
onChangeType={onChangeType}
onSelectDbrps={onSelectDbrps}
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility}
isNewTickscript={isNewTickscript}
/>
<div className="page-contents">
<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>
<div className="tickscript-editor">
<div className="page-contents--split">
<div className="tickscript">
<TickscriptEditorControls
isNewTickscript={isNewTickscript}
onSelectDbrps={onSelectDbrps}
onChangeType={onChangeType}
onChangeID={onChangeID}
task={task}
/>
<TickscriptEditorConsole validation={validation} />
<TickscriptEditor
script={task.tickscript}
onChangeScript={onChangeScript}
/>
</div>
{areLogsVisible ? <LogsTable logs={logs} /> : null}
</div>
</div>
const {arrayOf, bool, func, shape, string} = PropTypes
Tickscript.propTypes = {
logs: arrayOf(shape()).isRequired,
onSave: func.isRequired,
source: shape({
id: string,
}),
areLogsVisible: bool,
areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired,
task: shape({
id: string,
script: string,

View File

@ -21,7 +21,13 @@ class TickscriptEditor extends Component {
}
return (
<CodeMirror value={script} onChange={this.updateCode} options={options} />
<div className="tickscript-editor">
<CodeMirror
value={script}
onChange={this.updateCode}
options={options}
/>
</div>
)
}
}

View File

@ -0,0 +1,22 @@
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 {string} = PropTypes
TickscriptEditorConsole.propTypes = {
validation: string,
}
export default TickscriptEditorConsole

View File

@ -0,0 +1,44 @@
import React, {PropTypes} from 'react'
import TickscriptType from 'src/kapacitor/components/TickscriptType'
import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
import TickscriptID, {
TickscriptStaticID,
} from 'src/kapacitor/components/TickscriptID'
const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
const TickscriptEditorControls = ({
isNewTickscript,
onSelectDbrps,
onChangeType,
onChangeID,
task,
}) =>
<div className="tickscript-controls">
{isNewTickscript
? <TickscriptID onChangeID={onChangeID} id={task.id} />
: <TickscriptStaticID id={task.name} />}
<div className="tickscript-controls--right">
<TickscriptType type={task.type} onChangeType={onChangeType} />
<MultiSelectDBDropdown
selectedItems={addName(task.dbrps)}
onApply={onSelectDbrps}
/>
</div>
</div>
const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptEditorControls.propTypes = {
isNewTickscript: bool.isRequired,
onSelectDbrps: func.isRequired,
onChangeType: func.isRequired,
onChangeID: func.isRequired,
task: shape({
id: string,
script: string,
dbsrps: arrayOf(shape()),
}).isRequired,
}
export default TickscriptEditorControls

View File

@ -1,52 +1,36 @@
import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import SourceIndicator from 'shared/components/SourceIndicator'
import TickscriptType from 'src/kapacitor/components/TickscriptType'
import MultiSelectDBDropdown from 'shared/components/MultiSelectDBDropdown'
import TickscriptID, {
TickscriptStaticID,
} from 'src/kapacitor/components/TickscriptID'
const addName = list => list.map(l => ({...l, name: `${l.db}.${l.rp}`}))
import LogsToggle from 'src/kapacitor/components/LogsToggle'
const TickscriptHeader = ({
task: {id, type, dbrps},
task,
source,
task: {id},
onSave,
onChangeType,
onChangeID,
onSelectDbrps,
areLogsVisible,
areLogsEnabled,
isNewTickscript,
onToggleLogsVisbility,
}) =>
<div className="page-header">
<div className="page-header full-width">
<div className="page-header__container">
<div className="page-header__left">
{isNewTickscript
? <TickscriptID onChangeID={onChangeID} id={id} />
: <TickscriptStaticID id={task.name} />}
<h1 className="page-header__title">TICKscript Editor</h1>
</div>
{areLogsEnabled &&
<LogsToggle
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={onToggleLogsVisbility}
/>}
<div className="page-header__right">
<SourceIndicator />
<TickscriptType type={type} onChangeType={onChangeType} />
<MultiSelectDBDropdown
selectedItems={addName(dbrps)}
onApply={onSelectDbrps}
/>
<Link
className="btn btn-sm btn-default"
to={`/sources/${source.id}/alert-rules`}
>
Cancel
</Link>
<button
className="btn btn-success btn-sm"
title={id ? '' : 'ID your TICKscript to save'}
onClick={onSave}
disabled={!id}
>
Save Rule
{isNewTickscript ? 'Save New TICKscript' : 'Save TICKscript'}
</button>
</div>
</div>
@ -55,11 +39,11 @@ const TickscriptHeader = ({
const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptHeader.propTypes = {
isNewTickscript: bool,
onSave: func,
source: shape({
id: string,
}),
onSelectDbrps: func.isRequired,
areLogsVisible: bool,
areLogsEnabled: bool,
onToggleLogsVisbility: func.isRequired,
task: shape({
dbrps: arrayOf(
shape({
@ -68,9 +52,6 @@ TickscriptHeader.propTypes = {
})
),
}),
onChangeType: func.isRequired,
onChangeID: func.isRequired,
isNewTickscript: bool.isRequired,
}
export default TickscriptHeader

View File

@ -10,7 +10,7 @@ class TickscriptID extends Component {
return (
<input
className="page-header--editing kapacitor-theme"
className="form-control input-sm form-malachite"
autoFocus={true}
value={id}
onChange={onChangeID}
@ -23,10 +23,7 @@ class TickscriptID extends Component {
}
export const TickscriptStaticID = ({id}) =>
<h1
className="page-header--editing kapacitor-theme"
style={{display: 'flex', justifyContent: 'baseline'}}
>
<h1 className="tickscript-controls--name">
{id}
</h1>

View File

@ -1,11 +1,14 @@
import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
import uuid from 'node-uuid'
import Tickscript from 'src/kapacitor/components/Tickscript'
import * as kapactiorActionCreators from 'src/kapacitor/actions/view'
import * as errorActionCreators from 'shared/actions/errors'
import {getActiveKapacitor} from 'src/shared/apis'
import {getLogStreamByRuleID, pingKapacitorVersion} from 'src/kapacitor/apis'
import {publishNotification} from 'shared/actions/notifications'
class TickscriptPage extends Component {
constructor(props) {
@ -23,6 +26,96 @@ class TickscriptPage extends Component {
},
validation: '',
isEditingID: true,
logs: [],
areLogsEnabled: false,
failStr: '',
}
}
fetchChunkedLogs = async (kapacitor, ruleID) => {
const {notify} = this.props
try {
const version = await pingKapacitorVersion(kapacitor)
if (version && parseInt(version.split('.')[1], 10) < 4) {
this.setState({
areLogsEnabled: false,
})
notify(
'warning',
'Could not use logging, requires Kapacitor version 1.4'
)
return
}
if (this.state.logs.length === 0) {
this.setState({
areLogsEnabled: true,
logs: [
{
id: uuid.v4(),
key: uuid.v4(),
lvl: 'info',
msg: 'created log session',
service: 'sessions',
tags: 'nil',
ts: new Date().toISOString(),
},
],
})
}
const response = await getLogStreamByRuleID(kapacitor, ruleID)
const reader = await response.body.getReader()
const decoder = new TextDecoder()
let result
while (this.state.areLogsEnabled === true && !(result && result.done)) {
result = await reader.read()
const chunk = decoder.decode(result.value || new Uint8Array(), {
stream: !result.done,
})
const json = chunk.split('\n')
let logs = []
let failStr = this.state.failStr
try {
for (let objStr of json) {
objStr = failStr + objStr
failStr = objStr
const jsonStr = `[${objStr.split('}{').join('},{')}]`
logs = [
...logs,
...JSON.parse(jsonStr).map(log => ({
...log,
key: uuid.v4(),
})),
]
failStr = ''
}
this.setState({
logs: [...this.state.logs, ...logs],
failStr,
})
} catch (err) {
console.warn(err, failStr)
this.setState({
logs: [...this.state.logs, ...logs],
failStr,
})
}
}
} catch (error) {
console.error(error)
notify('error', error)
throw error
}
}
@ -50,9 +143,17 @@ class TickscriptPage extends Component {
this.setState({task: {tickscript, dbrps, type, status, name, id}})
}
this.fetchChunkedLogs(kapacitor, ruleID)
this.setState({kapacitor})
}
componentWillUnmount() {
this.setState({
areLogsEnabled: false,
})
}
handleSave = async () => {
const {kapacitor, task} = this.state
const {
@ -96,13 +197,18 @@ class TickscriptPage extends Component {
this.setState({task: {...this.state.task, id: e.target.value}})
}
handleToggleLogsVisbility = () => {
this.setState({areLogsVisible: !this.state.areLogsVisible})
}
render() {
const {source} = this.props
const {task, validation} = this.state
const {task, validation, logs, areLogsVisible, areLogsEnabled} = this.state
return (
<Tickscript
task={task}
logs={logs}
source={source}
validation={validation}
onSave={this.handleSave}
@ -111,6 +217,9 @@ class TickscriptPage extends Component {
onChangeScript={this.handleChangeScript}
onChangeType={this.handleChangeType}
onChangeID={this.handleChangeID}
areLogsVisible={areLogsVisible}
areLogsEnabled={areLogsEnabled}
onToggleLogsVisbility={this.handleToggleLogsVisbility}
/>
)
}
@ -142,6 +251,7 @@ TickscriptPage.propTypes = {
ruleID: string,
}).isRequired,
rules: arrayOf(shape()),
notify: func.isRequired,
}
const mapStateToProps = state => {
@ -153,6 +263,7 @@ const mapStateToProps = state => {
const mapDispatchToProps = dispatch => ({
kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch),
errorActions: bindActionCreators(errorActionCreators, dispatch),
notify: bindActionCreators(publishNotification, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage)

View File

@ -97,7 +97,7 @@ class ResizeContainer extends Component {
render() {
const {bottomHeightPixels, topHeight, bottomHeight, isDragging} = this.state
const {containerClass, children} = this.props
const {containerClass, children, theme} = this.props
if (React.Children.count(children) > maximumNumChildren) {
console.error(
@ -122,6 +122,7 @@ class ResizeContainer extends Component {
})}
</div>
<ResizeHandle
theme={theme}
isDragging={isDragging}
onHandleStartDrag={this.handleStartDrag}
top={topHeight}
@ -149,6 +150,7 @@ ResizeContainer.propTypes = {
minBottomHeight: number,
initialTopHeight: string,
initialBottomHeight: string,
theme: string,
}
export default ResizeContainer

View File

@ -6,15 +6,19 @@ const ResizeHandle = React.createClass({
propTypes: {
onHandleStartDrag: func.isRequired,
isDragging: bool.isRequired,
theme: string,
top: string,
},
render() {
const {isDragging, onHandleStartDrag, top} = this.props
const {isDragging, onHandleStartDrag, top, theme} = this.props
return (
<div
className={classnames('resizer--handle', {dragging: isDragging})}
className={classnames('resizer--handle', {
dragging: isDragging,
'resizer--malachite': theme === 'kapacitor',
})}
onMouseDown={onHandleStartDrag}
style={{top}}
/>

View File

@ -52,6 +52,7 @@
@import 'components/source-indicator';
@import 'components/source-selector';
@import 'components/tables';
@import 'components/kapacitor-logs-table';
// Pages
@import 'pages/config-endpoints';
@ -60,6 +61,7 @@
@import 'pages/kapacitor';
@import 'pages/dashboards';
@import 'pages/admin';
@import 'pages/tickscript-editor';
// TODO
@import 'unsorted';

View File

@ -7,45 +7,6 @@
*/
$tickscript-console-height: 120px;
.tickscript-console,
.tickscript-editor {
padding-left: $page-wrapper-padding;
padding-right: $page-wrapper-padding;
margin: 0 auto;
max-width: $page-wrapper-max-width;
position: relative;
}
.tickscript-console {
height: $tickscript-console-height;
padding-top: 30px;
}
.tickscript-console--output {
padding: 0 60px;
font-family: $code-font;
font-weight: 600;
display: flex;
align-items: center;
background-color: $g3-castle;
position: relative;
height: 100%;
width: 100%;
border-radius: $radius $radius 0 0;
> p {
margin: 0;
}
}
.tickscript-console--default {
color: $g10-wolf;
font-style: italic;
}
.tickscript-editor {
margin: 0 auto;
padding-bottom: 30px;
height: calc(100% - #{$tickscript-console-height});
}
.ReactCodeMirror {
position: relative;
width: 100%;
@ -54,8 +15,8 @@ $tickscript-console-height: 120px;
.cm-s-material.CodeMirror {
border-radius: 0 0 $radius $radius;
font-family: $code-font;
background-color: $g2-kevlar;
color: $c-neutrino;
background-color: transparent;
color: $g13-mist;
font-weight: 600;
height: 100%;
}
@ -63,7 +24,7 @@ $tickscript-console-height: 120px;
@include custom-scrollbar-round($g2-kevlar,$g6-smoke);
}
.cm-s-material .CodeMirror-gutters {
background-color: fade-out($g4-onyx, 0.5);
background-color: fade-out($g4-onyx, 0.7);
border: none;
}
.CodeMirror-gutter.CodeMirror-linenumbers {

View File

@ -0,0 +1,124 @@
/*
Styles for Kapacitor Logs Table
----------------------------------------------------------------------------
*/
$logs-table-header-height: 60px;
$logs-table-padding: 60px;
$logs-row-indent: 6px;
$logs-level-dot: 8px;
$logs-margin: 4px;
.logs-table--container {
width: 50%;
position: relative;
height: 100%;
@include gradient-v($g3-castle,$g1-raven);
}
.logs-table--header {
display: flex;
align-items: center;
justify-content: space-between;
flex-wrap: nowrap;
height: $logs-table-header-height;
padding: 0 $logs-table-padding 0 ($logs-table-padding / 2);
background-color: $g4-onyx;
}
.logs-table--panel {
position: absolute !important;
top: $logs-table-header-height;
left: 0;
width: 100%;
height: calc(100% - #{$logs-table-header-height}) !important;
}
.logs-table,
.logs-table--row {
display: flex;
align-items: stretch;
flex-direction: column;
}
@keyframes LogsFadeIn {
from {
background-color: $g6-smoke;
}
to {
background-color: transparent;
}
}
.logs-table {
flex-direction: column-reverse;
}
.logs-table--row {
padding: 8px ($logs-table-padding - 16px) 8px ($logs-table-padding / 2);
border-bottom: 2px solid $g3-castle;
animation-name: LogsFadeIn;
animation-duration: 2.5s;
animation-iteration-count: 1;
animation-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
transition: background-color 0.25s ease;
&:hover {
background-color: $g4-onyx;
}
&:first-child {
border-bottom: none;
}
}
.logs-table--divider {
display: flex;
align-items: center;
}
.logs-table--level {
width: $logs-level-dot;
height: $logs-level-dot;
border-radius: 50%;
position: relative;
margin-right: $logs-row-indent;
&.debug {background-color: $c-comet;}
&.info {background-color: $g6-smoke;}
&.warn {background-color: $c-pineapple;}
&.ok {background-color: $c-rainforest;}
&.error {background-color: $c-dreamsicle;}
}
.logs-table--timestamp {
font-family: $code-font;
font-weight: 500;
font-size: 11px;
color: $g9-mountain;
flex: 1 0 0;
}
.logs-table--details {
display: flex;
align-items: flex-start;
font-size: 13px;
color: $g13-mist;
font-weight: 600;
padding-left: ($logs-level-dot + $logs-row-indent);
.error {color: $c-dreamsicle;}
.debug {color: $c-comet;}
}
/* Logs Table Item Types */
.logs-table--session {
text-transform: capitalize;
font-style: italic;
}
.logs-table--service {
width: 140px;
}
.logs-table--blah {
display: flex;
flex: 1 0 0;
}
.logs-table--key-values {
color: $g11-sidewalk;
flex: 1 0 50%;
}
.logs-table--key-value {
}
.logs-table--key-value span {
color: $c-pool;
}

View File

@ -13,6 +13,7 @@ $resizer-dots: $g3-castle;
$resizer-color: $g5-pepper;
$resizer-color-hover: $g8-storm;
$resizer-color-active: $c-pool;
$resizer-color-kapacitor: $c-rainforest;
.resize--container {
overflow: hidden !important;
@ -109,4 +110,13 @@ $resizer-color-active: $c-pool;
box-shadow: 0 0 $resizer-glow $resizer-color-active;
}
}
}
}
/* Kapacitor Theme */
.resizer--handle.resizer--malachite.dragging {
&:before,
&:after {
background-color: $resizer-color-kapacitor;
box-shadow: 0 0 $resizer-glow $resizer-color-kapacitor;
}
}

View File

@ -15,7 +15,8 @@
.page {
flex-grow: 1;
}
.page-contents {
.page-contents,
.page-contents--split {
position: absolute !important;
top: $chronograf-page-header-height;
left: 0;
@ -28,6 +29,10 @@
height: 100%;
}
}
.page-contents--split {
display: flex;
align-items: stretch;
}
.container-fluid {
padding: ($chronograf-page-header-height / 2) $page-wrapper-padding;
max-width: $page-wrapper-max-width;

View File

@ -0,0 +1,90 @@
/*
Styles for TICKscript Editor
----------------------------------------------------------------------------
*/
$tickscript-console-height: 60px;
.tickscript {
flex: 1 0 0;
}
.tickscript-controls,
.tickscript-console,
.tickscript-editor {
padding: 0;
margin: 0;
width: 100%;
position: relative;
}
.tickscript-controls,
.tickscript-console {
height: $tickscript-console-height;
}
.tickscript-controls {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 60px;
background-color: $g3-castle;
}
.tickscript-controls--name {
margin: 0;
letter-spacing: 0;
@include no-user-select();
font-size: 17px;
font-weight: 400;
color: $g13-mist;
}
.tickscript-controls--right {
display: flex;
align-items: center;
flex-wrap: nowrap;
> * {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;
> p {
margin: 0;
}
}
.tickscript-console--default {
color: $g10-wolf;
font-style: italic;
}
.tickscript-editor {
height: calc(100% - #{$tickscript-console-height * 2});
}
/*
Toggle for displaying Logs
----------------------------------------------------------------------------
*/
.logs-toggle {
position: absolute;
left: 50%;
transform: translateX(-50%);
> li {
width: 100px;
justify-content: center;
}
> li:not(.active) {
background-color: $g0-obsidian;
&:hover {
background-color: $g3-castle;
}
}
}