Skip to content

Commit 7f6a799

Browse files
authored
fix(3875): export only valid alerts to task, and provide user feedback. (#3916)
1 parent 9421f35 commit 7f6a799

File tree

4 files changed

+399
-295
lines changed

4 files changed

+399
-295
lines changed
Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
import React, {FC, useContext, useCallback, useState} from 'react'
2+
import {useDispatch} from 'react-redux'
3+
import {parse, format_from_js_file} from '@influxdata/flux-lsp-browser'
4+
5+
// Components
6+
import ExportTaskButton from 'src/flows/pipes/Schedule/ExportTaskButton'
7+
import {ENDPOINT_DEFINITIONS} from 'src/flows/pipes/Notification/endpoints'
8+
9+
// Contexts
10+
import {FlowQueryContext} from 'src/flows/context/flow.query'
11+
import {PipeContext} from 'src/flows/context/pipe'
12+
import {remove} from 'src/shared/contexts/query'
13+
14+
// Types
15+
import {
16+
deadmanType,
17+
THRESHOLD_TYPES,
18+
} from 'src/flows/pipes/Visualization/threshold'
19+
import {RemoteDataState} from 'src/types'
20+
21+
// Utils
22+
import {event} from 'src/cloud/utils/reporting'
23+
import {notify} from 'src/shared/actions/notifications'
24+
import {
25+
exportAlertToTaskSuccess,
26+
exportAlertToTaskFailure,
27+
} from 'src/shared/copy/notifications'
28+
29+
const ExportTask: FC = () => {
30+
const dispatch = useDispatch()
31+
const {id, data} = useContext(PipeContext)
32+
const {query, simplify, getPanelQueries} = useContext(FlowQueryContext)
33+
const [status, setStatus] = useState<RemoteDataState>(
34+
RemoteDataState.NotStarted
35+
)
36+
37+
const queryText = getPanelQueries(id)?.source
38+
39+
const generateDeadmanTask = useCallback(() => {
40+
// simplify takes care of all the variable nonsense in the query
41+
const ast = parse(simplify(queryText))
42+
43+
const [deadman] = data.thresholds
44+
45+
const vars = remove(
46+
ast,
47+
node => node.type === 'OptionStatement' && node.assignment.id.name === 'v'
48+
).reduce((acc, curr) => {
49+
// eslint-disable-next-line no-extra-semi
50+
;(curr.assignment?.init?.properties || []).reduce((_acc, _curr) => {
51+
if (_curr.key?.name && _curr.value?.location?.source) {
52+
_acc[_curr.key.name] = _curr.value.location.source
53+
}
54+
55+
return _acc
56+
}, acc)
57+
58+
return acc
59+
}, {})
60+
61+
vars.timeRangeStart = `-${deadman?.deadmanStopValue}`
62+
63+
const params = remove(
64+
ast,
65+
node =>
66+
node.type === 'OptionStatement' && node.assignment.id.name === 'task'
67+
).reduce((acc, curr) => {
68+
// eslint-disable-next-line no-extra-semi
69+
;(curr.assignment?.init?.properties || []).reduce((_acc, _curr) => {
70+
if (_curr.key?.name && _curr.value?.location?.source) {
71+
_acc[_curr.key.name] = _curr.value.location.source
72+
}
73+
74+
return _acc
75+
}, acc)
76+
77+
return acc
78+
}, {})
79+
80+
const conditions = THRESHOLD_TYPES[deadmanType].condition(deadman)
81+
82+
const newQuery = `import "strings"
83+
import "regexp"
84+
import "influxdata/influxdb/monitor"
85+
import "influxdata/influxdb/schema"
86+
import "influxdata/influxdb/secrets"
87+
import "experimental"
88+
${ENDPOINT_DEFINITIONS[data.endpoint]?.generateImports()}
89+
90+
check = {
91+
_check_id: "${id}",
92+
_check_name: "Notebook Generated Deadman Check",
93+
_type: "deadman",
94+
tags: {},
95+
}
96+
97+
notification = {
98+
_notification_rule_id: "${id}",
99+
_notification_rule_name: "Notebook Generated Rule",
100+
_notification_endpoint_id: "${id}",
101+
_notification_endpoint_name: "Notebook Generated Endpoint",
102+
}
103+
104+
task_data = ${format_from_js_file(ast)}
105+
trigger = ${conditions}
106+
messageFn = (r) => ("${data.message}")
107+
108+
${ENDPOINT_DEFINITIONS[data.endpoint]?.generateQuery(data.endpointData)}
109+
|> monitor["deadman"](t: experimental["subDuration"](from: now(), d: ${
110+
deadman.deadmanCheckValue
111+
}))`
112+
113+
const newAST = parse(newQuery)
114+
115+
if (!params.name) {
116+
params.name = `"Notebook Deadman Task for ${id}"`
117+
}
118+
119+
if (data.interval) {
120+
params.every = data.interval
121+
}
122+
123+
if (data.offset) {
124+
params.offset = data.offset
125+
}
126+
127+
if (Object.keys(vars).length) {
128+
const varString = Object.entries(vars)
129+
.map(([key, val]) => `${key}: ${val}`)
130+
.join(',\n')
131+
const varHeader = parse(`option v = {${varString}}\n`)
132+
newAST.body.unshift(varHeader.body[0])
133+
}
134+
135+
const paramString = Object.entries(params)
136+
.map(([key, val]) => `${key}: ${val}`)
137+
.join(',\n')
138+
const taskHeader = parse(`option task = {${paramString}}\n`)
139+
newAST.body.unshift(taskHeader.body[0])
140+
141+
return format_from_js_file(newAST)
142+
}, [
143+
id,
144+
queryText,
145+
data.every,
146+
data.offset,
147+
data.endpointData,
148+
data.endpoint,
149+
data.thresholds,
150+
data.message,
151+
])
152+
153+
const generateThresholdTask = useCallback(() => {
154+
// simplify takes care of all the variable nonsense in the query
155+
const ast = parse(simplify(queryText))
156+
157+
const vars = remove(
158+
ast,
159+
node => node.type === 'OptionStatement' && node.assignment.id.name === 'v'
160+
).reduce((acc, curr) => {
161+
// eslint-disable-next-line no-extra-semi
162+
;(curr.assignment?.init?.properties || []).reduce((_acc, _curr) => {
163+
if (_curr.key?.name && _curr.value?.location?.source) {
164+
_acc[_curr.key.name] = _curr.value.location.source
165+
}
166+
167+
return _acc
168+
}, acc)
169+
170+
return acc
171+
}, {})
172+
const params = remove(
173+
ast,
174+
node =>
175+
node.type === 'OptionStatement' && node.assignment.id.name === 'task'
176+
).reduce((acc, curr) => {
177+
// eslint-disable-next-line no-extra-semi
178+
;(curr.assignment?.init?.properties || []).reduce((_acc, _curr) => {
179+
if (_curr.key?.name && _curr.value?.location?.source) {
180+
_acc[_curr.key.name] = _curr.value.location.source
181+
}
182+
183+
return _acc
184+
}, acc)
185+
186+
return acc
187+
}, {})
188+
189+
const conditions = data.thresholds
190+
.map(threshold => THRESHOLD_TYPES[threshold.type].condition(threshold))
191+
.join(' and ')
192+
193+
const newQuery = `import "strings"
194+
import "regexp"
195+
import "influxdata/influxdb/monitor"
196+
import "influxdata/influxdb/schema"
197+
import "influxdata/influxdb/secrets"
198+
import "experimental"
199+
${ENDPOINT_DEFINITIONS[data.endpoint]?.generateImports()}
200+
201+
check = {
202+
_check_id: "${id}",
203+
_check_name: "Notebook Generated Check",
204+
_type: "custom",
205+
tags: {},
206+
}
207+
notification = {
208+
_notification_rule_id: "${id}",
209+
_notification_rule_name: "Notebook Generated Rule",
210+
_notification_endpoint_id: "${id}",
211+
_notification_endpoint_name: "Notebook Generated Endpoint",
212+
}
213+
214+
task_data = ${format_from_js_file(ast)}
215+
trigger = ${conditions}
216+
messageFn = (r) => ("${data.message}")
217+
218+
${ENDPOINT_DEFINITIONS[data.endpoint]?.generateQuery(data.endpointData)}`
219+
220+
const newAST = parse(newQuery)
221+
222+
if (!params.name) {
223+
params.name = `"Notebook Task for ${id}"`
224+
}
225+
226+
if (data.interval) {
227+
params.every = data.interval
228+
}
229+
230+
if (data.offset) {
231+
params.offset = data.offset
232+
}
233+
234+
if (Object.keys(vars).length) {
235+
const varString = Object.entries(vars)
236+
.map(([key, val]) => `${key}: ${val}`)
237+
.join(',\n')
238+
const varHeader = parse(`option v = {${varString}}\n`)
239+
newAST.body.unshift(varHeader.body[0])
240+
}
241+
242+
const paramString = Object.entries(params)
243+
.map(([key, val]) => `${key}: ${val}`)
244+
.join(',\n')
245+
const taskHeader = parse(`option task = {${paramString}}\n`)
246+
newAST.body.unshift(taskHeader.body[0])
247+
248+
return format_from_js_file(newAST)
249+
}, [
250+
id,
251+
queryText,
252+
data.every,
253+
data.offset,
254+
data.endpointData,
255+
data.endpoint,
256+
data.thresholds,
257+
data.message,
258+
])
259+
260+
const generateTask = useCallback(() => {
261+
event('Alert Panel (Notebooks) - Export Alert Task Clicked')
262+
263+
if (data.thresholds[0].type === deadmanType) {
264+
return generateDeadmanTask()
265+
} else {
266+
return generateThresholdTask()
267+
}
268+
}, [generateDeadmanTask, generateThresholdTask, data.thresholds])
269+
270+
const validateTask = async (queryText: string): Promise<boolean> => {
271+
try {
272+
setStatus(RemoteDataState.Loading)
273+
await query(queryText)
274+
275+
setStatus(RemoteDataState.Done)
276+
dispatch(notify(exportAlertToTaskSuccess(data.endpoint)))
277+
return true
278+
} catch {
279+
setStatus(RemoteDataState.Error)
280+
dispatch(notify(exportAlertToTaskFailure(data.endpoint)))
281+
return false
282+
}
283+
}
284+
285+
const handleTaskCreation = _ => {
286+
dispatch(notify(exportAlertToTaskSuccess(data.endpoint)))
287+
}
288+
289+
return (
290+
<ExportTaskButton
291+
loading={status == RemoteDataState.Loading}
292+
generate={generateTask}
293+
validate={validateTask}
294+
onCreate={handleTaskCreation}
295+
text="Export Alert Task"
296+
type="alert"
297+
/>
298+
)
299+
}
300+
301+
export default ExportTask

0 commit comments

Comments
 (0)