fix: alert rule message text template parsing (#5228)

* feat: add go text template parsing endpoint

* chore: yarn.lock

* chore(routes): change text validation endpoint name

* feat: validate kapacitor rule templates

* test(alerts): remove redundant tests

* chore(routes): update validate endpoint name

* test(routes): add validate endpoint to tests

* test(server): add validation endpoints

* feat(alerts): change template validation to 204

* chore: update react et al
pull/5265/head
Andrew Watkins 2019-07-09 12:31:56 -07:00 committed by Michael Desa
parent 7383f0c18a
commit 58b0b891ae
No known key found for this signature in database
GPG Key ID: 87002651EC5DFFE6
27 changed files with 526 additions and 292 deletions

View File

@ -2886,7 +2886,8 @@ func TestServer(t *testing.T) {
"ast": "/chronograf/v1/flux/ast",
"self": "/chronograf/v1/flux",
"suggestions": "/chronograf/v1/flux/suggestions"
}
},
"validateTextTemplates":"chronograf/v1/validate_text_templates"
}
`,
},
@ -2981,7 +2982,8 @@ func TestServer(t *testing.T) {
"ast": "/chronograf/v1/flux/ast",
"self": "/chronograf/v1/flux",
"suggestions": "/chronograf/v1/flux/suggestions"
}
},
"validateTextTemplates":"chronograf/v1/validate_text_templates"
}
`,
},

View File

@ -301,6 +301,7 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/dashboards/:id/cells/:cid", EnsureViewer(service.DashboardCellID))
router.DELETE("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.RemoveDashboardCell))
router.PUT("/chronograf/v1/dashboards/:id/cells/:cid", EnsureEditor(service.ReplaceDashboardCell))
// Dashboard Templates
router.GET("/chronograf/v1/dashboards/:id/templates", EnsureViewer(service.Templates))
router.POST("/chronograf/v1/dashboards/:id/templates", EnsureEditor(service.NewTemplate))
@ -337,6 +338,9 @@ func NewMux(opts MuxOpts, service Service) http.Handler {
router.GET("/chronograf/v1/env", EnsureViewer(service.Environment))
// Validates go templates for the js client
router.POST("/chronograf/v1/validate_text_templates", EnsureViewer(service.ValidateTextTemplate))
/// V2 Cells
router.GET("/chronograf/v2/cells", EnsureViewer(service.CellsV2))
router.POST("/chronograf/v2/cells", EnsureEditor(service.NewCellV2))

View File

@ -49,6 +49,7 @@ type getRoutesResponse struct {
ExternalLinks getExternalLinksResponse `json:"external"` // All external links for the client to use
OrganizationConfig getOrganizationConfigLinksResponse `json:"orgConfig"` // Location of the organization config endpoint
Flux getFluxLinksResponse `json:"flux"`
ValidTextTemplates string `json:"validateTextTemplates"` // Location of the valid text templates endpoint
}
// AllRoutes is a handler that returns all links to resources in Chronograf server, as well as
@ -111,6 +112,7 @@ func (a *AllRoutes) ServeHTTP(w http.ResponseWriter, r *http.Request) {
AST: "/chronograf/v1/flux/ast",
Suggestions: "/chronograf/v1/flux/suggestions",
},
ValidTextTemplates: "chronograf/v1/validate_text_templates",
}
// The JSON response will have no field present for the LogoutLink if there is no logout link.

View File

@ -29,7 +29,7 @@ func TestAllRoutes(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutes not able to unmarshal JSON response")
}
want := `{"protoboards":"/chronograf/v1/protoboards", "dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
want := `{"protoboards":"/chronograf/v1/protoboards", "dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}, "validateTextTemplates":"chronograf/v1/validate_text_templates"}
`
eq, err := jsonEqual(want, string(body))
@ -72,7 +72,7 @@ func TestAllRoutesWithAuth(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithAuth not able to unmarshal JSON response")
}
want := `{"protoboards":"/chronograf/v1/protoboards","dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
want := `{"protoboards":"/chronograf/v1/protoboards","dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[{"name":"github","label":"GitHub","login":"/oauth/github/login","logout":"/oauth/github/logout","callback":"/oauth/github/callback"}],"logout":"/oauth/logout","external":{"statusFeed":""},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"},"validateTextTemplates":"chronograf/v1/validate_text_templates"}
`
eq, err := jsonEqual(want, string(body))
if err != nil {
@ -109,7 +109,7 @@ func TestAllRoutesWithExternalLinks(t *testing.T) {
if err := json.Unmarshal(body, &routes); err != nil {
t.Error("TestAllRoutesWithExternalLinks not able to unmarshal JSON response")
}
want := `{"protoboards":"/chronograf/v1/protoboards","dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"}}
want := `{"protoboards":"/chronograf/v1/protoboards","dashboardsv2":"/chronograf/v2/dashboards","orgConfig":{"self":"/chronograf/v1/org_config","logViewer":"/chronograf/v1/org_config/logviewer"},"cells":"/chronograf/v2/cells","layouts":"/chronograf/v1/layouts","users":"/chronograf/v1/organizations/default/users","allUsers":"/chronograf/v1/users","organizations":"/chronograf/v1/organizations","mappings":"/chronograf/v1/mappings","sources":"/chronograf/v1/sources","me":"/chronograf/v1/me","environment":"/chronograf/v1/env","dashboards":"/chronograf/v1/dashboards","config":{"self":"/chronograf/v1/config","auth":"/chronograf/v1/config/auth"},"auth":[],"external":{"statusFeed":"http://pineapple.life/feed.json","custom":[{"name":"cubeapple","url":"https://cube.apple"}]},"flux":{"ast":"/chronograf/v1/flux/ast","self":"/chronograf/v1/flux","suggestions":"/chronograf/v1/flux/suggestions"},"validateTextTemplates":"chronograf/v1/validate_text_templates"}
`
eq, err := jsonEqual(want, string(body))
if err != nil {

View File

@ -0,0 +1,29 @@
package server
import (
"encoding/json"
"net/http"
"text/template"
)
// ValidTextTemplateRequest is the request json for validation
type ValidTextTemplateRequest struct {
Template string `json:"template"`
}
// ValidateTextTemplate will validate the template string
func (s *Service) ValidateTextTemplate(w http.ResponseWriter, r *http.Request) {
var req ValidTextTemplateRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
invalidJSON(w, s.Logger)
return
}
_, err := template.New("test_template").Parse(req.Template)
if err != nil {
Error(w, http.StatusBadRequest, err.Error(), s.Logger)
return
}
w.WriteHeader(http.StatusNoContent)
}

View File

@ -47,10 +47,10 @@
"@types/papaparse": "^4.1.34",
"@types/prop-types": "^15.5.2",
"@types/qs": "^6.5.1",
"@types/react": "^16.8.6",
"@types/react": "^16.8.23",
"@types/react-dnd": "^2.0.36",
"@types/react-dnd-html5-backend": "^2.1.9",
"@types/react-dom": "^16.0.7",
"@types/react-dom": "^16.8.4",
"@types/react-router": "^3.0.15",
"@types/react-router-redux": "4",
"@types/react-virtualized": "^9.18.3",

View File

@ -6,9 +6,9 @@ import {InjectedRouter} from 'react-router'
import {Page} from 'src/reusable_ui'
import NameSection from 'src/kapacitor/components/NameSection'
import ValuesSection from 'src/kapacitor/components/ValuesSection'
import RuleHeaderSave from 'src/kapacitor/components/RuleHeaderSave'
import RuleHandlers from 'src/kapacitor/components/RuleHandlers'
import RuleMessage from 'src/kapacitor/components/RuleMessage'
import RuleHeaderSave from 'src/kapacitor/components/alert_rules/RuleHeaderSave'
import RuleMessage from 'src/kapacitor/components/alert_rules/RuleMessage'
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
import {createRule, editRule} from 'src/kapacitor/apis'

View File

@ -1,47 +0,0 @@
import React, {SFC, ChangeEvent} from 'react'
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
import {AlertRule} from 'src/types'
interface Props {
rule: AlertRule
updateMessage: (e: ChangeEvent<HTMLTextAreaElement>) => void
}
const RuleMessageText: SFC<Props> = ({rule, updateMessage}) => {
const isValid = isValidMessage(rule.message)
const textAreaClass = isValid ? 'form-malachite' : 'form-volcano'
const iconName = isValid ? 'checkmark' : 'stop'
const validationCopy = isValid
? 'Alert message is syntactically correct.'
: 'Please correct templates in alert message.'
const outputStatusClass = isValid
? 'query-status-output--success'
: 'query-status-output--error'
return (
<div className="rule-builder--message">
<textarea
className={`form-control input-sm monotype ${textAreaClass}`}
onChange={updateMessage}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
value={rule.message}
spellCheck={false}
/>
{rule.message ? (
<div className="query-editor--status">
<span className={`query-status-output ${outputStatusClass}`}>
<span className={`icon ${iconName}`} />
{validationCopy}
</span>
</div>
) : null}
</div>
)
}
export default RuleMessageText

View File

@ -1,7 +1,7 @@
import React, {Component, ChangeEvent} from 'react'
import RuleMessageText from 'src/kapacitor/components/RuleMessageText'
import RuleMessageTemplates from 'src/kapacitor/components/RuleMessageTemplates'
import RuleMessageText from 'src/kapacitor/components/alert_rules/RuleMessageText'
import RuleMessageTemplates from 'src/kapacitor/components/alert_rules/RuleMessageTemplates'
import {ErrorHandling} from 'src/shared/decorators/errors'
import {AlertRule} from 'src/types'
@ -26,7 +26,7 @@ class RuleMessage extends Component<Props> {
<h3 className="rule-section--heading">Message</h3>
<div className="rule-section--body">
<RuleMessageText
rule={rule}
message={rule.message}
updateMessage={this.handleChangeMessage}
/>
<RuleMessageTemplates

View File

@ -0,0 +1,30 @@
// Libraries
import React, {FunctionComponent} from 'react'
// Components
import LoadingSpinner from 'src/reusable_ui/components/spinners/LoadingSpinner'
// Types
import {ValidationState} from 'src/types'
interface Props {
validationState: ValidationState
}
const RuleMessageIcon: FunctionComponent<Props> = ({validationState}) => {
if (validationState === ValidationState.Validating) {
return <LoadingSpinner diameter={15} />
}
if (validationState === ValidationState.Error) {
return <span className="icon stop" />
}
if (validationState === ValidationState.Success) {
return <span className="icon checkmark" />
}
return null
}
export default RuleMessageIcon

View File

@ -0,0 +1,35 @@
// Libraries
import React, {FunctionComponent} from 'react'
import classnames from 'classnames'
// Components
import RuleMessageIcon from './RuleMessageIcon'
// Types
import {ValidationState} from 'src/types'
interface Props {
validationText: string
validationState: ValidationState
}
const RuleMessageStatus: FunctionComponent<Props> = ({
validationText,
validationState,
}) => {
const className = classnames('query-status-output', {
'query-status-output--success': validationState === ValidationState.Success,
'query-status-output--error': validationState === ValidationState.Error,
})
return (
<div className="query-editor--status">
<span className={className}>
<RuleMessageIcon validationState={validationState} />
{validationText}
</span>
</div>
)
}
export default RuleMessageStatus

View File

@ -0,0 +1,73 @@
// Libraries
import React, {
FunctionComponent,
ChangeEvent,
useState,
useCallback,
} from 'react'
// Components
import RuleMessageStatus from 'src/kapacitor/components/alert_rules/RuleMessageStatus'
import RuleMessageTextArea from 'src/kapacitor/components/alert_rules/RuleMessageTextArea'
// Hooks
import {useMessageValidation} from 'src/kapacitor/components/alert_rules/hooks'
// Helpers
import DefaultDebouncer, {Debouncer} from 'src/shared/utils/debouncer'
// Types
import {TypingStatus} from 'src/types'
interface Props {
message: string
updateMessage: (e: ChangeEvent<HTMLTextAreaElement>) => void
}
const debouncer: Debouncer = new DefaultDebouncer()
const RuleMessageText: FunctionComponent<Props> = ({
message,
updateMessage,
}) => {
const [typingStatus, setTypingStatus] = useState(TypingStatus.NotStarted)
const validation = useMessageValidation(typingStatus, message)
const typingDone = useCallback(
() => {
setTypingStatus(TypingStatus.Done)
},
[setTypingStatus]
)
const onKeyUp = () => {
const waitForEndTypingMs = 500
debouncer.call(typingDone, waitForEndTypingMs)
}
const onChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
if (typingStatus !== TypingStatus.Started) {
setTypingStatus(TypingStatus.Started)
}
updateMessage(e)
}
return (
<div className="rule-builder--message">
<RuleMessageTextArea
message={message}
onKeyUp={onKeyUp}
onChange={onChange}
validationState={validation.state}
/>
{message && (
<RuleMessageStatus
validationState={validation.state}
validationText={validation.text}
/>
)}
</div>
)
}
export default RuleMessageText

View File

@ -0,0 +1,38 @@
// Libraries
import React, {FunctionComponent, ChangeEvent} from 'react'
import classnames from 'classnames'
// Types
import {ValidationState} from 'src/types'
interface Props {
message: string
validationState: ValidationState
onChange: (e: ChangeEvent<HTMLTextAreaElement>) => void
onKeyUp: () => void
}
const RuleMessageTextArea: FunctionComponent<Props> = ({
message,
onChange,
onKeyUp,
validationState,
}) => {
const className = classnames('form-control input-sm monotype', {
'form-volcano': validationState === ValidationState.Error,
'form-malachite': validationState === ValidationState.Success,
})
return (
<textarea
value={message}
onKeyUp={onKeyUp}
spellCheck={false}
onChange={onChange}
className={className}
placeholder="Example: {{ .ID }} is {{ .Level }} value: {{ index .Fields &quot;value&quot; }}"
/>
)
}
export default RuleMessageTextArea

View File

@ -0,0 +1,66 @@
// Libraries
import {useEffect, useState} from 'react'
import {get} from 'lodash'
// Types
import {TypingStatus, ValidationState} from 'src/types'
// Helpers
import isValidMessage from 'src/kapacitor/utils/alertMessageValidation'
// APIs
import {validateTextTemplate} from 'src/shared/apis/validate_templates'
// Copy
const messageValid = 'Alert message is syntactically correct.'
const messageInvalid = 'Please correct templates in alert message.'
const messageValidating = 'Validating template'
export const useMessageValidation = (
typingStatus: TypingStatus,
ruleMessage: string
) => {
// text is the validation text to display to the user
const [text, setValidationText] = useState('')
// state is the current state of go template parsing
const [state, setValidationState] = useState(ValidationState.NotStarted)
useEffect(
() => {
const validate = async () => {
if (typingStatus === TypingStatus.Started) {
setValidationState(ValidationState.Validating)
setValidationText(messageValidating)
}
if (typingStatus === TypingStatus.Done) {
try {
await validateTextTemplate(
'/chronograf/v1/validate_text_templates',
ruleMessage
)
const matchesVars = isValidMessage(ruleMessage)
if (matchesVars) {
setValidationText(messageValid)
setValidationState(ValidationState.Success)
} else {
setValidationText(messageInvalid)
setValidationState(ValidationState.Error)
}
} catch (error) {
const errorMessage = get(error, 'data.message', messageInvalid)
setValidationText(errorMessage)
setValidationState(ValidationState.Error)
}
}
}
validate()
},
[typingStatus]
)
return {text, state}
}

View File

@ -21,43 +21,7 @@ export const isValidTemplate = (template: string): boolean => {
return exactMatch || fuzzyMatch
}
export const mismatchedBrackets = (str: string): boolean => {
const arr = str.split('')
const accumulator: string[] = []
let isMismatched = false
arr.forEach(cur => {
if (cur === '{') {
accumulator.push('{')
}
if (cur === '}') {
const lastElt = accumulator.pop()
if (lastElt !== '{') {
isMismatched = true
}
}
})
if (accumulator.length !== 0) {
isMismatched = true
}
return isMismatched
}
export const isValidMessage = (message: string): boolean => {
if (message[message.length] === '}') {
message = message + ' '
}
const malformedTemplateRegexp1 = RegExp('(({{)([^{}]*)(})([^}]+))') // matches {{*} where star does not contain '{' or '}'
if (malformedTemplateRegexp1.test(message)) {
return false
}
if (mismatchedBrackets(message)) {
return false
}
const templateRegexp = /((?:{{)([^{}]*)(?:}}))/g // matches {{*}} where star does not contain '{' or '}'
const matches = []
let match = templateRegexp.exec(message)

View File

@ -0,0 +1,17 @@
// Libraries
import React, {FunctionComponent} from 'react'
interface Props {
diameter?: number
}
const LoadingSpinner: FunctionComponent<Props> = ({diameter = 30}) => {
const style = {width: `${diameter}px`, height: `${diameter}px`}
return (
<div className="loading-spinner--container" style={style}>
<div className="loading-spinner--circle" style={style} />
</div>
)
}
export default LoadingSpinner

View File

@ -0,0 +1,32 @@
/*
Loading Spinner
------------------------------------------------------------------------------
*/
@keyframes LoadingSpinner {
0% {
transform: translate(-50%, -50%) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg);
}
}
.loading-spinner--container {
display: inline-block;
position: relative;
}
.loading-spinner--circle {
pointer-events: none;
border: $ix-border solid rgba($g20-white, 0.25);
border-right-color: $g20-white;
border-radius: 50%;
animation-duration: 0.85s;
animation-name: LoadingSpinner;
animation-timing-function: linear;
animation-iteration-count: infinite;
position: absolute;
top: 50%;
left: 50%;
}

View File

@ -1,10 +1,11 @@
// Reusable UI Components
@import 'components/Button/Button';
@import 'components/dropdowns/Dropdown';
@import 'components/dropdowns/DropdownButton';
@import 'components/form_layout/Form';
@import 'components/inputs/Input';
@import 'components/overlays/Overlay';
@import 'components/panel/Panel';
@import 'components/slide_toggle/SlideToggle';
@import 'components/wizard/WizardCheckbox';
@import "components/Button/Button";
@import "components/dropdowns/Dropdown";
@import "components/dropdowns/DropdownButton";
@import "components/form_layout/Form";
@import "components/inputs/Input";
@import "components/overlays/Overlay";
@import "components/panel/Panel";
@import "components/slide_toggle/SlideToggle";
@import "components/wizard/WizardCheckbox";
@import "components/spinners/loadingSpinner";

View File

@ -0,0 +1,20 @@
import AJAX from 'src/utils/ajax'
export const validateTextTemplate = async (
url: string,
template: string
): Promise<string> => {
try {
const {data: validation} = await AJAX({
url,
method: 'POST',
data: {
template,
},
})
return validation
} catch (error) {
throw error
}
}

View File

@ -4,130 +4,129 @@
*/
// Modules
@import 'modules/modules';
@import "modules/modules";
// Fonts
@import 'fonts/fonts';
@import 'fonts/icon-font';
@import "fonts/fonts";
@import "fonts/icon-font";
// Theme
@import 'theme/chronograf-theme';
@import "theme/chronograf-theme";
// 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 "external/react-grid-layout";
@import "external/fixed-data-table-base";
@import "external/fixed-data-table-style";
@import "external/fixed-data-table";
@import "external/codemirror";
// Layout
@import 'layout/page-subsections';
@import 'layout/sidebar';
@import "layout/page-subsections";
@import "layout/sidebar";
// Components
@import 'components/annotations';
@import 'components/crosshairs';
@import 'components/ceo-display-options';
@import 'components/confirm-button';
@import 'components/confirm-or-cancel';
@import 'components/code-mirror/theme';
@import 'components/color-dropdown';
@import 'components/custom-time-range';
@import 'components/customize-fields';
@import 'components/drag-and-drop';
@import 'components/dygraphs';
@import 'components/fancy-scrollbars';
@import 'components/fancy-table';
@import 'components/fill-query';
@import 'components/flip-toggle';
@import 'components/function-selector';
@import 'components/graph-tips';
@import 'components/graph';
@import 'components/input-click-to-edit';
@import 'components/input-tag-list';
@import 'components/logs-viewer-options';
@import 'components/newsfeed';
@import 'components/opt-in';
@import 'components/organizations-table';
@import 'components/page-header-dropdown';
@import 'components/page-header-editable';
@import 'src/shared/components/PageSpinner';
@import 'components/single-stat';
@import 'components/static-legend';
@import 'components/query-maker';
@import 'components/react-tooltips';
@import 'components/redacted-input';
@import 'components/resizer';
@import 'components/search-widget';
@import 'components/info-indicators';
@import 'components/source-selector';
@import 'components/tables';
@import 'components/table-graph';
@import 'components/threesizer';
@import 'components/threshold-controls';
@import 'components/kapacitor-logs-table';
@import 'components/dropdown-placeholder';
@import 'components/histogram-chart';
@import 'src/sources/components/ConnectionLink';
@import 'src/sources/components/WelcomeStep';
@import 'src/sources/components/KapacitorStep';
@import 'src/shared/components/TimeMachine/CellNoteEditor';
@import 'src/shared/components/MarkdownCell';
@import 'src/sources/components/DashboardStep';
@import 'src/shared/components/DygraphLegend';
@import 'src/dashboards/components/GraphTypeSelector';
@import '../shared/components/dropdown_auto_refresh/AutoRefreshDropdown.scss';
@import '../dashboards/components/rename_dashboard/RenameDashboard.scss';
@import '../logs/components/logs_message/LogsMessage';
@import '../logs/components/expandable_message/ExpandableMessage';
@import '../dashboards/components/import_dashboard_mappings/ImportDashboardMappings.scss';
@import 'src/flux/components/flux_functions_toolbar/FluxFunctionsToolbar';
@import 'components/dashboard-empty';
@import 'components/template-control-bar';
@import 'components/edit-template-variable';
@import 'components/write-data-form';
@import 'components/annotation-control-bar';
@import 'components/annotation-editor';
@import 'src/shared/components/TimeMachine/RawFluxDataTable';
@import 'src/shared/components/TimeMachine/FluxScriptWizard';
@import 'src/shared/components/Spinner';
@import "components/annotations";
@import "components/crosshairs";
@import "components/ceo-display-options";
@import "components/confirm-button";
@import "components/confirm-or-cancel";
@import "components/code-mirror/theme";
@import "components/color-dropdown";
@import "components/custom-time-range";
@import "components/customize-fields";
@import "components/drag-and-drop";
@import "components/dygraphs";
@import "components/fancy-scrollbars";
@import "components/fancy-table";
@import "components/fill-query";
@import "components/flip-toggle";
@import "components/function-selector";
@import "components/graph-tips";
@import "components/graph";
@import "components/input-click-to-edit";
@import "components/input-tag-list";
@import "components/logs-viewer-options";
@import "components/newsfeed";
@import "components/opt-in";
@import "components/organizations-table";
@import "components/page-header-dropdown";
@import "components/page-header-editable";
@import "src/shared/components/PageSpinner";
@import "components/single-stat";
@import "components/static-legend";
@import "components/query-maker";
@import "components/react-tooltips";
@import "components/redacted-input";
@import "components/resizer";
@import "components/search-widget";
@import "components/info-indicators";
@import "components/source-selector";
@import "components/tables";
@import "components/table-graph";
@import "components/threesizer";
@import "components/threshold-controls";
@import "components/kapacitor-logs-table";
@import "components/dropdown-placeholder";
@import "components/histogram-chart";
@import "src/sources/components/ConnectionLink";
@import "src/sources/components/WelcomeStep";
@import "src/sources/components/KapacitorStep";
@import "src/shared/components/TimeMachine/CellNoteEditor";
@import "src/shared/components/MarkdownCell";
@import "src/sources/components/DashboardStep";
@import "src/shared/components/DygraphLegend";
@import "src/dashboards/components/GraphTypeSelector";
@import "../shared/components/dropdown_auto_refresh/AutoRefreshDropdown.scss";
@import "../dashboards/components/rename_dashboard/RenameDashboard.scss";
@import "../logs/components/logs_message/LogsMessage";
@import "../logs/components/expandable_message/ExpandableMessage";
@import "../dashboards/components/import_dashboard_mappings/ImportDashboardMappings.scss";
@import "src/flux/components/flux_functions_toolbar/FluxFunctionsToolbar";
@import "components/dashboard-empty";
@import "components/template-control-bar";
@import "components/edit-template-variable";
@import "components/write-data-form";
@import "components/annotation-control-bar";
@import "components/annotation-editor";
@import "src/shared/components/TimeMachine/RawFluxDataTable";
@import "src/shared/components/TimeMachine/FluxScriptWizard";
@import "src/shared/components/Spinner";
// Reusable UI Components
@import '../reusable_ui/components/panel/Panel.scss';
@import '../reusable_ui/components/overlays/Overlay.scss';
@import '../reusable_ui/components/card_select/CardSelectCard.scss';
@import '../reusable_ui/components/card_select/ProtoboardIcon.scss';
@import '../reusable_ui/components/grid_sizer/GridSizer.scss';
@import '../reusable_ui/components/wizard/WizardController.scss';
@import '../reusable_ui/components/wizard/WizardButtonBar.scss';
@import '../reusable_ui/components/wizard/WizardFullScreen.scss';
@import '../reusable_ui/components/wizard/WizardOverlay.scss';
@import '../reusable_ui/components/wizard/WizardCheckbox.scss';
@import '../reusable_ui/components/wizard/ProgressConnector.scss';
@import '../reusable_ui/components/wizard/WizardProgressBar.scss';
@import '../reusable_ui/components/wizard/WizardStep.scss';
@import '../reusable_ui/components/Button/Button.scss';
@import '../reusable_ui/components/radio_buttons/RadioButtons.scss';
@import '../reusable_ui/components/dropdowns/Dropdown.scss';
@import '../reusable_ui/components/slide_toggle/SlideToggle.scss';
@import '../reusable_ui/components/Button/Button.scss';
@import '../reusable_ui/components/dropdowns/DropdownButton.scss';
@import '../reusable_ui/components/inputs/Input.scss';
@import '../reusable_ui/components/form_layout/Form.scss';
@import '../reusable_ui/components/page_layout/Page.scss';
@import "../reusable_ui/components/panel/Panel.scss";
@import "../reusable_ui/components/overlays/Overlay.scss";
@import "../reusable_ui/components/card_select/CardSelectCard.scss";
@import "../reusable_ui/components/card_select/ProtoboardIcon.scss";
@import "../reusable_ui/components/grid_sizer/GridSizer.scss";
@import "../reusable_ui/components/wizard/WizardController.scss";
@import "../reusable_ui/components/wizard/WizardButtonBar.scss";
@import "../reusable_ui/components/wizard/WizardFullScreen.scss";
@import "../reusable_ui/components/wizard/WizardOverlay.scss";
@import "../reusable_ui/components/wizard/WizardCheckbox.scss";
@import "../reusable_ui/components/wizard/ProgressConnector.scss";
@import "../reusable_ui/components/wizard/WizardProgressBar.scss";
@import "../reusable_ui/components/wizard/WizardStep.scss";
@import "../reusable_ui/components/Button/Button.scss";
@import "../reusable_ui/components/radio_buttons/RadioButtons.scss";
@import "../reusable_ui/components/dropdowns/Dropdown.scss";
@import "../reusable_ui/components/slide_toggle/SlideToggle.scss";
@import "../reusable_ui/components/Button/Button.scss";
@import "../reusable_ui/components/dropdowns/DropdownButton.scss";
@import "../reusable_ui/components/inputs/Input.scss";
@import "../reusable_ui/components/form_layout/Form.scss";
@import "../reusable_ui/components/page_layout/Page.scss";
@import "../reusable_ui/components/spinners/loadingSpinner.scss";
// Pages
@import 'pages/config-endpoints';
@import 'pages/auth-page';
@import 'pages/kapacitor';
@import 'pages/dashboards';
@import 'pages/admin';
@import 'pages/users';
@import 'pages/tickscript-editor';
@import 'pages/time-machine';
@import 'pages/manage-providers';
@import 'pages/logs-viewer';
@import "pages/auth-page";
@import "pages/kapacitor";
@import "pages/dashboards";
@import "pages/admin";
@import "pages/users";
@import "pages/tickscript-editor";
@import "pages/time-machine";
@import "pages/manage-providers";
@import "pages/logs-viewer";
// TODO
@import 'unsorted';
@import "unsorted";

View File

@ -58,18 +58,30 @@
font-size: 12px;
font-family: $code-font;
span.icon {
span.icon,
.loading-spinner--container {
margin-right: 5px;
}
.loading-spinner--container {
top: 2px;
}
/* Error State */
&.query-status-output--error { color: $query-editor--status-error; }
&.query-status-output--error {
color: $query-editor--status-error;
}
/* Warning State */
&.query-status-output--warning { color: $query-editor--status-warning; }
&.query-status-output--warning {
color: $query-editor--status-warning;
}
/* Success State */
&.query-status-output--success { color: $query-editor--status-success; }
&.query-status-output--success {
color: $query-editor--status-success;
}
}
.dropdown.query-editor--templates {
margin: 0 4px 0 0 ;
margin: 0 4px 0 0;
.dropdown-menu {
left: initial;
@ -172,9 +184,7 @@
color: $c-comet;
border-radius: $radius-small;
cursor: pointer;
transition:
color 0.25s ease,
background-color 0.25s ease;
transition: color 0.25s ease, background-color 0.25s ease;
/* Selected State */
&.template-drawer--selected {
@ -183,7 +193,7 @@
}
.divider {
background: linear-gradient(to right, #00C9FF 0%, #22ADF6 100%);
background: linear-gradient(to right, #00c9ff 0%, #22adf6 100%);
}
}
@ -202,7 +212,7 @@
&:before,
&:after {
content: '';
content: "";
position: absolute;
width: 14px;
height: 3px;

View File

@ -1,4 +1,5 @@
export * from './app'
export * from 'src/types/kapacitor'
import {LayoutCell, LayoutQuery} from './layouts'
import {Service, NewService, ServiceLinks} from './services'
import {Links, Organization, Role, Permission, User, Me} from './auth'
@ -40,13 +41,6 @@ import {
Tags,
TagValues,
} from './queries'
import {
AlertRule,
Kapacitor,
Task,
RuleValues,
AlertRuleType,
} from './kapacitor'
import {
NewSource,
Source,
@ -102,9 +96,6 @@ export {
Tag,
Tags,
TagValues,
AlertRule,
AlertRuleType,
Kapacitor,
NewSource,
Source,
SourceLinks,
@ -112,8 +103,6 @@ export {
DropdownAction,
DropdownItem,
TimeRange,
Task,
RuleValues,
DygraphData,
DygraphSeries,
DygraphValue,

View File

@ -1,6 +1,19 @@
import {QueryConfig} from './'
import {AlertTypes} from 'src/kapacitor/constants'
export enum ValidationState {
NotStarted = 'Not Started',
Success = 'Success',
Error = 'Error',
Validating = 'Validating',
}
export enum TypingStatus {
NotStarted = 'Not Started',
Started = 'Started',
Done = 'Done',
}
export interface Kapacitor {
id?: string
url: string

View File

@ -1,6 +1,5 @@
import {
isValidMessage,
mismatchedBrackets,
isValidTemplate,
} from 'src/kapacitor/utils/alertMessageValidation'
import {RULE_MESSAGE_TEMPLATE_TEXTS} from 'src/kapacitor/constants'
@ -60,47 +59,6 @@ describe('kapacitor.utils.alertMessageValidation', () => {
expect(isValid).toEqual(true)
})
it('rejects message with invalid template', () => {
const isValid = isValidMessage('{{ I am invalid}}')
expect(isValid).toEqual(false)
})
it('rejects message containing template with missing closing bracket', () => {
const isValid = isValidMessage('{{ index .Tags "value" } {{.Name}}')
expect(isValid).toEqual(false)
})
it('rejects message containing non-matching brackets', () => {
const isValid = isValidMessage('{{ index .Tags "value" {{.Name}}')
expect(isValid).toEqual(false)
})
})
describe('mismatchedBrackets', () => {
it('String containing matched brackets is not mismatched', () => {
const isMismatched = mismatchedBrackets('{{}}')
expect(isMismatched).toEqual(false)
})
it('String containing matched brackets and other characters is not mismatched', () => {
const isMismatched = mismatchedBrackets('asdf{{asdfaasdas}}asdfa')
expect(isMismatched).toEqual(false)
})
it('String containing unmatched brackets is mismatched', () => {
const isMismatched = mismatchedBrackets('{{}')
expect(isMismatched).toEqual(true)
})
it('String containing unmatched brackets and other characters is mismatched', () => {
const isMismatched = mismatchedBrackets('asdf{{as}asdfa)')
expect(isMismatched).toEqual(true)
})
})
describe('isValidTemplate', () => {

View File

@ -859,12 +859,11 @@
dependencies:
"@types/react" "*"
"@types/react-dom@^16.0.7":
version "16.0.8"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.0.8.tgz#6e1366ed629cadf55860cbfcc25db533f5d2fa7d"
integrity sha512-WF/KAOia7pskV+J8f+UlNuFeCRkJuJAkyyeYPPtNe6suw0y7cWyUP/DPdPXsGUwQEkv2qlLVSrgVaoCm/PmO0Q==
"@types/react-dom@^16.8.4":
version "16.8.4"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.8.4.tgz#7fb7ba368857c7aa0f4e4511c4710ca2c5a12a88"
integrity sha512-eIRpEW73DCzPIMaNBDP5pPIpK1KXyZwNgfxiVagb5iGiz6da+9A5hslSX6GAQKdO7SayVCS/Fr2kjqprgAvkfA==
dependencies:
"@types/node" "*"
"@types/react" "*"
"@types/react-router-redux@4":
@ -899,10 +898,10 @@
"@types/prop-types" "*"
csstype "^2.2.0"
"@types/react@^16.8.6":
version "16.8.22"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.22.tgz#7f18bf5ea0c1cad73c46b6b1c804a3ce0eec6d54"
integrity sha512-C3O1yVqk4sUXqWyx0wlys76eQfhrQhiDhDlHBrjER76lR2S2Agiid/KpOU9oCqj1dISStscz7xXz1Cg8+sCQeA==
"@types/react@^16.8.23":
version "16.8.23"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.8.23.tgz#ec6be3ceed6353a20948169b6cb4c97b65b97ad2"
integrity sha512-abkEOIeljniUN9qB5onp++g0EY38h7atnDHxwKUFz1r3VH1+yG1OKi2sNPTyObL40goBmfKFpdii2lEzwLX1cA==
dependencies:
"@types/prop-types" "*"
csstype "^2.2.0"
@ -7910,14 +7909,14 @@ react-dnd@^2.6.0:
prop-types "^15.5.10"
react-dom@^16.5.1:
version "16.5.2"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.5.2.tgz#b69ee47aa20bab5327b2b9d7c1fe2a30f2cfa9d7"
integrity sha512-RC8LDw8feuZOHVgzEf7f+cxBr/DnKdqp56VU0lAs1f4UfKc4cU8wU4fTq/mgnvynLQo8OtlPC19NUFh/zjZPuA==
version "16.8.6"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-16.8.6.tgz#71d6303f631e8b0097f56165ef608f051ff6e10f"
integrity sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==
dependencies:
loose-envify "^1.1.0"
object-assign "^4.1.1"
prop-types "^15.6.2"
schedule "^0.5.0"
scheduler "^0.13.6"
react-draggable@3.x, "react-draggable@^2.2.6 || ^3.0.3":
version "3.0.5"