Skip to content

Commit 15ffb7a

Browse files
waleedlatif1claude
andcommitted
improvement(grafana): align tools and block with official Grafana API spec
Validates and corrects the Grafana integration against the official API docs: fixes wire-format field naming for provisioned alert rules (missing_series_evals_to_resolve, keepFiringFor, orgID), adds X-Disable-Provenance support, expands alert-rule params (isPaused, notificationSettings, record, annotations, labels), corrects defaults (execErrState=Error, dashboard overwrite=false), and centralizes alert-rule output mapping in a shared utils module. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent ec936be commit 15ffb7a

18 files changed

Lines changed: 1077 additions & 517 deletions

apps/docs/content/docs/en/tools/grafana.mdx

Lines changed: 146 additions & 34 deletions
Large diffs are not rendered by default.

apps/sim/blocks/blocks/grafana.ts

Lines changed: 253 additions & 23 deletions
Large diffs are not rendered by default.

apps/sim/tools/grafana/create_alert_rule.ts

Lines changed: 82 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type {
22
GrafanaCreateAlertRuleParams,
33
GrafanaCreateAlertRuleResponse,
44
} from '@/tools/grafana/types'
5+
import { alertRuleOutputFields, mapAlertRule } from '@/tools/grafana/utils'
56
import type { ToolConfig } from '@/tools/types'
67

78
export const createAlertRuleTool: ToolConfig<
@@ -52,9 +53,10 @@ export const createAlertRuleTool: ToolConfig<
5253
},
5354
condition: {
5455
type: 'string',
55-
required: true,
56+
required: false,
5657
visibility: 'user-or-llm',
57-
description: 'The refId of the query or expression to use as the alert condition',
58+
description:
59+
'The refId of the query or expression to use as the alert condition (required for alerting rules; omit for recording rules)',
5860
},
5961
data: {
6062
type: 'string',
@@ -78,7 +80,7 @@ export const createAlertRuleTool: ToolConfig<
7880
type: 'string',
7981
required: false,
8082
visibility: 'user-only',
81-
description: 'State on execution error (Alerting, OK)',
83+
description: 'State on execution error (Error, Alerting, OK)',
8284
},
8385
annotations: {
8486
type: 'string',
@@ -92,6 +94,48 @@ export const createAlertRuleTool: ToolConfig<
9294
visibility: 'user-or-llm',
9395
description: 'JSON object of labels',
9496
},
97+
uid: {
98+
type: 'string',
99+
required: false,
100+
visibility: 'user-or-llm',
101+
description: 'Optional custom UID for the alert rule',
102+
},
103+
isPaused: {
104+
type: 'boolean',
105+
required: false,
106+
visibility: 'user-only',
107+
description: 'Whether the rule is paused on creation',
108+
},
109+
keepFiringFor: {
110+
type: 'string',
111+
required: false,
112+
visibility: 'user-or-llm',
113+
description: 'Duration to keep firing after the condition stops (e.g., 5m)',
114+
},
115+
missingSeriesEvalsToResolve: {
116+
type: 'number',
117+
required: false,
118+
visibility: 'user-only',
119+
description: 'Number of missing series evaluations before resolving',
120+
},
121+
notificationSettings: {
122+
type: 'string',
123+
required: false,
124+
visibility: 'user-only',
125+
description: 'JSON object of per-rule notification settings (overrides)',
126+
},
127+
record: {
128+
type: 'string',
129+
required: false,
130+
visibility: 'user-or-llm',
131+
description: 'JSON object configuring this as a recording rule (omit for alerting rules)',
132+
},
133+
disableProvenance: {
134+
type: 'boolean',
135+
required: false,
136+
visibility: 'user-only',
137+
description: 'Set X-Disable-Provenance header so the rule remains editable in the Grafana UI',
138+
},
95139
},
96140

97141
request: {
@@ -105,25 +149,36 @@ export const createAlertRuleTool: ToolConfig<
105149
if (params.organizationId) {
106150
headers['X-Grafana-Org-Id'] = params.organizationId
107151
}
152+
if (params.disableProvenance) {
153+
headers['X-Disable-Provenance'] = 'true'
154+
}
108155
return headers
109156
},
110157
body: (params) => {
111-
let dataArray: any[] = []
158+
let dataArray: unknown[] = []
112159
try {
113160
dataArray = JSON.parse(params.data)
114161
} catch {
115162
throw new Error('Invalid JSON for data parameter')
116163
}
117164

118-
const body: Record<string, any> = {
165+
const body: Record<string, unknown> = {
166+
orgID: params.organizationId ? Number(params.organizationId) : 1,
119167
title: params.title,
120168
folderUID: params.folderUid,
121169
ruleGroup: params.ruleGroup,
122-
condition: params.condition,
123170
data: dataArray,
124-
for: params.forDuration || '5m',
125-
noDataState: params.noDataState || 'NoData',
126-
execErrState: params.execErrState || 'Alerting',
171+
}
172+
173+
if (params.condition) body.condition = params.condition
174+
if (params.uid) body.uid = params.uid
175+
if (params.forDuration) body.for = params.forDuration
176+
if (params.noDataState) body.noDataState = params.noDataState
177+
if (params.execErrState) body.execErrState = params.execErrState
178+
if (params.isPaused !== undefined) body.isPaused = params.isPaused
179+
if (params.keepFiringFor) body.keepFiringFor = params.keepFiringFor
180+
if (params.missingSeriesEvalsToResolve !== undefined) {
181+
body.missing_series_evals_to_resolve = params.missingSeriesEvalsToResolve
127182
}
128183

129184
if (params.annotations) {
@@ -142,53 +197,30 @@ export const createAlertRuleTool: ToolConfig<
142197
}
143198
}
144199

200+
if (params.notificationSettings) {
201+
try {
202+
body.notification_settings = JSON.parse(params.notificationSettings)
203+
} catch {
204+
// skip invalid notification settings JSON
205+
}
206+
}
207+
208+
if (params.record) {
209+
try {
210+
body.record = JSON.parse(params.record)
211+
} catch {
212+
// skip invalid record JSON
213+
}
214+
}
215+
145216
return body
146217
},
147218
},
148219

149220
transformResponse: async (response: Response) => {
150221
const data = await response.json()
151-
152-
return {
153-
success: true,
154-
output: {
155-
uid: data.uid,
156-
title: data.title,
157-
condition: data.condition,
158-
data: data.data,
159-
updated: data.updated,
160-
noDataState: data.noDataState,
161-
execErrState: data.execErrState,
162-
for: data.for,
163-
annotations: data.annotations || {},
164-
labels: data.labels || {},
165-
isPaused: data.isPaused || false,
166-
folderUID: data.folderUID,
167-
ruleGroup: data.ruleGroup,
168-
orgId: data.orgId,
169-
namespace_uid: data.namespace_uid,
170-
namespace_id: data.namespace_id,
171-
provenance: data.provenance || '',
172-
},
173-
}
222+
return { success: true, output: mapAlertRule(data) }
174223
},
175224

176-
outputs: {
177-
uid: {
178-
type: 'string',
179-
description: 'The UID of the created alert rule',
180-
},
181-
title: {
182-
type: 'string',
183-
description: 'Alert rule title',
184-
},
185-
folderUID: {
186-
type: 'string',
187-
description: 'Parent folder UID',
188-
},
189-
ruleGroup: {
190-
type: 'string',
191-
description: 'Rule group name',
192-
},
193-
},
225+
outputs: alertRuleOutputFields,
194226
}

apps/sim/tools/grafana/create_annotation.ts

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ export const createAnnotationTool: ToolConfig<
4646
},
4747
dashboardUid: {
4848
type: 'string',
49-
required: true,
49+
required: false,
5050
visibility: 'user-or-llm',
51-
description: 'UID of the dashboard to add the annotation to (e.g., abc123def)',
51+
description:
52+
'UID of the dashboard to add the annotation to (e.g., abc123def). Omit to create a global organization annotation.',
5253
},
5354
panelId: {
5455
type: 'number',
@@ -84,30 +85,22 @@ export const createAnnotationTool: ToolConfig<
8485
return headers
8586
},
8687
body: (params) => {
87-
const body: Record<string, any> = {
88+
const body: Record<string, unknown> = {
8889
text: params.text,
89-
time: params.time || Date.now(),
9090
}
9191

92+
if (params.time) body.time = params.time
93+
if (params.timeEnd) body.timeEnd = params.timeEnd
94+
if (params.dashboardUid) body.dashboardUID = params.dashboardUid
95+
if (params.panelId) body.panelId = params.panelId
96+
9297
if (params.tags) {
9398
body.tags = params.tags
9499
.split(',')
95100
.map((t) => t.trim())
96101
.filter((t) => t)
97102
}
98103

99-
if (params.dashboardUid) {
100-
body.dashboardUID = params.dashboardUid
101-
}
102-
103-
if (params.panelId) {
104-
body.panelId = params.panelId
105-
}
106-
107-
if (params.timeEnd) {
108-
body.timeEnd = params.timeEnd
109-
}
110-
111104
return body
112105
},
113106
},

apps/sim/tools/grafana/create_folder.ts

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
3939
visibility: 'user-or-llm',
4040
description: 'Optional UID for the folder (auto-generated if not provided)',
4141
},
42+
parentUid: {
43+
type: 'string',
44+
required: false,
45+
visibility: 'user-or-llm',
46+
description: 'Parent folder UID for nested folders (requires nested folders enabled)',
47+
},
4248
},
4349

4450
request: {
@@ -55,13 +61,12 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
5561
return headers
5662
},
5763
body: (params) => {
58-
const body: Record<string, any> = {
64+
const body: Record<string, unknown> = {
5965
title: params.title,
6066
}
6167

62-
if (params.uid) {
63-
body.uid = params.uid
64-
}
68+
if (params.uid) body.uid = params.uid
69+
if (params.parentUid) body.parentUid = params.parentUid
6570

6671
return body
6772
},
@@ -73,80 +78,80 @@ export const createFolderTool: ToolConfig<GrafanaCreateFolderParams, GrafanaCrea
7378
return {
7479
success: true,
7580
output: {
76-
id: data.id,
77-
uid: data.uid,
78-
title: data.title,
79-
url: data.url,
80-
hasAcl: data.hasAcl || false,
81-
canSave: data.canSave || false,
82-
canEdit: data.canEdit || false,
83-
canAdmin: data.canAdmin || false,
84-
canDelete: data.canDelete || false,
85-
createdBy: data.createdBy || '',
86-
created: data.created || '',
87-
updatedBy: data.updatedBy || '',
88-
updated: data.updated || '',
89-
version: data.version || 0,
81+
id: (data.id as number) ?? null,
82+
uid: (data.uid as string) ?? null,
83+
title: (data.title as string) ?? null,
84+
url: (data.url as string) ?? null,
85+
parentUid: (data.parentUid as string) ?? null,
86+
parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [],
87+
hasAcl: (data.hasAcl as boolean) ?? null,
88+
canSave: (data.canSave as boolean) ?? null,
89+
canEdit: (data.canEdit as boolean) ?? null,
90+
canAdmin: (data.canAdmin as boolean) ?? null,
91+
createdBy: (data.createdBy as string) ?? null,
92+
created: (data.created as string) ?? null,
93+
updatedBy: (data.updatedBy as string) ?? null,
94+
updated: (data.updated as string) ?? null,
95+
version: (data.version as number) ?? null,
9096
},
9197
}
9298
},
9399

94100
outputs: {
95-
id: {
96-
type: 'number',
97-
description: 'The numeric ID of the created folder',
98-
},
99-
uid: {
101+
id: { type: 'number', description: 'The numeric ID of the created folder' },
102+
uid: { type: 'string', description: 'The UID of the created folder' },
103+
title: { type: 'string', description: 'The title of the created folder' },
104+
url: { type: 'string', description: 'The URL path to the folder', optional: true },
105+
parentUid: {
100106
type: 'string',
101-
description: 'The UID of the created folder',
107+
description: 'Parent folder UID (nested folders only)',
108+
optional: true,
102109
},
103-
title: {
104-
type: 'string',
105-
description: 'The title of the created folder',
106-
},
107-
url: {
108-
type: 'string',
109-
description: 'The URL path to the folder',
110+
parents: {
111+
type: 'array',
112+
description: 'Ancestor folder hierarchy (nested folders only)',
113+
optional: true,
110114
},
111115
hasAcl: {
112116
type: 'boolean',
113117
description: 'Whether the folder has custom ACL permissions',
118+
optional: true,
114119
},
115120
canSave: {
116121
type: 'boolean',
117122
description: 'Whether the current user can save the folder',
123+
optional: true,
118124
},
119125
canEdit: {
120126
type: 'boolean',
121127
description: 'Whether the current user can edit the folder',
128+
optional: true,
122129
},
123130
canAdmin: {
124131
type: 'boolean',
125132
description: 'Whether the current user has admin rights on the folder',
126-
},
127-
canDelete: {
128-
type: 'boolean',
129-
description: 'Whether the current user can delete the folder',
133+
optional: true,
130134
},
131135
createdBy: {
132136
type: 'string',
133137
description: 'Username of who created the folder',
138+
optional: true,
134139
},
135140
created: {
136141
type: 'string',
137142
description: 'Timestamp when the folder was created',
143+
optional: true,
138144
},
139145
updatedBy: {
140146
type: 'string',
141147
description: 'Username of who last updated the folder',
148+
optional: true,
142149
},
143150
updated: {
144151
type: 'string',
145152
description: 'Timestamp when the folder was last updated',
153+
optional: true,
146154
},
147-
version: {
148-
type: 'number',
149-
description: 'Version number of the folder',
150-
},
155+
version: { type: 'number', description: 'Version number of the folder', optional: true },
151156
},
152157
}

0 commit comments

Comments
 (0)