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 alpull/5265/head
parent
7383f0c18a
commit
58b0b891ae
|
@ -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"
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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 "value" }}"
|
||||
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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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
|
|
@ -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 "value" }}"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default RuleMessageTextArea
|
|
@ -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}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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%;
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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";
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
25
ui/yarn.lock
25
ui/yarn.lock
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue