Merge pull request #1721 from influxdata/feature/tickscript-editor

TICKscript Editor
pull/10616/head
Andrew Watkins 2017-09-27 15:55:41 -07:00 committed by GitHub
commit 56dcc31d82
29 changed files with 1737 additions and 130 deletions

View File

@ -9,6 +9,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. [#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
### UI Improvements
@ -157,6 +158,8 @@
1. [#1738](https://github.com/influxdata/chronograf/pull/1738): Add shared secret JWT authorization to InfluxDB
1. [#1724](https://github.com/influxdata/chronograf/pull/1724): Add Pushover alert support
1. [#1762](https://github.com/influxdata/chronograf/pull/1762): Restore all supported Kapacitor services when creating rules, and add most optional message parameters
1. [#1681](https://github.com/influxdata/chronograf/pull/1681): Add the ability to select Custom Time Ranges in the Hostpages, Data Explorer, and Dashboards.
1. [#1717](https://github.com/influxdata/chronograf/pull/1717): View server generated TICKscripts
### UI Improvements
1. [#1707](https://github.com/influxdata/chronograf/pull/1707): Polish alerts table in status page to wrap text less

View File

@ -39,6 +39,8 @@ type kapaLinks struct {
Proxy string `json:"proxy"` // URL location of proxy endpoint for this source
Self string `json:"self"` // Self link mapping to this resource
Rules string `json:"rules"` // Rules link for defining roles alerts for kapacitor
Tasks string `json:"tasks"` // Tasks link to define a task against the proxy
Ping string `json:"ping"` // Ping path to kapacitor
}
type kapacitor struct {
@ -108,6 +110,8 @@ func newKapacitor(srv chronograf.Server) kapacitor {
Self: fmt.Sprintf("%s/%d/kapacitors/%d", httpAPISrcs, srv.SrcID, srv.ID),
Proxy: fmt.Sprintf("%s/%d/kapacitors/%d/proxy", httpAPISrcs, srv.SrcID, srv.ID),
Rules: fmt.Sprintf("%s/%d/kapacitors/%d/rules", httpAPISrcs, srv.SrcID, srv.ID),
Tasks: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/tasks", httpAPISrcs, srv.SrcID, srv.ID),
Ping: fmt.Sprintf("%s/%d/kapacitors/%d/proxy?path=/kapacitor/v1/ping", httpAPISrcs, srv.SrcID, srv.ID),
},
}
}

View File

@ -95,6 +95,7 @@
"webpack-dev-server": "^1.14.1"
},
"dependencies": {
"@skidding/react-codemirror": "^1.0.1",
"axios": "^0.13.1",
"bignumber.js": "^4.0.2",
"bootstrap": "^3.3.7",
@ -112,6 +113,7 @@
"query-string": "^5.0.0",
"react": "^15.0.2",
"react-addons-shallow-compare": "^15.0.2",
"react-codemirror": "^1.0.0",
"react-custom-scrollbars": "^4.1.1",
"react-dimensions": "^1.2.0",
"react-dom": "^15.0.2",

View File

@ -10,7 +10,7 @@ import DeleteConfirmTableCell from 'shared/components/DeleteConfirmTableCell'
import {ROLES_TABLE} from 'src/admin/constants/tableSizing'
const RoleRow = ({
role: {name, permissions, users},
role: {name: roleName, permissions, users = []},
role,
allUsers,
allPermissions,
@ -23,12 +23,14 @@ const RoleRow = ({
onUpdateRoleUsers,
onUpdateRolePermissions,
}) => {
const handleUpdateUsers = u => {
onUpdateRoleUsers(role, u.map(n => ({name: n})))
const handleUpdateUsers = usrs => {
onUpdateRoleUsers(role, usrs)
}
const handleUpdatePermissions = allowed => {
onUpdateRolePermissions(role, [{scope: 'all', allowed}])
onUpdateRolePermissions(role, [
{scope: 'all', allowed: allowed.map(({name}) => name)},
])
}
const perms = _.get(permissions, ['0', 'allowed'], [])
@ -62,13 +64,13 @@ const RoleRow = ({
return (
<tr>
<td style={{width: `${ROLES_TABLE.colName}px`}}>
{name}
{roleName}
</td>
<td>
{allPermissions && allPermissions.length
? <MultiSelectDropdown
items={allPermissions}
selectedItems={perms}
items={allPermissions.map(name => ({name}))}
selectedItems={perms.map(name => ({name}))}
label={perms.length ? '' : 'Select Permissions'}
onApply={handleUpdatePermissions}
buttonSize="btn-xs"
@ -85,9 +87,9 @@ const RoleRow = ({
<td>
{allUsers && allUsers.length
? <MultiSelectDropdown
items={allUsers.map(u => u.name)}
selectedItems={users === undefined ? [] : users.map(u => u.name)}
label={users && users.length ? '' : 'Select Users'}
items={allUsers}
selectedItems={users}
label={users.length ? '' : 'Select Users'}
onApply={handleUpdateUsers}
buttonSize="btn-xs"
buttonColor="btn-primary"

View File

@ -12,7 +12,7 @@ import ChangePassRow from 'src/admin/components/ChangePassRow'
import {USERS_TABLE} from 'src/admin/constants/tableSizing'
const UserRow = ({
user: {name, roles, permissions, password},
user: {name, roles = [], permissions, password},
user,
allRoles,
allPermissions,
@ -27,14 +27,15 @@ const UserRow = ({
onUpdateRoles,
onUpdatePassword,
}) => {
const handleUpdatePermissions = allowed => {
const handleUpdatePermissions = perms => {
const allowed = perms.map(p => p.name)
onUpdatePermissions(user, [{scope: 'all', allowed}])
}
const handleUpdateRoles = roleNames => {
onUpdateRoles(
user,
allRoles.filter(r => roleNames.find(rn => rn === r.name))
allRoles.filter(r => roleNames.find(rn => rn.name === r.name))
)
}
@ -42,6 +43,8 @@ const UserRow = ({
onUpdatePassword(user, password)
}
const perms = _.get(permissions, ['0', 'allowed'], [])
if (isEditing) {
return (
<tr className="admin-table--edit-row">
@ -85,13 +88,9 @@ const UserRow = ({
{hasRoles
? <td>
<MultiSelectDropdown
items={allRoles.map(r => r.name)}
selectedItems={
roles
? roles.map(r => r.name)
: [] /* TODO remove check when server returns empty list */
}
label={roles && roles.length ? '' : 'Select Roles'}
items={allRoles}
selectedItems={roles.map(r => ({name: r.name}))}
label={roles.length ? '' : 'Select Roles'}
onApply={handleUpdateRoles}
buttonSize="btn-xs"
buttonColor="btn-primary"
@ -104,8 +103,8 @@ const UserRow = ({
<td>
{allPermissions && allPermissions.length
? <MultiSelectDropdown
items={allPermissions}
selectedItems={_.get(permissions, ['0', 'allowed'], [])}
items={allPermissions.map(p => ({name: p}))}
selectedItems={perms.map(p => ({name: p}))}
label={
permissions && permissions.length ? '' : 'Select Permissions'
}

349
ui/src/external/codemirror.js vendored Normal file
View File

@ -0,0 +1,349 @@
/* eslint-disable */
const CodeMirror = require('codemirror')
CodeMirror.defineSimpleMode = function(name, states) {
CodeMirror.defineMode(name, function(config) {
return CodeMirror.simpleMode(config, states)
})
}
CodeMirror.simpleMode = function(config, states) {
ensureState(states, 'start')
const states_ = {},
meta = states.meta || {}
let hasIndentation = false
for (const state in states) {
if (state !== meta && states.hasOwnProperty(state)) {
const list = (states_[state] = []),
orig = states[state]
for (let i = 0; i < orig.length; i++) {
const data = orig[i]
list.push(new Rule(data, states))
if (data.indent || data.dedent) {
hasIndentation = true
}
}
}
}
const mode = {
startState() {
return {
state: 'start',
pending: null,
local: null,
localState: null,
indent: hasIndentation ? [] : null,
}
},
copyState(state) {
const s = {
state: state.state,
pending: state.pending,
local: state.local,
localState: null,
indent: state.indent && state.indent.slice(0),
}
if (state.localState) {
s.localState = CodeMirror.copyState(state.local.mode, state.localState)
}
if (state.stack) {
s.stack = state.stack.slice(0)
}
for (let pers = state.persistentStates; pers; pers = pers.next) {
s.persistentStates = {
mode: pers.mode,
spec: pers.spec,
state:
pers.state === state.localState
? s.localState
: CodeMirror.copyState(pers.mode, pers.state),
next: s.persistentStates,
}
}
return s
},
token: tokenFunction(states_, config),
innerMode(state) {
return state.local && {mode: state.local.mode, state: state.localState}
},
indent: indentFunction(states_, meta),
}
if (meta) {
for (const prop in meta) {
if (meta.hasOwnProperty(prop)) {
mode[prop] = meta[prop]
}
}
}
return mode
}
function ensureState(states, name) {
if (!states.hasOwnProperty(name)) {
throw new Error(`Undefined state ${name} in simple mode`)
}
}
function toRegex(val, caret) {
if (!val) {
return /(?:)/
}
let flags = ''
if (val instanceof RegExp) {
if (val.ignoreCase) {
flags = 'i'
}
val = val.source
} else {
val = String(val)
}
return new RegExp(`${caret === false ? '' : '^'}(?:${val})`, flags)
}
function asToken(val) {
if (!val) {
return null
}
if (val.apply) {
return val
}
if (typeof val === 'string') {
return val.replace(/\./g, ' ')
}
const result = []
for (let i = 0; i < val.length; i++) {
result.push(val[i] && val[i].replace(/\./g, ' '))
}
return result
}
function Rule(data, states) {
if (data.next || data.push) {
ensureState(states, data.next || data.push)
}
this.regex = toRegex(data.regex)
this.token = asToken(data.token)
this.data = data
}
function tokenFunction(states, config) {
return function(stream, state) {
if (state.pending) {
const pend = state.pending.shift()
if (state.pending.length === 0) {
state.pending = null
}
stream.pos += pend.text.length
return pend.token
}
if (state.local) {
let tok, m
if (state.local.end && stream.match(state.local.end)) {
tok = state.local.endToken || null
state.local = state.localState = null
return tok
}
tok = state.local.mode.token(stream, state.localState)
if (
state.local.endScan &&
(m = state.local.endScan.exec(stream.current()))
) {
stream.pos = stream.start + m.index
}
return tok
}
const curState = states[state.state]
for (let i = 0; i < curState.length; i++) {
const rule = curState[i]
const matches =
(!rule.data.sol || stream.sol()) && stream.match(rule.regex)
if (matches) {
if (rule.data.next) {
state.state = rule.data.next
} else if (rule.data.push) {
;(state.stack || (state.stack = [])).push(state.state)
state.state = rule.data.push
} else if (rule.data.pop && state.stack && state.stack.length) {
state.state = state.stack.pop()
}
if (rule.data.mode) {
enterLocalMode(config, state, rule.data.mode, rule.token)
}
if (rule.data.indent) {
state.indent.push(stream.indentation() + config.indentUnit)
}
if (rule.data.dedent) {
state.indent.pop()
}
let token = rule.token
if (token && token.apply) {
token = token(matches)
}
if (matches.length > 2) {
state.pending = []
for (let j = 2; j < matches.length; j++) {
if (matches[j]) {
state.pending.push({text: matches[j], token: rule.token[j - 1]})
}
}
stream.backUp(
matches[0].length - (matches[1] ? matches[1].length : 0)
)
return token[0]
} else if (token && token.join) {
return token[0]
}
return token
}
}
stream.next()
return null
}
}
function cmp(a, b) {
if (a === b) {
return true
}
if (!a || typeof a !== 'object' || !b || typeof b !== 'object') {
return false
}
let props = 0
for (const prop in a) {
if (a.hasOwnProperty(prop)) {
if (!b.hasOwnProperty(prop) || !cmp(a[prop], b[prop])) {
return false
}
props += 1
}
}
for (const prop in b) {
if (b.hasOwnProperty(prop)) {
props -= 1
}
}
return props === 0
}
function enterLocalMode(config, state, spec, token) {
let pers
if (spec.persistent) {
for (let p = state.persistentStates; p && !pers; p = p.next) {
if (spec.spec ? cmp(spec.spec, p.spec) : spec.mode === p.mode) {
pers = p
}
}
}
const mode = pers
? pers.mode
: spec.mode || CodeMirror.getMode(config, spec.spec)
const lState = pers ? pers.state : CodeMirror.startState(mode)
if (spec.persistent && !pers) {
state.persistentStates = {
mode,
spec: spec.spec,
state: lState,
next: state.persistentStates,
}
}
state.localState = lState
state.local = {
mode,
end: spec.end && toRegex(spec.end),
endScan: spec.end && spec.forceEnd !== false && toRegex(spec.end, false),
endToken: token && token.join ? token[token.length - 1] : token,
}
}
function indexOf(val, arr) {
for (let i = 0; i < arr.length; i++) {
if (arr[i] === val) {
return true
}
}
}
function indentFunction(states, meta) {
return function(state, textAfter, line) {
if (state.local && state.local.mode.indent) {
return state.local.mode.indent(state.localState, textAfter, line)
}
if (
state.indent === null ||
state.local ||
(meta.dontIndentStates &&
indexOf(state.state, meta.dontIndentStates) > -1)
) {
return CodeMirror.Pass
}
let pos = state.indent.length - 1,
rules = states[state.state]
scan: for (;;) {
for (let i = 0; i < rules.length; i++) {
const rule = rules[i]
if (rule.data.dedent && rule.data.dedentIfLineStart !== false) {
const m = rule.regex.exec(textAfter)
if (m && m[0]) {
pos -= 1
if (rule.next || rule.push) {
rules = states[rule.next || rule.push]
}
textAfter = textAfter.slice(m[0].length)
continue scan
}
}
}
break
}
return pos < 0 ? 0 : state.indent[pos]
}
}
CodeMirror.defineSimpleMode('tickscript', {
// The start state contains the rules that are intially used
start: [
// The regex matches the token, the token property contains the type
{regex: /"(?:[^\\]|\\.)*?(?:"|$)/, token: 'string.double'},
{regex: /'(?:[^\\]|\\.)*?(?:'|$)/, token: 'string.single'},
{
regex: /(function)(\s+)([a-z$][\w$]*)/,
token: ['keyword', null, 'variable-2'],
},
// Rules are matched in the order in which they appear, so there is
// no ambiguity between this one and the one above
{
regex: /(?:var|return|if|for|while|else|do|this|stream|batch|influxql|lambda)/,
token: 'keyword',
},
{regex: /true|false|null|undefined|TRUE|FALSE/, token: 'atom'},
{
regex: /0x[a-f\d]+|[-+]?(?:\.\d+|\d+\.?\d*)(?:e[-+]?\d+)?/i,
token: 'number',
},
{regex: /\/\/.*/, token: 'comment'},
{regex: /\/(?:[^\\]|\\.)*?\//, token: 'variable-3'},
// A next property will cause the mode to move to a different state
{regex: /\/\*/, token: 'comment', next: 'comment'},
{regex: /[-+\/*=<>!]+/, token: 'operator'},
{regex: /[a-z$][\w$]*/, token: 'variable'},
],
// The multi-line comment state.
comment: [
{regex: /.*?\*\//, token: 'comment', next: 'start'},
{regex: /.*/, token: 'comment'},
],
// The meta property contains global information about the mode. It
// can contain properties like lineComment, which are supported by
// all modes, and also directives like dontIndentStates, which are
// specific to simple modes.
meta: {
dontIndentStates: ['comment'],
lineComment: '//',
},
})

View File

@ -23,6 +23,7 @@ import {
KapacitorRulePage,
KapacitorRulesPage,
KapacitorTasksPage,
TickscriptPage,
} from 'src/kapacitor'
import {AdminPage} from 'src/admin'
import {CreateSource, SourcePage, ManageSources} from 'src/sources'
@ -140,6 +141,8 @@ const Root = React.createClass({
<Route path="alert-rules" component={KapacitorRulesPage} />
<Route path="alert-rules/:ruleID" component={KapacitorRulePage} />
<Route path="alert-rules/new" component={KapacitorRulePage} />
<Route path="tickscript/new" component={TickscriptPage} />
<Route path="tickscript/:ruleID" component={TickscriptPage} />
<Route path="kapacitors/new" component={KapacitorPage} />
<Route path="kapacitors/:id/edit" component={KapacitorPage} />
<Route path="kapacitor-tasks" component={KapacitorTasksPage} />

View File

@ -3,9 +3,11 @@ import {getActiveKapacitor} from 'shared/apis'
import {publishNotification} from 'shared/actions/notifications'
import {
getRules,
getRule,
getRule as getRuleAJAX,
deleteRule as deleteRuleAPI,
updateRuleStatus as updateRuleStatusAPI,
createTask as createTaskAJAX,
updateTask as updateTaskAJAX,
} from 'src/kapacitor/apis'
import {errorThrown} from 'shared/actions/errors'
@ -19,7 +21,7 @@ const loadQuery = query => ({
export function fetchRule(source, ruleID) {
return dispatch => {
getActiveKapacitor(source).then(kapacitor => {
getRule(kapacitor, ruleID).then(({data: rule}) => {
getRuleAJAX(kapacitor, ruleID).then(({data: rule}) => {
dispatch({
type: 'LOAD_RULE',
payload: {
@ -39,6 +41,31 @@ const addQuery = queryID => ({
},
})
export const getRule = (kapacitor, ruleID) => async dispatch => {
try {
const {data: rule} = await getRuleAJAX(kapacitor, ruleID)
dispatch({
type: 'LOAD_RULE',
payload: {
rule: {...rule, queryID: rule.query && rule.query.id},
},
})
if (rule.query) {
dispatch({
type: 'LOAD_KAPACITOR_QUERY',
payload: {
query: rule.query,
},
})
}
} catch (error) {
console.error(error)
throw error
}
}
export function loadDefaultRule() {
return dispatch => {
const queryID = uuid.v4()
@ -208,3 +235,46 @@ export function updateRuleStatus(rule, status) {
})
}
}
export const createTask = (
kapacitor,
task,
router,
sourceID
) => async dispatch => {
try {
const {data} = await createTaskAJAX(kapacitor, task)
router.push(`/sources/${sourceID}/alert-rules`)
dispatch(publishNotification('success', 'You made a TICKscript!'))
return data
} catch (error) {
if (!error) {
dispatch(errorThrown('Could not communicate with server'))
return
}
return error.data
}
}
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'))
return data
} catch (error) {
if (!error) {
dispatch(errorThrown('Could not communicate with server'))
return
}
return error.data
}
}

View File

@ -1,6 +1,6 @@
import AJAX from 'utils/ajax'
function rangeRule(rule) {
const rangeRule = rule => {
const {value, rangeValue, operator} = rule.values
if (operator === 'inside range' || operator === 'outside range') {
@ -11,7 +11,7 @@ function rangeRule(rule) {
return rule
}
export function createRule(kapacitor, rule) {
export const createRule = (kapacitor, rule) => {
return AJAX({
method: 'POST',
url: kapacitor.links.rules,
@ -19,21 +19,26 @@ export function createRule(kapacitor, rule) {
})
}
export function getRules(kapacitor) {
export const getRules = kapacitor => {
return AJAX({
method: 'GET',
url: kapacitor.links.rules,
})
}
export function getRule(kapacitor, ruleID) {
return AJAX({
method: 'GET',
url: `${kapacitor.links.rules}/${ruleID}`,
})
export const getRule = async (kapacitor, ruleID) => {
try {
return await AJAX({
method: 'GET',
url: `${kapacitor.links.rules}/${ruleID}`,
})
} catch (error) {
console.error(error)
throw error
}
}
export function editRule(rule) {
export const editRule = rule => {
return AJAX({
method: 'PUT',
url: rule.links.self,
@ -41,17 +46,57 @@ export function editRule(rule) {
})
}
export function deleteRule(rule) {
export const deleteRule = rule => {
return AJAX({
method: 'DELETE',
url: rule.links.self,
})
}
export function updateRuleStatus(rule, status) {
export const updateRuleStatus = (rule, status) => {
return AJAX({
method: 'PATCH',
url: rule.links.self,
data: {status},
})
}
export const createTask = async (kapacitor, {id, dbrps, tickscript, type}) => {
try {
return await AJAX({
method: 'POST',
url: kapacitor.links.rules,
data: {
id,
type,
dbrps,
tickscript,
},
})
} catch (error) {
console.error(error)
throw error
}
}
export const updateTask = async (
kapacitor,
{id, dbrps, tickscript, type},
ruleID
) => {
try {
return await AJAX({
method: 'PUT',
url: `${kapacitor.links.rules}/${ruleID}`,
data: {
id,
type,
dbrps,
tickscript,
},
})
} catch (error) {
console.error(error)
throw error
}
}

View File

@ -4,8 +4,8 @@ import {Link} from 'react-router'
import NoKapacitorError from 'shared/components/NoKapacitorError'
import SourceIndicator from 'shared/components/SourceIndicator'
import KapacitorRulesTable from 'src/kapacitor/components/KapacitorRulesTable'
import TasksTable from 'src/kapacitor/components/TasksTable'
import FancyScrollbar from 'shared/components/FancyScrollbar'
import TICKscriptOverlay from 'src/kapacitor/components/TICKscriptOverlay'
const KapacitorRules = ({
source,
@ -13,10 +13,7 @@ const KapacitorRules = ({
hasKapacitor,
loading,
onDelete,
tickscript,
onChangeRuleStatus,
onReadTickscript,
onCloseTickscript,
}) => {
if (loading) {
return (
@ -44,43 +41,72 @@ const KapacitorRules = ({
)
}
const tableHeader =
rules.length === 1 ? '1 Alert Rule' : `${rules.length} Alert Rules`
const rulez = rules.filter(r => r.query)
const tasks = rules.filter(r => !r.query)
const rHeader = `${rulez.length} Alert Rule${rulez.length === 1 ? '' : 's'}`
const tHeader = `${tasks.length} TICKscript${tasks.length === 1 ? '' : 's'}`
return (
<PageContents
source={source}
tickscript={tickscript}
onReadTickscript={onReadTickscript}
onCloseTickscript={onCloseTickscript}
>
<PageContents source={source}>
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{tableHeader}
{rHeader}
</h2>
<Link
to={`/sources/${source.id}/alert-rules/new`}
className="btn btn-sm btn-primary"
>
Create Rule
</Link>
<div className="u-flex u-ai-center u-jc-space-between">
<Link
to={`/sources/${source.id}/alert-rules/new`}
className="btn btn-sm btn-primary"
style={{marginRight: '4px'}}
>
Build Rule
</Link>
</div>
</div>
<KapacitorRulesTable
source={source}
rules={rules}
rules={rulez}
onDelete={onDelete}
onReadTickscript={onReadTickscript}
onChangeRuleStatus={onChangeRuleStatus}
/>
<div className="row">
<div className="col-md-12">
<div className="panel panel-minimal">
<div className="panel-heading u-flex u-ai-center u-jc-space-between">
<h2 className="panel-title">
{tHeader}
</h2>
<div className="u-flex u-ai-center u-jc-space-between">
<Link
to={`/sources/${source.id}/tickscript/new`}
className="btn btn-sm btn-info"
>
Write TICKscript
</Link>
</div>
</div>
<TasksTable
source={source}
tasks={tasks}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
</div>
</div>
</div>
</PageContents>
)
}
const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
const PageContents = ({children, source}) =>
<div className="page">
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
<h1 className="page-header__title">Alert Rules</h1>
<h1 className="page-header__title">
Build Alert Rules or Write TICKscripts
</h1>
</div>
<div className="page-header__right">
<SourceIndicator sourceName={source && source.name} />
@ -98,15 +124,9 @@ const PageContents = ({children, source, tickscript, onCloseTickscript}) =>
</div>
</div>
</FancyScrollbar>
{tickscript
? <TICKscriptOverlay
tickscript={tickscript}
onClose={onCloseTickscript}
/>
: null}
</div>
const {arrayOf, bool, func, node, shape, string} = PropTypes
const {arrayOf, bool, func, node, shape} = PropTypes
KapacitorRules.propTypes = {
source: shape(),
@ -115,15 +135,11 @@ KapacitorRules.propTypes = {
loading: bool,
onChangeRuleStatus: func,
onDelete: func,
tickscript: string,
onReadTickscript: func,
onCloseTickscript: func,
}
PageContents.propTypes = {
children: node,
source: shape(),
tickscript: string,
onCloseTickscript: func,
}

View File

@ -5,26 +5,20 @@ import _ from 'lodash'
import {KAPACITOR_RULES_TABLE} from 'src/kapacitor/constants/tableSizing'
const {
colName,
colType,
colTrigger,
colMessage,
colAlerts,
colEnabled,
colActions,
} = KAPACITOR_RULES_TABLE
const KapacitorRulesTable = ({
rules,
source,
onDelete,
onReadTickscript,
onChangeRuleStatus,
}) =>
const KapacitorRulesTable = ({rules, source, onDelete, onChangeRuleStatus}) =>
<div className="panel-body">
<table className="table v-center">
<thead>
<tr>
<th style={{width: colName}}>Name</th>
<th style={{width: colType}}>Rule Type</th>
<th style={{width: colTrigger}}>Rule Trigger</th>
<th style={{width: colMessage}}>Message</th>
<th style={{width: colAlerts}}>Alerts</th>
<th style={{width: colEnabled}} className="text-center">
@ -42,7 +36,6 @@ const KapacitorRulesTable = ({
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
onRead={onReadTickscript}
/>
)
})}
@ -50,12 +43,14 @@ const KapacitorRulesTable = ({
</table>
</div>
const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
const handleDelete = (rule, onDelete) => onDelete(rule)
const RuleRow = ({rule, source, onDelete, onChangeRuleStatus}) =>
<tr key={rule.id}>
<td style={{width: colName}} className="monotype">
<RuleTitle rule={rule} source={source} />
</td>
<td style={{width: colType}} className="monotype">
<td style={{width: colTrigger}} className="monotype">
{rule.trigger}
</td>
<td className="monotype">
@ -82,10 +77,16 @@ const RuleRow = ({rule, source, onRead, onDelete, onChangeRuleStatus}) =>
</div>
</td>
<td style={{width: colActions}} className="text-right table-cell-nowrap">
<button className="btn btn-info btn-xs" onClick={onRead(rule)}>
View TICKscript
</button>
<button className="btn btn-danger btn-xs" onClick={onDelete(rule)}>
<Link
className="btn btn-info btn-xs"
to={`/sources/${source.id}/tickscript/${rule.id}`}
>
Edit TICKscript
</Link>
<button
className="btn btn-danger btn-xs"
onClick={handleDelete(rule, onDelete)}
>
Delete
</button>
</td>
@ -117,7 +118,6 @@ KapacitorRulesTable.propTypes = {
source: shape({
id: string.isRequired,
}).isRequired,
onReadTickscript: func,
}
RuleRow.propTypes = {
@ -125,7 +125,6 @@ RuleRow.propTypes = {
source: shape(),
onChangeRuleStatus: func,
onDelete: func,
onRead: func,
}
RuleTitle.propTypes = {

View File

@ -0,0 +1,96 @@
import React, {PropTypes} from 'react'
import {Link} from 'react-router'
import _ from 'lodash'
import {TASKS_TABLE} from 'src/kapacitor/constants/tableSizing'
const {colID, colType, colEnabled, colActions} = TASKS_TABLE
const TasksTable = ({tasks, source, onDelete, onChangeRuleStatus}) =>
<div className="panel-body">
<table className="table v-center">
<thead>
<tr>
<th style={{width: colID}}>ID</th>
<th style={{width: colType}}>Type</th>
<th style={{width: colEnabled}} className="text-center">
Enabled
</th>
<th style={{width: colActions}} />
</tr>
</thead>
<tbody>
{_.sortBy(tasks, t => t.name.toLowerCase()).map(task => {
return (
<TaskRow
key={task.id}
task={task}
source={source}
onDelete={onDelete}
onChangeRuleStatus={onChangeRuleStatus}
/>
)
})}
</tbody>
</table>
</div>
const handleDelete = (task, onDelete) => onDelete(task)
const TaskRow = ({task, source, onDelete, onChangeRuleStatus}) =>
<tr key={task.id}>
<td style={{width: colID}} className="monotype">
<i>
{task.id}
</i>
</td>
<td style={{width: colType}} className="monotype">
{task.type}
</td>
<td style={{width: colEnabled}} className="monotype text-center">
<div className="dark-checkbox">
<input
id={`kapacitor-enabled ${task.id}`}
className="form-control-static"
type="checkbox"
defaultChecked={task.status === 'enabled'}
onClick={onChangeRuleStatus(task)}
/>
<label htmlFor={`kapacitor-enabled ${task.id}`} />
</div>
</td>
<td style={{width: colActions}} className="text-right table-cell-nowrap">
<Link
className="btn btn-info btn-xs"
to={`/sources/${source.id}/tickscript/${task.id}`}
>
Edit TICKscript
</Link>
<button
className="btn btn-danger btn-xs"
onClick={handleDelete(task, onDelete)}
>
Delete
</button>
</td>
</tr>
const {arrayOf, func, shape, string} = PropTypes
TasksTable.propTypes = {
tasks: arrayOf(shape()),
onChangeRuleStatus: func,
onDelete: func,
source: shape({
id: string.isRequired,
}).isRequired,
}
TaskRow.propTypes = {
task: shape(),
source: shape(),
onChangeRuleStatus: func,
onDelete: func,
}
export default TasksTable

View File

@ -0,0 +1,65 @@
import React, {PropTypes} from 'react'
import TickscriptHeader from 'src/kapacitor/components/TickscriptHeader'
import TickscriptEditor from 'src/kapacitor/components/TickscriptEditor'
const Tickscript = ({
source,
onSave,
task,
validation,
onSelectDbrps,
onChangeScript,
onChangeType,
onChangeID,
isNewTickscript,
}) =>
<div className="page">
<TickscriptHeader
task={task}
source={source}
onSave={onSave}
onChangeID={onChangeID}
onChangeType={onChangeType}
onSelectDbrps={onSelectDbrps}
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">
<TickscriptEditor
script={task.tickscript}
onChangeScript={onChangeScript}
/>
</div>
</div>
</div>
const {arrayOf, bool, func, shape, string} = PropTypes
Tickscript.propTypes = {
onSave: func.isRequired,
source: shape(),
task: shape({
id: string,
script: string,
dbsrps: arrayOf(shape()),
}).isRequired,
onChangeScript: func.isRequired,
onSelectDbrps: func.isRequired,
validation: string,
onChangeType: func.isRequired,
onChangeID: func.isRequired,
isNewTickscript: bool.isRequired,
}
export default Tickscript

View File

@ -0,0 +1,36 @@
import React, {PropTypes, Component} from 'react'
import CodeMirror from '@skidding/react-codemirror'
import 'src/external/codemirror'
class TickscriptEditor extends Component {
constructor(props) {
super(props)
}
updateCode = script => {
this.props.onChangeScript(script)
}
render() {
const {script} = this.props
const options = {
lineNumbers: true,
theme: 'material',
tabIndex: 1,
}
return (
<CodeMirror value={script} onChange={this.updateCode} options={options} />
)
}
}
const {func, string} = PropTypes
TickscriptEditor.propTypes = {
onChangeScript: func,
script: string,
}
export default TickscriptEditor

View File

@ -0,0 +1,66 @@
import React, {PropTypes} from 'react'
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}`}))
const TickscriptHeader = ({
task: {id, type, dbrps},
task,
source: {name},
onSave,
onChangeType,
onChangeID,
onSelectDbrps,
isNewTickscript,
}) =>
<div className="page-header">
<div className="page-header__container">
<div className="page-header__left">
{isNewTickscript
? <TickscriptID onChangeID={onChangeID} id={id} />
: <TickscriptStaticID id={task.name} />}
</div>
<div className="page-header__right">
<SourceIndicator sourceName={name} />
<TickscriptType type={type} onChangeType={onChangeType} />
<MultiSelectDBDropdown
selectedItems={addName(dbrps)}
onApply={onSelectDbrps}
/>
<button
className="btn btn-success btn-sm"
title={id ? '' : 'ID your TICKscript to save'}
onClick={onSave}
disabled={!id}
>
Save Rule
</button>
</div>
</div>
</div>
const {arrayOf, bool, func, shape, string} = PropTypes
TickscriptHeader.propTypes = {
onSave: func,
source: shape(),
onSelectDbrps: func.isRequired,
task: shape({
dbrps: arrayOf(
shape({
db: string,
rp: string,
})
),
}),
onChangeType: func.isRequired,
onChangeID: func.isRequired,
isNewTickscript: bool.isRequired,
}
export default TickscriptHeader

View File

@ -0,0 +1,44 @@
import React, {PropTypes, Component} from 'react'
class TickscriptID extends Component {
constructor(props) {
super(props)
}
render() {
const {onChangeID, id} = this.props
return (
<input
className="page-header--editing kapacitor-theme"
autoFocus={true}
value={id}
onChange={onChangeID}
placeholder="ID your TICKscript"
spellCheck={false}
autoComplete={false}
/>
)
}
}
export const TickscriptStaticID = ({id}) =>
<h1
className="page-header--editing kapacitor-theme"
style={{display: 'flex', justifyContent: 'baseline'}}
>
{id}
</h1>
const {func, string} = PropTypes
TickscriptID.propTypes = {
onChangeID: func.isRequired,
id: string.isRequired,
}
TickscriptStaticID.propTypes = {
id: string.isRequired,
}
export default TickscriptID

View File

@ -0,0 +1,28 @@
import React, {PropTypes} from 'react'
const STREAM = 'stream'
const BATCH = 'batch'
const TickscriptType = ({type, onChangeType}) =>
<ul className="nav nav-tablist nav-tablist-sm">
<li
className={type === STREAM ? 'active' : ''}
onClick={onChangeType(STREAM)}
>
Stream
</li>
<li
className={type === BATCH ? 'active' : ''}
onClick={onChangeType(BATCH)}
>
Batch
</li>
</ul>
const {string, func} = PropTypes
TickscriptType.propTypes = {
type: string,
onChangeType: func,
}
export default TickscriptType

View File

@ -1,8 +1,15 @@
export const KAPACITOR_RULES_TABLE = {
colName: '200px',
colType: '90px',
colTrigger: '90px',
colMessage: '460px',
colAlerts: '120px',
colEnabled: '64px',
colActions: '176px',
}
export const TASKS_TABLE = {
colID: '200px',
colType: '90px',
colEnabled: '64px',
colActions: '176px',
}

View File

@ -11,7 +11,6 @@ class KapacitorRulesPage extends Component {
this.state = {
hasKapacitor: false,
loading: true,
tickscript: null,
}
}
@ -38,17 +37,9 @@ class KapacitorRulesPage extends Component {
actions.updateRuleStatusSuccess(rule.id, status)
}
handleReadTickscript = ({tickscript}) => () => {
this.setState({tickscript})
}
handleCloseTickscript = () => {
this.setState({tickscript: null})
}
render() {
const {source, rules} = this.props
const {hasKapacitor, loading, tickscript} = this.state
const {hasKapacitor, loading} = this.state
return (
<KapacitorRules
@ -58,9 +49,6 @@ class KapacitorRulesPage extends Component {
loading={loading}
onDelete={this.handleDeleteRule}
onChangeRuleStatus={this.handleRuleStatus}
onReadTickscript={this.handleReadTickscript}
tickscript={tickscript}
onCloseTickscript={this.handleCloseTickscript}
/>
)
}

View File

@ -0,0 +1,158 @@
import React, {PropTypes, Component} from 'react'
import {connect} from 'react-redux'
import {bindActionCreators} from 'redux'
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'
class TickscriptPage extends Component {
constructor(props) {
super(props)
this.state = {
kapacitor: {},
task: {
id: '',
name: '',
status: 'enabled',
tickscript: '',
dbrps: [],
type: 'stream',
},
validation: '',
isEditingID: true,
}
}
async componentDidMount() {
const {
source,
errorActions,
kapacitorActions,
params: {ruleID},
} = this.props
const kapacitor = await getActiveKapacitor(source)
if (!kapacitor) {
errorActions.errorThrown(
'We could not find a configured Kapacitor for this source'
)
}
if (this._isEditing()) {
await kapacitorActions.getRule(kapacitor, ruleID)
const {id, name, tickscript, dbrps, type} = this.props.rules.find(
r => r.id === ruleID
)
this.setState({task: {tickscript, dbrps, type, status, name, id}})
}
this.setState({kapacitor})
}
handleSave = async () => {
const {kapacitor, task} = this.state
const {
source: {id: sourceID},
router,
kapacitorActions: {createTask, updateTask},
params: {ruleID},
} = this.props
let response
try {
if (this._isEditing()) {
response = await updateTask(kapacitor, task, ruleID, router, sourceID)
} else {
response = await createTask(kapacitor, task, router, sourceID)
}
if (response && response.code === 500) {
return this.setState({validation: response.message})
}
} catch (error) {
console.error(error)
throw error
}
}
handleChangeScript = tickscript => {
this.setState({task: {...this.state.task, tickscript}})
}
handleSelectDbrps = dbrps => {
this.setState({task: {...this.state.task, dbrps}})
}
handleChangeType = type => () => {
this.setState({task: {...this.state.task, type}})
}
handleChangeID = e => {
this.setState({task: {...this.state.task, id: e.target.value}})
}
render() {
const {source} = this.props
const {task, validation} = this.state
return (
<Tickscript
task={task}
source={source}
validation={validation}
onSave={this.handleSave}
isNewTickscript={!this._isEditing()}
onSelectDbrps={this.handleSelectDbrps}
onChangeScript={this.handleChangeScript}
onChangeType={this.handleChangeType}
onChangeID={this.handleChangeID}
/>
)
}
_isEditing() {
const {params} = this.props
return params.ruleID && params.ruleID !== 'new'
}
}
const {arrayOf, func, shape, string} = PropTypes
TickscriptPage.propTypes = {
source: shape({
name: string,
}),
errorActions: shape({
errorThrown: func.isRequired,
}).isRequired,
kapacitorActions: shape({
updateTask: func.isRequired,
createTask: func.isRequired,
getRule: func.isRequired,
}),
router: shape({
push: func.isRequired,
}).isRequired,
params: shape({
ruleID: string,
}).isRequired,
rules: arrayOf(shape()),
}
const mapStateToProps = state => {
return {
rules: Object.values(state.rules),
}
}
const mapDispatchToProps = dispatch => ({
kapacitorActions: bindActionCreators(kapactiorActionCreators, dispatch),
errorActions: bindActionCreators(errorActionCreators, dispatch),
})
export default connect(mapStateToProps, mapDispatchToProps)(TickscriptPage)

View File

@ -2,9 +2,11 @@ import KapacitorPage from './containers/KapacitorPage'
import KapacitorRulePage from './containers/KapacitorRulePage'
import KapacitorRulesPage from './containers/KapacitorRulesPage'
import KapacitorTasksPage from './containers/KapacitorTasksPage'
import TickscriptPage from './containers/TickscriptPage'
export {
KapacitorPage,
KapacitorRulePage,
KapacitorRulesPage,
KapacitorTasksPage,
TickscriptPage,
}

View File

@ -54,7 +54,7 @@ export function deleteSource(source) {
export function pingKapacitor(kapacitor) {
return AJAX({
method: 'GET',
url: `${kapacitor.links.proxy}?path=/kapacitor/v1/ping`,
url: kapacitor.links.ping,
})
}

View File

@ -0,0 +1,85 @@
import React, {PropTypes, Component} from 'react'
import {showDatabases, showRetentionPolicies} from 'shared/apis/metaQuery'
import showDatabasesParser from 'shared/parsing/showDatabases'
import showRetentionPoliciesParser from 'shared/parsing/showRetentionPolicies'
import MultiSelectDropdown from 'shared/components/MultiSelectDropdown'
class MultiSelectDBDropdown extends Component {
constructor(props) {
super(props)
this.state = {
dbrps: [],
}
}
componentDidMount() {
this._getDbRps()
}
render() {
const {dbrps} = this.state
const {onApply, selectedItems} = this.props
const label = 'Select databases'
return (
<MultiSelectDropdown
label={label}
items={dbrps}
onApply={onApply}
isApplyShown={false}
selectedItems={selectedItems}
/>
)
}
_getDbRps = async () => {
const {source: {links: {proxy}}} = this.context
const {onErrorThrown} = this.props
try {
const {data} = await showDatabases(proxy)
const {databases, errors} = showDatabasesParser(data)
if (errors.length > 0) {
throw errors[0] // only one error can come back from this, but it's returned as an array
}
const response = await showRetentionPolicies(proxy, databases)
const dbrps = response.data.results.reduce((acc, result, i) => {
const {retentionPolicies} = showRetentionPoliciesParser(result)
const db = databases[i]
const rps = retentionPolicies.map(({name: rp}) => ({
db,
rp,
name: `${db}.${rp}`,
}))
return [...acc, ...rps]
}, [])
this.setState({dbrps})
} catch (error) {
console.error(error)
onErrorThrown(error)
}
}
}
const {arrayOf, func, shape, string} = PropTypes
MultiSelectDBDropdown.contextTypes = {
source: shape({
links: shape({
proxy: string.isRequired,
}).isRequired,
}).isRequired,
}
MultiSelectDBDropdown.propTypes = {
onErrorThrown: func,
onApply: func.isRequired,
selectedItems: arrayOf(shape()),
}
export default MultiSelectDBDropdown

View File

@ -8,16 +8,19 @@ import FancyScrollbar from 'shared/components/FancyScrollbar'
import {DROPDOWN_MENU_MAX_HEIGHT} from 'shared/constants/index'
const labelText = ({localSelectedItems, isOpen, label}) => {
if (localSelectedItems.length) {
return localSelectedItems.map(s => s.name).join(', ')
}
if (label) {
return label
} else if (localSelectedItems.length) {
return localSelectedItems.map(s => s).join(', ')
}
// TODO: be smarter about the text displayed here
if (isOpen) {
return '0 Selected'
}
return 'None'
}
@ -27,42 +30,52 @@ class MultiSelectDropdown extends Component {
this.state = {
isOpen: false,
localSelectedItems: this.props.selectedItems,
localSelectedItems: props.selectedItems,
}
this.onSelect = ::this.onSelect
this.onApplyFunctions = ::this.onApplyFunctions
}
handleClickOutside() {
this.setState({isOpen: false})
}
componentWillReceiveProps(nextProps) {
if (!_.isEqual(this.props.selectedItems, nextProps.selectedItems)) {
return
}
this.setState({localSelectedItems: nextProps.selectedItems})
}
toggleMenu = e => {
e.stopPropagation()
this.setState({isOpen: !this.state.isOpen})
}
onSelect(item, e) {
onSelect = (item, e) => {
e.stopPropagation()
const {onApply, isApplyShown} = this.props
const {localSelectedItems} = this.state
let nextItems
if (this.isSelected(item)) {
nextItems = localSelectedItems.filter(i => i !== item)
nextItems = localSelectedItems.filter(i => i.name !== item.name)
} else {
nextItems = [...localSelectedItems, item]
}
if (!isApplyShown) {
onApply(nextItems)
}
this.setState({localSelectedItems: nextItems})
}
isSelected(item) {
return !!this.state.localSelectedItems.find(text => text === item)
return !!this.state.localSelectedItems.find(({name}) => name === item.name)
}
onApplyFunctions(e) {
handleApply = e => {
e.stopPropagation()
this.setState({isOpen: false})
@ -91,18 +104,18 @@ class MultiSelectDropdown extends Component {
}
renderMenu() {
const {items} = this.props
return (
<ul className="dropdown-menu">
<li className="multi-select--apply">
<button
className="btn btn-xs btn-info"
onClick={this.onApplyFunctions}
>
const {items, isApplyShown} = this.props
const applyButton = isApplyShown
? <li className="multi-select--apply">
<button className="btn btn-xs btn-info" onClick={this.handleApply}>
Apply
</button>
</li>
: null
return (
<ul className="dropdown-menu">
{applyButton}
<FancyScrollbar
autoHide={false}
autoHeight={true}
@ -119,7 +132,7 @@ class MultiSelectDropdown extends Component {
>
<div className="multi-select--checkbox" />
<span>
{listItem}
{listItem.name}
</span>
</li>
)
@ -130,22 +143,34 @@ class MultiSelectDropdown extends Component {
}
}
const {arrayOf, func, string} = PropTypes
const {arrayOf, bool, func, shape, string} = PropTypes
MultiSelectDropdown.propTypes = {
onApply: func.isRequired,
items: arrayOf(string.isRequired).isRequired,
selectedItems: arrayOf(string.isRequired).isRequired,
items: arrayOf(
shape({
name: string.isRequired,
})
).isRequired,
selectedItems: arrayOf(
shape({
name: string.isRequired,
})
),
label: string,
buttonSize: string,
buttonColor: string,
customClass: string,
iconName: string,
isApplyShown: bool,
}
MultiSelectDropdown.defaultProps = {
buttonSize: 'btn-sm',
buttonColor: 'btn-default',
customClass: 'dropdown-160',
selectedItems: [],
isApplyShown: true,
}
export default OnClickOutside(MultiSelectDropdown)

View File

@ -102,11 +102,9 @@ const SideNav = React.createClass({
link={`${sourcePrefix}/alerts`}
>
<NavHeader link={`${sourcePrefix}/alerts`} title="Alerting" />
<NavListItem link={`${sourcePrefix}/alerts`}>
Alert History
</NavListItem>
<NavListItem link={`${sourcePrefix}/alerts`}>History</NavListItem>
<NavListItem link={`${sourcePrefix}/alert-rules`}>
Alert Rules
Create
</NavListItem>
</NavBlock>
<NavBlock icon="crown2" link={`${sourcePrefix}/admin`}>

View File

@ -11,11 +11,13 @@
@import 'theme/bootstrap-theme';
@import 'theme/reset';
// External
// Vendor
@import 'external/react-grid-layout';
@import 'external/fixed-data-table-base';
@import 'external/fixed-data-table-style';
@import 'external/fixed-data-table';
@import "external/codemirror";
@import "../../node_modules/codemirror/theme/material.css";
// Layout
@import 'layout/page';
@ -27,6 +29,7 @@
// Components
@import 'components/ceo-display-options';
@import 'components/confirm-buttons';
@import 'components/code-mirror-theme';
@import 'components/custom-time-range';
@import 'components/dygraphs';
@import 'components/fancy-scrollbars';

View File

@ -0,0 +1,124 @@
/*
Name: CHRONOGRAF YO
Author: Michael Kaminsky (http://github.com/mkaminsky11)
Original material color scheme by Mattia Astorino (https://github.com/equinusocio/material-theme)
*/
$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%;
height: 100%;
}
.cm-s-material.CodeMirror {
border-radius: 0 0 $radius $radius;
font-family: $code-font;
background-color: $g2-kevlar;
color: $c-neutrino;
font-weight: 600;
height: 100%;
}
.CodeMirror-vscrollbar {
@include custom-scrollbar-round($g2-kevlar,$g6-smoke);
}
.cm-s-material .CodeMirror-gutters {
background-color: fade-out($g4-onyx, 0.5);
border: none;
}
.CodeMirror-gutter.CodeMirror-linenumbers {
width: 60px;
}
.cm-s-material.CodeMirror .CodeMirror-sizer {
margin-left: 60px;
}
.cm-s-material.CodeMirror .CodeMirror-linenumber.CodeMirror-gutter-elt {
padding-right: 9px;
width: 46px;
color: $g8-storm;
}
.cm-s-material .CodeMirror-guttermarker, .cm-s-material .CodeMirror-guttermarker-subtle, .cm-s-material .CodeMirror-linenumber { color: rgb(83,127,126); }
.cm-s-material .CodeMirror-cursor {
width: 2px;
border: 0;
background-color: $g20-white;
box-shadow:
0 0 3px $c-laser,
0 0 6px $c-ocean,
0 0 11px $c-amethyst;
}
.cm-s-material div.CodeMirror-selected,
.cm-s-material.CodeMirror-focused div.CodeMirror-selected {
background-color: fade-out($g8-storm,0.7);
}
.cm-s-material .CodeMirror-line::selection, .cm-s-material .CodeMirror-line > span::selection, .cm-s-material .CodeMirror-line > span > span::selection { background: rgba(255, 255, 255, 0.10); }
.cm-s-material .CodeMirror-line::-moz-selection, .cm-s-material .CodeMirror-line > span::-moz-selection, .cm-s-material .CodeMirror-line > span > span::-moz-selection { background: rgba(255, 255, 255, 0.10); }
.cm-s-material .CodeMirror-activeline-background { background: rgba(0, 0, 0, 0); }
.cm-s-material .cm-keyword { color: $c-comet; }
.cm-s-material .cm-operator { color: $c-dreamsicle; }
.cm-s-material .cm-variable-2 { color: #80CBC4; }
.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: $c-laser; }
.cm-s-material .cm-builtin { color: #DECB6B; }
.cm-s-material .cm-atom { color: $c-viridian; }
.cm-s-material .cm-number { color: $c-daisy; }
.cm-s-material .cm-def { color: rgba(233, 237, 237, 1); }
.cm-s-material .cm-string { color: $c-krypton; }
.cm-s-material .cm-string-2 { color: #80CBC4; }
.cm-s-material .cm-comment { color: $g10-wolf; }
.cm-s-material .cm-variable { color: $c-laser; }
.cm-s-material .cm-tag { color: #80CBC4; }
.cm-s-material .cm-meta { color: #80CBC4; }
.cm-s-material .cm-attribute { color: #FFCB6B; }
.cm-s-material .cm-property { color: #80CBAE; }
.cm-s-material .cm-qualifier { color: #DECB6B; }
.cm-s-material .cm-variable-3, .cm-s-material .cm-type { color: #DECB6B; }
.cm-s-material .cm-tag { color: rgba(255, 83, 112, 1); }
.cm-s-material .cm-error {
color: rgba(255, 255, 255, 1.0);
background-color: #EC5F67;
}
.cm-s-material .CodeMirror-matchingbracket {
text-decoration: underline;
color: white !important;
}

340
ui/src/style/external/codemirror.scss vendored Normal file
View File

@ -0,0 +1,340 @@
/* BASICS */
.CodeMirror {
/* Set height, width, borders, and global font properties here */
font-family: monospace;
height: calc(100vh - 140px);
color: black;
}
/* PADDING */
.CodeMirror-lines {
padding: 4px 0; /* Vertical padding around content */
}
.CodeMirror pre {
padding: 0 4px; /* Horizontal padding of content */
}
.CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
background-color: white; /* The little square between H and V scrollbars */
}
/* GUTTER */
.CodeMirror-gutters {
border-right: 1px solid #ddd;
background-color: #f7f7f7;
white-space: nowrap;
}
.CodeMirror-linenumbers {}
.CodeMirror-linenumber {
padding: 0 3px 0 5px;
min-width: 20px;
text-align: right;
color: #999;
white-space: nowrap;
}
.CodeMirror-guttermarker { color: black; }
.CodeMirror-guttermarker-subtle { color: #999; }
/* CURSOR */
.CodeMirror-cursor {
border-left: 1px solid black;
border-right: none;
width: 0;
}
/* Shown when moving in bi-directional text */
.CodeMirror div.CodeMirror-secondarycursor {
border-left: 1px solid silver;
}
.cm-fat-cursor .CodeMirror-cursor {
width: auto;
border: 0 !important;
background: #7e7;
}
.cm-fat-cursor div.CodeMirror-cursors {
z-index: 1;
}
.cm-animate-fat-cursor {
width: auto;
border: 0;
-webkit-animation: blink 1.06s steps(1) infinite;
-moz-animation: blink 1.06s steps(1) infinite;
animation: blink 1.06s steps(1) infinite;
background-color: #7e7;
}
@-moz-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@-webkit-keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
@keyframes blink {
0% {}
50% { background-color: transparent; }
100% {}
}
/* Can style cursor different in overwrite (non-insert) mode */
.CodeMirror-overwrite .CodeMirror-cursor {}
.cm-tab { display: inline-block; text-decoration: inherit; }
.CodeMirror-rulers {
position: absolute;
left: 0; right: 0; top: -50px; bottom: -20px;
overflow: hidden;
}
.CodeMirror-ruler {
border-left: 1px solid #ccc;
top: 0; bottom: 0;
position: absolute;
}
/* DEFAULT THEME */
.cm-s-default .cm-header {color: blue;}
.cm-s-default .cm-quote {color: #090;}
.cm-negative {color: #d44;}
.cm-positive {color: #292;}
.cm-header, .cm-strong {font-weight: bold;}
.cm-em {font-style: italic;}
.cm-link {text-decoration: underline;}
.cm-strikethrough {text-decoration: line-through;}
.cm-s-default .cm-keyword {color: #708;}
.cm-s-default .cm-atom {color: #219;}
.cm-s-default .cm-number {color: #164;}
.cm-s-default .cm-def {color: #00f;}
.cm-s-default .cm-variable,
.cm-s-default .cm-punctuation,
.cm-s-default .cm-property,
.cm-s-default .cm-operator {}
.cm-s-default .cm-variable-2 {color: #05a;}
.cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;}
.cm-s-default .cm-comment {color: #a50;}
.cm-s-default .cm-string {color: #a11;}
.cm-s-default .cm-string-2 {color: #f50;}
.cm-s-default .cm-meta {color: #555;}
.cm-s-default .cm-qualifier {color: #555;}
.cm-s-default .cm-builtin {color: #30a;}
.cm-s-default .cm-bracket {color: #997;}
.cm-s-default .cm-tag {color: #170;}
.cm-s-default .cm-attribute {color: #00c;}
.cm-s-default .cm-hr {color: #999;}
.cm-s-default .cm-link {color: #00c;}
.cm-s-default .cm-error {color: #f00;}
.cm-invalidchar {color: #f00;}
.CodeMirror-composing { border-bottom: 2px solid; }
/* Default styles for common addons */
div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;}
div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;}
.CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); }
.CodeMirror-activeline-background {background: #e8f2ff;}
/* STOP */
/* The rest of this file contains styles related to the mechanics of
the editor. You probably shouldn't touch them. */
.CodeMirror {
position: relative;
overflow: hidden;
background: white;
}
.CodeMirror-scroll {
overflow: scroll !important; /* Things will break if this is overridden */
/* 30px is the magic margin used to hide the element's real scrollbars */
/* See overflow: hidden in .CodeMirror */
margin-bottom: -30px; margin-right: -30px;
padding-bottom: 30px;
height: 100%;
outline: none; /* Prevent dragging from highlighting the element */
position: relative;
}
.CodeMirror-sizer {
position: relative;
border-right: 30px solid transparent;
}
/* The fake, visible scrollbars. Used to force redraw during scrolling
before actual scrolling happens, thus preventing shaking and
flickering artifacts. */
.CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler {
position: absolute;
z-index: 6;
display: none;
}
.CodeMirror-vscrollbar {
right: 0; top: 0;
overflow-x: hidden;
overflow-y: scroll;
}
.CodeMirror-hscrollbar {
bottom: 0; left: 0;
overflow-y: hidden;
overflow-x: scroll;
}
.CodeMirror-scrollbar-filler {
right: 0; bottom: 0;
}
.CodeMirror-gutter-filler {
left: 0; bottom: 0;
}
.CodeMirror-gutters {
position: absolute; left: 0; top: 0;
min-height: 100%;
z-index: 3;
}
.CodeMirror-gutter {
white-space: normal;
height: 100%;
display: inline-block;
vertical-align: top;
margin-bottom: -30px;
}
.CodeMirror-gutter-wrapper {
position: absolute;
z-index: 4;
background: none !important;
border: none !important;
}
.CodeMirror-gutter-background {
position: absolute;
top: 0; bottom: 0;
z-index: 4;
}
.CodeMirror-gutter-elt {
position: absolute;
cursor: default;
z-index: 4;
}
.CodeMirror-gutter-wrapper ::selection { background-color: transparent }
.CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent }
.CodeMirror-lines {
cursor: text;
min-height: 1px; /* prevents collapsing before first draw */
}
.CodeMirror pre {
/* Reset some styles that the rest of the page might have set */
-moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0;
border-width: 0;
background: transparent;
font-family: inherit;
font-size: inherit;
margin: 0;
white-space: pre;
word-wrap: normal;
line-height: inherit;
color: inherit;
z-index: 2;
position: relative;
overflow: visible;
-webkit-tap-highlight-color: transparent;
-webkit-font-variant-ligatures: contextual;
font-variant-ligatures: contextual;
}
.CodeMirror-wrap pre {
word-wrap: break-word;
white-space: pre-wrap;
word-break: normal;
}
.CodeMirror-linebackground {
position: absolute;
left: 0; right: 0; top: 0; bottom: 0;
z-index: 0;
}
.CodeMirror-linewidget {
position: relative;
z-index: 2;
overflow: auto;
}
.CodeMirror-widget {}
.CodeMirror-rtl pre { direction: rtl; }
.CodeMirror-code {
outline: none;
}
/* Force content-box sizing for the elements where we expect it */
.CodeMirror-scroll,
.CodeMirror-sizer,
.CodeMirror-gutter,
.CodeMirror-gutters,
.CodeMirror-linenumber {
-moz-box-sizing: content-box;
box-sizing: content-box;
}
.CodeMirror-measure {
position: absolute;
width: 100%;
height: 0;
overflow: hidden;
visibility: hidden;
}
.CodeMirror-cursor {
position: absolute;
pointer-events: none;
}
.CodeMirror-measure pre { position: static; }
div.CodeMirror-cursors {
visibility: hidden;
position: relative;
z-index: 3;
}
div.CodeMirror-dragcursors {
visibility: visible;
}
.CodeMirror-focused div.CodeMirror-cursors {
visibility: visible;
}
.CodeMirror-selected { background: #d9d9d9; }
.CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; }
.CodeMirror-crosshair { cursor: crosshair; }
.CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; }
.CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; }
.cm-searching {
background: #ffa;
background: rgba(255, 255, 0, .4);
}
/* Used to force a border model for a node */
.cm-force-border { padding-right: .1px; }
@media print {
/* Hide the cursor when printing */
.CodeMirror div.CodeMirror-cursors {
visibility: hidden;
}
}
/* See issue #2901 */
.cm-tab-wrap-hack:after { content: ''; }
/* Help users use markselection to safely style text background */
span.CodeMirror-selectedtext { background: none; }

View File

@ -98,6 +98,16 @@
webpack-dev-middleware "^1.6.0"
webpack-hot-middleware "^2.13.2"
"@skidding/react-codemirror@^1.0.1":
version "1.0.1"
resolved "https://registry.yarnpkg.com/@skidding/react-codemirror/-/react-codemirror-1.0.1.tgz#ce7927a10248e2369f8bce03669b92d88fead797"
dependencies:
classnames "^2.2.5"
codemirror "^5.18.2"
create-react-class "^15.5.1"
lodash.isequal "^4.5.0"
prop-types "^15.5.4"
abab@^1.0.0:
version "1.0.3"
resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.3.tgz#b81de5f7274ec4e756d797cd834f303642724e5d"
@ -1794,6 +1804,10 @@ code-point-at@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
codemirror@^5.18.2:
version "5.28.0"
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.28.0.tgz#2978d9280d671351a4f5737d06bbd681a0fd6f83"
color-convert@^1.3.0:
version "1.8.2"
resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.8.2.tgz#be868184d7c8631766d54e7078e2672d7c7e3339"
@ -4402,6 +4416,10 @@ lodash.debounce@^3.1.1:
dependencies:
lodash._getnative "^3.0.0"
lodash.debounce@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af"
lodash.deburr@^3.0.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/lodash.deburr/-/lodash.deburr-3.2.0.tgz#6da8f54334a366a7cf4c4c76ef8d80aa1b365ed5"
@ -4462,6 +4480,10 @@ lodash.isequal@^4.0.0:
version "4.4.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.4.0.tgz#6295768e98e14dc15ce8d362ef6340db82852031"
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
lodash.keys@^3.0.0, lodash.keys@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@ -4559,6 +4581,12 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0:
dependencies:
js-tokens "^2.0.0"
loose-envify@^1.3.1:
version "1.3.1"
resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848"
dependencies:
js-tokens "^3.0.0"
loud-rejection@^1.0.0:
version "1.6.0"
resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f"
@ -5814,6 +5842,13 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.5.4:
version "15.5.10"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.10.tgz#2797dfc3126182e3a95e3dfbb2e893ddd7456154"
dependencies:
fbjs "^0.8.9"
loose-envify "^1.3.1"
prop-types@^15.5.6, prop-types@^15.5.8:
version "15.5.8"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
@ -5932,6 +5967,21 @@ react-addons-test-utils@^15.0.2:
version "15.4.1"
resolved "https://registry.yarnpkg.com/react-addons-test-utils/-/react-addons-test-utils-15.4.1.tgz#1e4caab151bf27cce26df5f9cb714f4fd8359ae1"
react-addons-update@^15.1.0:
version "15.4.1"
resolved "https://registry.yarnpkg.com/react-addons-update/-/react-addons-update-15.4.1.tgz#00c07f45243aa9715e1706bbfd1f23d3d8d80bd1"
react-codemirror@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/react-codemirror/-/react-codemirror-1.0.0.tgz#91467b53b1f5d80d916a2fd0b4c7adb85a9001ba"
dependencies:
classnames "^2.2.5"
codemirror "^5.18.2"
create-react-class "^15.5.1"
lodash.debounce "^4.0.8"
lodash.isequal "^4.5.0"
prop-types "^15.5.4"
react-custom-scrollbars@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/react-custom-scrollbars/-/react-custom-scrollbars-4.1.1.tgz#cf08cd43b1297ab11e6fcc5c9a800e7b70b6f248"