diff --git a/apps/docs/components/icons.tsx b/apps/docs/components/icons.tsx index 2d6b6eb2c03..b53f133f9ba 100644 --- a/apps/docs/components/icons.tsx +++ b/apps/docs/components/icons.tsx @@ -2936,12 +2936,11 @@ export function ClickHouseIcon(props: SVGProps) { export function MicrosoftIcon(props: SVGProps) { return ( - - - - - - + + + + + ) } diff --git a/apps/docs/content/docs/en/integrations/grafana.mdx b/apps/docs/content/docs/en/integrations/grafana.mdx index 4033539ff75..103d69dc265 100644 --- a/apps/docs/content/docs/en/integrations/grafana.mdx +++ b/apps/docs/content/docs/en/integrations/grafana.mdx @@ -7,7 +7,7 @@ import { BlockInfoCard } from "@/components/ui/block-info-card" {/* MANUAL-CONTENT-START:intro */} @@ -401,6 +401,34 @@ List all alert notification contact points | ↳ `disableResolveMessage` | boolean | Whether resolve messages are disabled | | ↳ `provenance` | string | Provisioning source \(empty if API-managed\) | +### `grafana_create_contact_point` + +Create a notification contact point (e.g., Slack, email, PagerDuty) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `name` | string | Yes | Name of the contact point \(groups receivers shown in the UI\) | +| `type` | string | Yes | Receiver type \(e.g., slack, email, pagerduty, webhook\) | +| `settings` | string | Yes | JSON object of type-specific settings \(e.g., \{"addresses":"a@b.com"\} for email, \{"url":"..."\} for slack\) | +| `disableResolveMessage` | boolean | No | Do not send a notification when the alert resolves | +| `disableProvenance` | boolean | No | Set X-Disable-Provenance header so the contact point remains editable in the UI | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `uid` | string | UID of the created contact point | +| `name` | string | Name of the contact point | +| `type` | string | Receiver type | +| `settings` | json | Type-specific settings | +| `disableResolveMessage` | boolean | Whether resolve notifications are suppressed | +| `provenance` | string | Provisioning source \(empty if API-managed\) | + ### `grafana_create_annotation` Create an annotation on a dashboard or as a global annotation @@ -584,6 +612,26 @@ Get a data source by its ID or UID | `version` | number | Data source version | | `readOnly` | boolean | Whether the data source is read-only | +### `grafana_check_data_source_health` + +Test connectivity to a data source by its UID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `dataSourceUid` | string | Yes | The UID of the data source to health-check \(e.g., P1234AB5678\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `status` | string | Health status of the data source \(e.g., OK\) | +| `message` | string | Detailed health message from the data source | + ### `grafana_list_folders` List all folders in Grafana @@ -655,4 +703,112 @@ Create a new folder in Grafana | `updated` | string | Timestamp when the folder was last updated | | `version` | number | Version number of the folder | +### `grafana_get_folder` + +Get a folder by its UID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `folderUid` | string | Yes | The UID of the folder to retrieve \(e.g., folder-abc123\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | The numeric ID of the folder | +| `uid` | string | The UID of the folder | +| `title` | string | The title of the folder | +| `url` | string | The URL path to the folder | +| `parentUid` | string | Parent folder UID \(nested folders only\) | +| `parents` | array | Ancestor folder hierarchy \(nested folders only\) | +| `hasAcl` | boolean | Whether the folder has custom ACL permissions | +| `canSave` | boolean | Whether the current user can save the folder | +| `canEdit` | boolean | Whether the current user can edit the folder | +| `canAdmin` | boolean | Whether the current user has admin rights on the folder | +| `createdBy` | string | Username of who created the folder | +| `created` | string | Timestamp when the folder was created | +| `updatedBy` | string | Username of who last updated the folder | +| `updated` | string | Timestamp when the folder was last updated | +| `version` | number | Version number of the folder | + +### `grafana_update_folder` + +Update (rename) a folder. Fetches the current folder and merges your changes. + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `folderUid` | string | Yes | The UID of the folder to update \(e.g., folder-abc123\) | +| `title` | string | Yes | New title for the folder | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `id` | number | The numeric ID of the folder | +| `uid` | string | The UID of the folder | +| `title` | string | The updated title of the folder | +| `url` | string | The URL path to the folder | +| `parentUid` | string | Parent folder UID \(nested folders only\) | +| `parents` | array | Ancestor folder hierarchy \(nested folders only\) | +| `hasAcl` | boolean | Whether the folder has custom ACL permissions | +| `canSave` | boolean | Whether the current user can save the folder | +| `canEdit` | boolean | Whether the current user can edit the folder | +| `canAdmin` | boolean | Whether the current user has admin rights on the folder | +| `createdBy` | string | Username of who created the folder | +| `created` | string | Timestamp when the folder was created | +| `updatedBy` | string | Username of who last updated the folder | +| `updated` | string | Timestamp when the folder was last updated | +| `version` | number | Version number of the folder | + +### `grafana_delete_folder` + +Delete a folder by its UID + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | +| `folderUid` | string | Yes | The UID of the folder to delete \(e.g., folder-abc123\) | +| `forceDeleteRules` | boolean | No | Delete any alert rules stored in the folder along with it \(default false\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `uid` | string | The UID of the deleted folder | +| `message` | string | Confirmation message | + +### `grafana_get_health` + +Check the health of the Grafana instance (version, database status) + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `apiKey` | string | Yes | Grafana Service Account Token | +| `baseUrl` | string | Yes | Grafana instance URL \(e.g., https://your-grafana.com\) | +| `organizationId` | string | No | Organization ID for multi-org Grafana instances \(e.g., 1, 2\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `commit` | string | Git commit hash of the running Grafana build | +| `database` | string | Database health status \(e.g., ok\) | +| `version` | string | Grafana version | + diff --git a/apps/sim/blocks/blocks/grafana.ts b/apps/sim/blocks/blocks/grafana.ts index 42753c8d1bb..9cb12c810ce 100644 --- a/apps/sim/blocks/blocks/grafana.ts +++ b/apps/sim/blocks/blocks/grafana.ts @@ -13,44 +13,43 @@ export const GrafanaBlock: BlockConfig = { docsLink: 'https://docs.sim.ai/integrations/grafana', category: 'tools', integrationType: IntegrationType.Observability, - bgColor: '#FFFFFF', + bgColor: '#F46800', icon: GrafanaIcon, subBlocks: [ - // Operation dropdown { id: 'operation', title: 'Operation', type: 'dropdown', options: [ - // Dashboards { label: 'List Dashboards', id: 'grafana_list_dashboards' }, { label: 'Get Dashboard', id: 'grafana_get_dashboard' }, { label: 'Create Dashboard', id: 'grafana_create_dashboard' }, { label: 'Update Dashboard', id: 'grafana_update_dashboard' }, { label: 'Delete Dashboard', id: 'grafana_delete_dashboard' }, - // Alerts { label: 'List Alert Rules', id: 'grafana_list_alert_rules' }, { label: 'Get Alert Rule', id: 'grafana_get_alert_rule' }, { label: 'Create Alert Rule', id: 'grafana_create_alert_rule' }, { label: 'Update Alert Rule', id: 'grafana_update_alert_rule' }, { label: 'Delete Alert Rule', id: 'grafana_delete_alert_rule' }, { label: 'List Contact Points', id: 'grafana_list_contact_points' }, - // Annotations + { label: 'Create Contact Point', id: 'grafana_create_contact_point' }, { label: 'Create Annotation', id: 'grafana_create_annotation' }, { label: 'List Annotations', id: 'grafana_list_annotations' }, { label: 'Update Annotation', id: 'grafana_update_annotation' }, { label: 'Delete Annotation', id: 'grafana_delete_annotation' }, - // Data Sources { label: 'List Data Sources', id: 'grafana_list_data_sources' }, { label: 'Get Data Source', id: 'grafana_get_data_source' }, - // Folders + { label: 'Check Data Source Health', id: 'grafana_check_data_source_health' }, { label: 'List Folders', id: 'grafana_list_folders' }, { label: 'Create Folder', id: 'grafana_create_folder' }, + { label: 'Get Folder', id: 'grafana_get_folder' }, + { label: 'Update Folder', id: 'grafana_update_folder' }, + { label: 'Delete Folder', id: 'grafana_delete_folder' }, + { label: 'Get Health', id: 'grafana_get_health' }, ], value: () => 'grafana_list_dashboards', }, - // Base Configuration (common to all operations) { id: 'baseUrl', title: 'Grafana URL', @@ -73,7 +72,6 @@ export const GrafanaBlock: BlockConfig = { placeholder: 'Optional - for multi-org instances', }, - // Data Source operations { id: 'dataSourceId', title: 'Data Source ID', @@ -85,8 +83,18 @@ export const GrafanaBlock: BlockConfig = { value: 'grafana_get_data_source', }, }, + { + id: 'dataSourceUid', + title: 'Data Source UID', + type: 'short-input', + placeholder: 'Enter data source UID', + required: true, + condition: { + field: 'operation', + value: 'grafana_check_data_source_health', + }, + }, - // Dashboard operations { id: 'dashboardUid', title: 'Dashboard UID', @@ -152,8 +160,25 @@ Return ONLY the search query - no explanations, no quotes, no extra text.`, value: ['grafana_list_dashboards', 'grafana_list_folders'], }, }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + placeholder: 'Maximum results to return', + mode: 'advanced', + condition: { + field: 'operation', + value: ['grafana_list_dashboards', 'grafana_list_folders', 'grafana_list_annotations'], + }, + }, + { + id: 'starred', + title: 'Only Starred', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_dashboards' }, + }, - // Create/Update Dashboard { id: 'title', title: 'Dashboard Title', @@ -268,7 +293,6 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, }, }, - // Alert Rule operations { id: 'alertRuleUid', title: 'Alert Rule UID', @@ -285,6 +309,7 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, title: 'Alert Title', type: 'short-input', placeholder: 'Enter alert rule name', + required: { field: 'operation', value: 'grafana_create_alert_rule' }, condition: { field: 'operation', value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], @@ -311,6 +336,7 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`, title: 'Rule Group', type: 'short-input', placeholder: 'Enter rule group name', + required: { field: 'operation', value: 'grafana_create_alert_rule' }, condition: { field: 'operation', value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], @@ -331,6 +357,7 @@ Return ONLY the alert title - no explanations, no quotes, no extra text.`, title: 'Query Data (JSON)', type: 'long-input', placeholder: 'JSON array of query/expression data objects', + required: { field: 'operation', value: 'grafana_create_alert_rule' }, condition: { field: 'operation', value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], @@ -509,11 +536,14 @@ Return ONLY the JSON array - no explanations, no markdown, no extra text.`, mode: 'advanced', condition: { field: 'operation', - value: ['grafana_create_alert_rule', 'grafana_update_alert_rule'], + value: [ + 'grafana_create_alert_rule', + 'grafana_update_alert_rule', + 'grafana_create_contact_point', + ], }, }, - // Annotation operations { id: 'text', title: 'Annotation Text', @@ -591,6 +621,19 @@ Return ONLY the annotation text - no explanations, no quotes, no extra text.`, mode: 'advanced', condition: { field: 'operation', value: 'grafana_list_annotations' }, }, + { + id: 'annotationType', + title: 'Type', + type: 'dropdown', + options: [ + { label: 'All', id: '' }, + { label: 'Alert', id: 'alert' }, + { label: 'Annotation', id: 'annotation' }, + ], + value: () => '', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_list_annotations' }, + }, { id: 'time', title: 'Time (epoch ms)', @@ -689,7 +732,6 @@ Return ONLY the numeric timestamp - no explanations, no quotes, no extra text.`, }, }, - // Folder operations { id: 'folderTitle', title: 'Folder Title', @@ -737,6 +779,33 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, mode: 'advanced', condition: { field: 'operation', value: 'grafana_list_folders' }, }, + { + id: 'manageFolderUid', + title: 'Folder UID', + type: 'short-input', + placeholder: 'Enter folder UID', + required: true, + condition: { + field: 'operation', + value: ['grafana_get_folder', 'grafana_update_folder', 'grafana_delete_folder'], + }, + }, + { + id: 'updateFolderTitle', + title: 'New Folder Title', + type: 'short-input', + placeholder: 'Enter new folder title', + required: { field: 'operation', value: 'grafana_update_folder' }, + condition: { field: 'operation', value: 'grafana_update_folder' }, + }, + { + id: 'forceDeleteRules', + title: 'Force Delete Alert Rules', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_delete_folder' }, + }, + { id: 'contactPointName', title: 'Contact Point Name', @@ -745,6 +814,60 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, mode: 'advanced', condition: { field: 'operation', value: 'grafana_list_contact_points' }, }, + { + id: 'contactPointNameNew', + title: 'Contact Point Name', + type: 'short-input', + placeholder: 'Enter contact point name', + required: true, + condition: { field: 'operation', value: 'grafana_create_contact_point' }, + }, + { + id: 'contactPointType', + title: 'Type', + type: 'dropdown', + options: [ + { label: 'Slack', id: 'slack' }, + { label: 'Email', id: 'email' }, + { label: 'PagerDuty', id: 'pagerduty' }, + { label: 'Webhook', id: 'webhook' }, + { label: 'Microsoft Teams', id: 'teams' }, + { label: 'Opsgenie', id: 'opsgenie' }, + { label: 'Discord', id: 'discord' }, + ], + value: () => 'slack', + required: true, + condition: { field: 'operation', value: 'grafana_create_contact_point' }, + }, + { + id: 'contactPointSettings', + title: 'Settings (JSON)', + type: 'long-input', + placeholder: 'JSON object of receiver settings (e.g., {"url":"https://hooks.slack.com/..."})', + required: true, + condition: { field: 'operation', value: 'grafana_create_contact_point' }, + wandConfig: { + enabled: true, + prompt: `Generate a Grafana contact point settings JSON object based on the user's description and receiver type. + +Examples by type: +- slack -> {"recipient":"#alerts","url":"https://hooks.slack.com/services/XXX"} +- email -> {"addresses":"oncall@example.com;sre@example.com"} +- pagerduty -> {"integrationKey":"YOUR_INTEGRATION_KEY","severity":"critical"} +- webhook -> {"url":"https://example.com/hook","httpMethod":"POST"} + +Return ONLY the JSON object - no explanations, no markdown, no extra text.`, + placeholder: 'Describe the notification target...', + generationType: 'json-object', + }, + }, + { + id: 'disableResolveMessage', + title: 'Disable Resolve Message', + type: 'switch', + mode: 'advanced', + condition: { field: 'operation', value: 'grafana_create_contact_point' }, + }, ], tools: { access: [ @@ -759,39 +882,96 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, 'grafana_update_alert_rule', 'grafana_delete_alert_rule', 'grafana_list_contact_points', + 'grafana_create_contact_point', 'grafana_create_annotation', 'grafana_list_annotations', 'grafana_update_annotation', 'grafana_delete_annotation', 'grafana_list_data_sources', 'grafana_get_data_source', + 'grafana_check_data_source_health', 'grafana_list_folders', 'grafana_create_folder', + 'grafana_get_folder', + 'grafana_update_folder', + 'grafana_delete_folder', + 'grafana_get_health', ], config: { tool: (params) => params.operation, params: (params) => { const result: Record = {} - if (params.alertTitle) result.title = params.alertTitle - if (params.folderTitle) result.title = params.folderTitle - if (params.folderUidNew) result.uid = params.folderUidNew - if (params.alertRuleUidNew) result.uid = params.alertRuleUidNew - if (params.parentUidNew) result.parentUid = params.parentUidNew - if (params.parentUidList) result.parentUid = params.parentUidList - if (params.contactPointName) result.name = params.contactPointName - if (params.annotationTags) result.tags = params.annotationTags - if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid - if (params.panelId) result.panelId = Number(params.panelId) - if (params.annotationId) result.annotationId = Number(params.annotationId) - if (params.alertId) result.alertId = Number(params.alertId) - if (params.userId) result.userId = Number(params.userId) - if (params.time) result.time = Number(params.time) - if (params.timeEnd) result.timeEnd = Number(params.timeEnd) - if (params.from) result.from = Number(params.from) - if (params.to) result.to = Number(params.to) - if (params.page) result.page = Number(params.page) - if (params.missingSeriesEvalsToResolve) { - result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve) + switch (params.operation) { + case 'grafana_list_dashboards': + if (params.page) result.page = Number(params.page) + if (params.limit) result.limit = Number(params.limit) + break + case 'grafana_create_alert_rule': + if (params.alertTitle) result.title = params.alertTitle + if (params.alertRuleUidNew) result.uid = params.alertRuleUidNew + if (params.missingSeriesEvalsToResolve) { + result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve) + } + break + case 'grafana_update_alert_rule': + if (params.alertTitle) result.title = params.alertTitle + if (params.missingSeriesEvalsToResolve) { + result.missingSeriesEvalsToResolve = Number(params.missingSeriesEvalsToResolve) + } + break + case 'grafana_list_contact_points': + if (params.contactPointName) result.name = params.contactPointName + break + case 'grafana_create_contact_point': + if (params.contactPointNameNew) result.name = params.contactPointNameNew + if (params.contactPointType) result.type = params.contactPointType + if (params.contactPointSettings) result.settings = params.contactPointSettings + break + case 'grafana_create_annotation': + if (params.annotationTags) result.tags = params.annotationTags + if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid + if (params.panelId) result.panelId = Number(params.panelId) + if (params.time) result.time = Number(params.time) + if (params.timeEnd) result.timeEnd = Number(params.timeEnd) + break + case 'grafana_update_annotation': + if (params.annotationTags) result.tags = params.annotationTags + if (params.annotationId) result.annotationId = Number(params.annotationId) + if (params.time) result.time = Number(params.time) + if (params.timeEnd) result.timeEnd = Number(params.timeEnd) + break + case 'grafana_delete_annotation': + if (params.annotationId) result.annotationId = Number(params.annotationId) + break + case 'grafana_list_annotations': + if (params.annotationTags) result.tags = params.annotationTags + if (params.annotationDashboardUid) result.dashboardUid = params.annotationDashboardUid + if (params.annotationType) result.type = params.annotationType + if (params.panelId) result.panelId = Number(params.panelId) + if (params.alertId) result.alertId = Number(params.alertId) + if (params.userId) result.userId = Number(params.userId) + if (params.from) result.from = Number(params.from) + if (params.to) result.to = Number(params.to) + if (params.limit) result.limit = Number(params.limit) + break + case 'grafana_list_folders': + if (params.parentUidList) result.parentUid = params.parentUidList + if (params.page) result.page = Number(params.page) + if (params.limit) result.limit = Number(params.limit) + break + case 'grafana_create_folder': + if (params.folderTitle) result.title = params.folderTitle + if (params.folderUidNew) result.uid = params.folderUidNew + if (params.parentUidNew) result.parentUid = params.parentUidNew + break + case 'grafana_get_folder': + case 'grafana_delete_folder': + if (params.manageFolderUid) result.folderUid = params.manageFolderUid + break + case 'grafana_update_folder': + if (params.manageFolderUid) result.folderUid = params.manageFolderUid + if (params.updateFolderTitle) result.title = params.updateFolderTitle + break } return result }, @@ -802,7 +982,6 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, baseUrl: { type: 'string', description: 'Grafana instance URL' }, apiKey: { type: 'string', description: 'Service Account Token' }, organizationId: { type: 'string', description: 'Organization ID (optional)' }, - // Dashboard inputs dashboardUid: { type: 'string', description: 'Dashboard UID' }, title: { type: 'string', description: 'Dashboard or folder title' }, folderUid: { type: 'string', description: 'Folder UID' }, @@ -817,7 +996,8 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, }, dashboardUIDs: { type: 'string', description: 'Filter by dashboard UIDs (comma-separated)' }, page: { type: 'number', description: 'Page number for pagination' }, - // Alert inputs + limit: { type: 'number', description: 'Maximum number of results to return' }, + starred: { type: 'boolean', description: 'Only return starred dashboards' }, alertRuleUid: { type: 'string', description: 'Alert rule UID' }, alertRuleUidNew: { type: 'string', description: 'Custom UID for newly created alert rule' }, alertTitle: { type: 'string', description: 'Alert rule title' }, @@ -848,7 +1028,6 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, annotations: { type: 'string', description: 'JSON of alert annotations' }, labels: { type: 'string', description: 'JSON of alert labels' }, overwrite: { type: 'boolean', description: 'Overwrite existing dashboard on version conflict' }, - // Annotation inputs text: { type: 'string', description: 'Annotation text' }, annotationId: { type: 'number', description: 'Annotation ID' }, annotationTags: { type: 'string', description: 'Annotation tags (comma-separated)' }, @@ -860,30 +1039,52 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, to: { type: 'number', description: 'Filter to time' }, alertId: { type: 'number', description: 'Filter annotations by alert ID' }, userId: { type: 'number', description: 'Filter annotations by creator user ID' }, - // Folder inputs + annotationType: { + type: 'string', + description: 'Filter annotations by type (alert or annotation)', + }, folderTitle: { type: 'string', description: 'Folder title for newly created folder' }, folderUidNew: { type: 'string', description: 'Custom UID for newly created folder' }, parentUidList: { type: 'string', description: 'Parent folder UID to list children of' }, parentUidNew: { type: 'string', description: 'Parent folder UID for newly created folder' }, - // Contact point inputs + manageFolderUid: { type: 'string', description: 'UID of the folder to get, update, or delete' }, + updateFolderTitle: { type: 'string', description: 'New title for the folder being updated' }, + forceDeleteRules: { + type: 'boolean', + description: 'Delete alert rules stored in the folder when deleting it', + }, contactPointName: { type: 'string', description: 'Filter contact points by name' }, - // Data source inputs + contactPointNameNew: { type: 'string', description: 'Name for the new contact point' }, + contactPointType: { + type: 'string', + description: 'Receiver type for the new contact point (e.g., slack, email)', + }, + contactPointSettings: { + type: 'string', + description: 'JSON of receiver-specific settings for the new contact point', + }, + disableResolveMessage: { + type: 'boolean', + description: 'Do not send a notification when the alert resolves', + }, dataSourceId: { type: 'string', description: 'Data source ID or UID' }, + dataSourceUid: { type: 'string', description: 'Data source UID for health checks' }, }, outputs: { - // Health outputs version: { type: 'string', description: 'Grafana version' }, database: { type: 'string', description: 'Database health status' }, - status: { type: 'string', description: 'Health status' }, - // Dashboard outputs + commit: { type: 'string', description: 'Git commit hash of the Grafana build' }, + status: { type: 'string', description: 'Health status (e.g., data source health)' }, dashboard: { type: 'json', description: 'Dashboard JSON' }, meta: { type: 'json', description: 'Dashboard metadata' }, dashboards: { type: 'json', description: 'List of dashboards' }, uid: { type: 'string', description: 'Created/updated UID' }, url: { type: 'string', description: 'Dashboard URL' }, - // Alert outputs rules: { type: 'json', description: 'Alert rules list' }, contactPoints: { type: 'json', description: 'Contact points list' }, + name: { type: 'string', description: 'Name of the created contact point' }, + type: { type: 'string', description: 'Type of the created contact point' }, + settings: { type: 'json', description: 'Contact point receiver settings' }, condition: { type: 'string', description: 'Alert condition refId' }, for: { type: 'string', description: 'Duration the condition must hold before firing' }, keepFiringFor: { @@ -904,14 +1105,10 @@ Return ONLY the folder title - no explanations, no quotes, no extra text.`, notification_settings: { type: 'json', description: 'Per-rule notification settings' }, record: { type: 'json', description: 'Recording rule configuration' }, updated: { type: 'string', description: 'Last update timestamp' }, - // Annotation outputs annotations: { type: 'json', description: 'Annotations list' }, id: { type: 'number', description: 'Annotation ID' }, - // Data source outputs dataSources: { type: 'json', description: 'Data sources list' }, - // Folder outputs folders: { type: 'json', description: 'Folders list' }, - // Common message: { type: 'string', description: 'Status message' }, }, } @@ -1007,7 +1204,7 @@ export const GrafanaBlockMeta = { name: 'audit-dashboards', description: 'List Grafana dashboards and folders and report data sources each depends on.', content: - '# Audit Dashboards\n\nInventory dashboards and the data sources they rely on.\n\n## Steps\n1. List folders and dashboards to build the full inventory.\n2. Get details for each dashboard of interest to read its panels and referenced data sources.\n3. List data sources and cross-reference to flag dashboards pointing at missing or deprecated sources.\n\n## Output\nReturn an inventory grouped by folder, each dashboard with its UID and the data sources it uses, plus a flagged list of dashboards with broken or unknown data source references.', + '# Audit Dashboards\n\nInventory dashboards and the data sources they rely on.\n\n## Steps\n1. List folders and dashboards to build the full inventory.\n2. Get details for each dashboard of interest to read its panels and referenced data sources.\n3. List data sources, then check the health of each one to flag dashboards pointing at unreachable or deprecated sources.\n\n## Output\nReturn an inventory grouped by folder, each dashboard with its UID and the data sources it uses, plus a flagged list of dashboards whose data sources failed their health check.', }, { name: 'provision-monitoring-folder', @@ -1016,5 +1213,12 @@ export const GrafanaBlockMeta = { content: '# Provision Monitoring Folder\n\nSet up an organized monitoring home for a new service or team.\n\n## Steps\n1. Create a folder with a descriptive title for the service or team.\n2. List data sources and pick the one the new dashboard should query.\n3. Create a dashboard inside the folder with starter panels for the key metrics.\n4. Get the dashboard back to confirm it was created in the right folder.\n\n## Output\nReturn the folder UID and the new dashboard UID and link. Note the data source the dashboard was wired to.', }, + { + name: 'provision-alerting', + description: + 'Stand up a Grafana alert rule and the contact point it notifies for a new service.', + content: + '# Provision Alerting\n\nWire up end-to-end alerting for a service: a notification target plus the rule that fires to it.\n\n## Steps\n1. List existing contact points to avoid duplicating one.\n2. Create a contact point for the destination (Slack, email, or PagerDuty) with its settings.\n3. Create an alert rule in the target folder with the query data, condition, and for-duration.\n4. Get the alert rule back to confirm it was created and is not paused.\n\n## Output\nReturn the new contact point UID and alert rule UID, with the data source and threshold the rule evaluates.', + }, ], } as const satisfies BlockMeta diff --git a/apps/sim/lib/integrations/integrations.json b/apps/sim/lib/integrations/integrations.json index 5f11ba3c480..54a932d1006 100644 --- a/apps/sim/lib/integrations/integrations.json +++ b/apps/sim/lib/integrations/integrations.json @@ -1,5 +1,5 @@ { - "updatedAt": "2026-06-15", + "updatedAt": "2026-06-16", "integrations": [ { "type": "onepassword", @@ -6812,7 +6812,7 @@ "name": "Grafana", "description": "Interact with Grafana dashboards, alerts, and annotations", "longDescription": "Integrate Grafana into workflows. Manage dashboards, alerts, annotations, data sources, folders, and monitor health status.", - "bgColor": "#FFFFFF", + "bgColor": "#F46800", "iconName": "GrafanaIcon", "docsUrl": "https://docs.sim.ai/integrations/grafana", "operations": [ @@ -6860,6 +6860,10 @@ "name": "List Contact Points", "description": "List all alert notification contact points" }, + { + "name": "Create Contact Point", + "description": "Create a notification contact point (e.g., Slack, email, PagerDuty)" + }, { "name": "Create Annotation", "description": "Create an annotation on a dashboard or as a global annotation" @@ -6884,6 +6888,10 @@ "name": "Get Data Source", "description": "Get a data source by its ID or UID" }, + { + "name": "Check Data Source Health", + "description": "Test connectivity to a data source by its UID" + }, { "name": "List Folders", "description": "List all folders in Grafana" @@ -6891,9 +6899,25 @@ { "name": "Create Folder", "description": "Create a new folder in Grafana" + }, + { + "name": "Get Folder", + "description": "Get a folder by its UID" + }, + { + "name": "Update Folder", + "description": "Update (rename) a folder. Fetches the current folder and merges your changes." + }, + { + "name": "Delete Folder", + "description": "Delete a folder by its UID" + }, + { + "name": "Get Health", + "description": "Check the health of the Grafana instance (version, database status)" } ], - "operationCount": 19, + "operationCount": 25, "triggers": [], "triggerCount": 0, "authType": "api-key", diff --git a/apps/sim/tools/grafana/check_data_source_health.ts b/apps/sim/tools/grafana/check_data_source_health.ts new file mode 100644 index 00000000000..2a026c9e77a --- /dev/null +++ b/apps/sim/tools/grafana/check_data_source_health.ts @@ -0,0 +1,75 @@ +import type { + GrafanaDataSourceHealthParams, + GrafanaDataSourceHealthResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const checkDataSourceHealthTool: ToolConfig< + GrafanaDataSourceHealthParams, + GrafanaDataSourceHealthResponse +> = { + id: 'grafana_check_data_source_health', + name: 'Grafana Check Data Source Health', + description: 'Test connectivity to a data source by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + dataSourceUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the data source to health-check (e.g., P1234AB5678)', + }, + }, + + request: { + url: (params) => + `${params.baseUrl.replace(/\/$/, '')}/api/datasources/uid/${params.dataSourceUid.trim()}/health`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + status: (data.status as string) ?? 'UNKNOWN', + message: (data.message as string) ?? '', + }, + } + }, + + outputs: { + status: { type: 'string', description: 'Health status of the data source (e.g., OK)' }, + message: { type: 'string', description: 'Detailed health message from the data source' }, + }, +} diff --git a/apps/sim/tools/grafana/create_alert_rule.ts b/apps/sim/tools/grafana/create_alert_rule.ts index 0c07eea7683..360cd74b31c 100644 --- a/apps/sim/tools/grafana/create_alert_rule.ts +++ b/apps/sim/tools/grafana/create_alert_rule.ts @@ -172,7 +172,7 @@ export const createAlertRuleTool: ToolConfig< if (params.organizationId) body.orgID = Number(params.organizationId) if (params.condition) body.condition = params.condition - if (params.uid) body.uid = params.uid + if (params.uid) body.uid = params.uid.trim() if (params.forDuration) body.for = params.forDuration if (params.noDataState) body.noDataState = params.noDataState if (params.execErrState) body.execErrState = params.execErrState diff --git a/apps/sim/tools/grafana/create_contact_point.ts b/apps/sim/tools/grafana/create_contact_point.ts new file mode 100644 index 00000000000..cd264b8ecc9 --- /dev/null +++ b/apps/sim/tools/grafana/create_contact_point.ts @@ -0,0 +1,132 @@ +import type { + GrafanaCreateContactPointParams, + GrafanaCreateContactPointResponse, +} from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const createContactPointTool: ToolConfig< + GrafanaCreateContactPointParams, + GrafanaCreateContactPointResponse +> = { + id: 'grafana_create_contact_point', + name: 'Grafana Create Contact Point', + description: 'Create a notification contact point (e.g., Slack, email, PagerDuty)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + name: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Name of the contact point (groups receivers shown in the UI)', + }, + type: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'Receiver type (e.g., slack, email, pagerduty, webhook)', + }, + settings: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: + 'JSON object of type-specific settings (e.g., {"addresses":"a@b.com"} for email, {"url":"..."} for slack)', + }, + disableResolveMessage: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Do not send a notification when the alert resolves', + }, + disableProvenance: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: + 'Set X-Disable-Provenance header so the contact point remains editable in the UI', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/contact-points`, + method: 'POST', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + if (params.disableProvenance) { + headers['X-Disable-Provenance'] = 'true' + } + return headers + }, + body: (params) => { + let settings: Record = {} + try { + settings = JSON.parse(params.settings) + } catch { + throw new Error('Invalid JSON for settings parameter') + } + + const body: Record = { + name: params.name, + type: params.type, + settings, + } + if (params.disableResolveMessage !== undefined) { + body.disableResolveMessage = params.disableResolveMessage + } + return body + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + uid: (data.uid as string) ?? '', + name: (data.name as string) ?? '', + type: (data.type as string) ?? '', + settings: (data.settings as Record) ?? {}, + disableResolveMessage: (data.disableResolveMessage as boolean) ?? false, + provenance: (data.provenance as string) ?? '', + }, + } + }, + + outputs: { + uid: { type: 'string', description: 'UID of the created contact point' }, + name: { type: 'string', description: 'Name of the contact point' }, + type: { type: 'string', description: 'Receiver type' }, + settings: { type: 'json', description: 'Type-specific settings' }, + disableResolveMessage: { + type: 'boolean', + description: 'Whether resolve notifications are suppressed', + }, + provenance: { type: 'string', description: 'Provisioning source (empty if API-managed)' }, + }, +} diff --git a/apps/sim/tools/grafana/delete_alert_rule.ts b/apps/sim/tools/grafana/delete_alert_rule.ts index 8910617b96a..68484c5244c 100644 --- a/apps/sim/tools/grafana/delete_alert_rule.ts +++ b/apps/sim/tools/grafana/delete_alert_rule.ts @@ -42,7 +42,7 @@ export const deleteAlertRuleTool: ToolConfig< request: { url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`, method: 'DELETE', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/grafana/delete_dashboard.ts b/apps/sim/tools/grafana/delete_dashboard.ts index 5c251150d38..f8da6dd0103 100644 --- a/apps/sim/tools/grafana/delete_dashboard.ts +++ b/apps/sim/tools/grafana/delete_dashboard.ts @@ -42,7 +42,7 @@ export const deleteDashboardTool: ToolConfig< request: { url: (params) => - `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`, method: 'DELETE', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/grafana/delete_folder.ts b/apps/sim/tools/grafana/delete_folder.ts new file mode 100644 index 00000000000..b2c851480c5 --- /dev/null +++ b/apps/sim/tools/grafana/delete_folder.ts @@ -0,0 +1,79 @@ +import type { GrafanaDeleteFolderParams, GrafanaDeleteFolderResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const deleteFolderTool: ToolConfig = + { + id: 'grafana_delete_folder', + name: 'Grafana Delete Folder', + description: 'Delete a folder by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + folderUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the folder to delete (e.g., folder-abc123)', + }, + forceDeleteRules: { + type: 'boolean', + required: false, + visibility: 'user-only', + description: 'Delete any alert rules stored in the folder along with it (default false)', + }, + }, + + request: { + url: (params) => { + const baseUrl = params.baseUrl.replace(/\/$/, '') + const query = params.forceDeleteRules ? '?forceDeleteRules=true' : '' + return `${baseUrl}/api/folders/${params.folderUid.trim()}${query}` + }, + method: 'DELETE', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response, params) => { + const data = await response.json().catch(() => ({})) + + return { + success: true, + output: { + uid: params?.folderUid?.trim() ?? '', + message: (data.message as string) ?? 'Folder deleted', + }, + } + }, + + outputs: { + uid: { type: 'string', description: 'The UID of the deleted folder' }, + message: { type: 'string', description: 'Confirmation message' }, + }, + } diff --git a/apps/sim/tools/grafana/get_alert_rule.ts b/apps/sim/tools/grafana/get_alert_rule.ts index 1389872c620..7a6d85b116f 100644 --- a/apps/sim/tools/grafana/get_alert_rule.ts +++ b/apps/sim/tools/grafana/get_alert_rule.ts @@ -42,7 +42,7 @@ export const getAlertRuleTool: ToolConfig - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`, method: 'GET', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/grafana/get_dashboard.ts b/apps/sim/tools/grafana/get_dashboard.ts index 7af81bd6780..e72b9d00e45 100644 --- a/apps/sim/tools/grafana/get_dashboard.ts +++ b/apps/sim/tools/grafana/get_dashboard.ts @@ -37,7 +37,7 @@ export const getDashboardTool: ToolConfig - `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`, method: 'GET', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/grafana/get_data_source.ts b/apps/sim/tools/grafana/get_data_source.ts index 1ef7bfaa1bc..d20f9ed7850 100644 --- a/apps/sim/tools/grafana/get_data_source.ts +++ b/apps/sim/tools/grafana/get_data_source.ts @@ -44,8 +44,6 @@ export const getDataSourceTool: ToolConfig< url: (params) => { const baseUrl = params.baseUrl.replace(/\/$/, '') const id = params.dataSourceId.trim() - // Numeric DB id route only matches purely-numeric ids up to int64 length; - // anything else is treated as a UID (Grafana UIDs are short slug strings). const isNumericId = /^\d+$/.test(id) && id.length <= 18 if (isNumericId) { return `${baseUrl}/api/datasources/${id}` diff --git a/apps/sim/tools/grafana/get_folder.ts b/apps/sim/tools/grafana/get_folder.ts new file mode 100644 index 00000000000..267bf5f6b42 --- /dev/null +++ b/apps/sim/tools/grafana/get_folder.ts @@ -0,0 +1,134 @@ +import type { GrafanaGetFolderParams, GrafanaGetFolderResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const getFolderTool: ToolConfig = { + id: 'grafana_get_folder', + name: 'Grafana Get Folder', + description: 'Get a folder by its UID', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + folderUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the folder to retrieve (e.g., folder-abc123)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders/${params.folderUid.trim()}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + id: (data.id as number) ?? null, + uid: (data.uid as string) ?? null, + title: (data.title as string) ?? null, + url: (data.url as string) ?? null, + parentUid: (data.parentUid as string) ?? null, + parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [], + hasAcl: (data.hasAcl as boolean) ?? null, + canSave: (data.canSave as boolean) ?? null, + canEdit: (data.canEdit as boolean) ?? null, + canAdmin: (data.canAdmin as boolean) ?? null, + createdBy: (data.createdBy as string) ?? null, + created: (data.created as string) ?? null, + updatedBy: (data.updatedBy as string) ?? null, + updated: (data.updated as string) ?? null, + version: (data.version as number) ?? null, + }, + } + }, + + outputs: { + id: { type: 'number', description: 'The numeric ID of the folder' }, + uid: { type: 'string', description: 'The UID of the folder' }, + title: { type: 'string', description: 'The title of the folder' }, + url: { type: 'string', description: 'The URL path to the folder', optional: true }, + parentUid: { + type: 'string', + description: 'Parent folder UID (nested folders only)', + optional: true, + }, + parents: { + type: 'array', + description: 'Ancestor folder hierarchy (nested folders only)', + optional: true, + }, + hasAcl: { + type: 'boolean', + description: 'Whether the folder has custom ACL permissions', + optional: true, + }, + canSave: { + type: 'boolean', + description: 'Whether the current user can save the folder', + optional: true, + }, + canEdit: { + type: 'boolean', + description: 'Whether the current user can edit the folder', + optional: true, + }, + canAdmin: { + type: 'boolean', + description: 'Whether the current user has admin rights on the folder', + optional: true, + }, + createdBy: { + type: 'string', + description: 'Username of who created the folder', + optional: true, + }, + created: { + type: 'string', + description: 'Timestamp when the folder was created', + optional: true, + }, + updatedBy: { + type: 'string', + description: 'Username of who last updated the folder', + optional: true, + }, + updated: { + type: 'string', + description: 'Timestamp when the folder was last updated', + optional: true, + }, + version: { type: 'number', description: 'Version number of the folder', optional: true }, + }, +} diff --git a/apps/sim/tools/grafana/get_health.ts b/apps/sim/tools/grafana/get_health.ts new file mode 100644 index 00000000000..5b35c529422 --- /dev/null +++ b/apps/sim/tools/grafana/get_health.ts @@ -0,0 +1,64 @@ +import type { GrafanaHealthCheckParams, GrafanaHealthCheckResponse } from '@/tools/grafana/types' +import type { ToolConfig } from '@/tools/types' + +export const getHealthTool: ToolConfig = { + id: 'grafana_get_health', + name: 'Grafana Get Health', + description: 'Check the health of the Grafana instance (version, database status)', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/health`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + + return { + success: true, + output: { + commit: (data.commit as string) ?? '', + database: (data.database as string) ?? '', + version: (data.version as string) ?? '', + }, + } + }, + + outputs: { + commit: { type: 'string', description: 'Git commit hash of the running Grafana build' }, + database: { type: 'string', description: 'Database health status (e.g., ok)' }, + version: { type: 'string', description: 'Grafana version' }, + }, +} diff --git a/apps/sim/tools/grafana/index.ts b/apps/sim/tools/grafana/index.ts index d911a193a8c..3dfd0c2285b 100644 --- a/apps/sim/tools/grafana/index.ts +++ b/apps/sim/tools/grafana/index.ts @@ -1,13 +1,18 @@ +import { checkDataSourceHealthTool } from '@/tools/grafana/check_data_source_health' import { createAlertRuleTool } from '@/tools/grafana/create_alert_rule' import { createAnnotationTool } from '@/tools/grafana/create_annotation' +import { createContactPointTool } from '@/tools/grafana/create_contact_point' import { createDashboardTool } from '@/tools/grafana/create_dashboard' import { createFolderTool } from '@/tools/grafana/create_folder' import { deleteAlertRuleTool } from '@/tools/grafana/delete_alert_rule' import { deleteAnnotationTool } from '@/tools/grafana/delete_annotation' import { deleteDashboardTool } from '@/tools/grafana/delete_dashboard' +import { deleteFolderTool } from '@/tools/grafana/delete_folder' import { getAlertRuleTool } from '@/tools/grafana/get_alert_rule' import { getDashboardTool } from '@/tools/grafana/get_dashboard' import { getDataSourceTool } from '@/tools/grafana/get_data_source' +import { getFolderTool } from '@/tools/grafana/get_folder' +import { getHealthTool } from '@/tools/grafana/get_health' import { listAlertRulesTool } from '@/tools/grafana/list_alert_rules' import { listAnnotationsTool } from '@/tools/grafana/list_annotations' import { listContactPointsTool } from '@/tools/grafana/list_contact_points' @@ -17,32 +22,35 @@ import { listFoldersTool } from '@/tools/grafana/list_folders' import { updateAlertRuleTool } from '@/tools/grafana/update_alert_rule' import { updateAnnotationTool } from '@/tools/grafana/update_annotation' import { updateDashboardTool } from '@/tools/grafana/update_dashboard' +import { updateFolderTool } from '@/tools/grafana/update_folder' -// Dashboard tools export const grafanaGetDashboardTool = getDashboardTool export const grafanaListDashboardsTool = listDashboardsTool export const grafanaCreateDashboardTool = createDashboardTool export const grafanaUpdateDashboardTool = updateDashboardTool export const grafanaDeleteDashboardTool = deleteDashboardTool -// Alert tools export const grafanaListAlertRulesTool = listAlertRulesTool export const grafanaGetAlertRuleTool = getAlertRuleTool export const grafanaCreateAlertRuleTool = createAlertRuleTool export const grafanaUpdateAlertRuleTool = updateAlertRuleTool export const grafanaDeleteAlertRuleTool = deleteAlertRuleTool export const grafanaListContactPointsTool = listContactPointsTool +export const grafanaCreateContactPointTool = createContactPointTool -// Annotation tools export const grafanaCreateAnnotationTool = createAnnotationTool export const grafanaListAnnotationsTool = listAnnotationsTool export const grafanaUpdateAnnotationTool = updateAnnotationTool export const grafanaDeleteAnnotationTool = deleteAnnotationTool -// Data Source tools export const grafanaListDataSourcesTool = listDataSourcesTool export const grafanaGetDataSourceTool = getDataSourceTool +export const grafanaCheckDataSourceHealthTool = checkDataSourceHealthTool -// Folder tools export const grafanaListFoldersTool = listFoldersTool export const grafanaCreateFolderTool = createFolderTool +export const grafanaGetFolderTool = getFolderTool +export const grafanaUpdateFolderTool = updateFolderTool +export const grafanaDeleteFolderTool = deleteFolderTool + +export const grafanaGetHealthTool = getHealthTool diff --git a/apps/sim/tools/grafana/types.ts b/apps/sim/tools/grafana/types.ts index dcab4638e69..a3467d69bd0 100644 --- a/apps/sim/tools/grafana/types.ts +++ b/apps/sim/tools/grafana/types.ts @@ -1,4 +1,3 @@ -// Common types for Grafana API tools import type { OutputProperty, ToolResponse } from '@/tools/types' /** @@ -43,17 +42,15 @@ export const ALERT_RULE_OUTPUT_FIELDS: Record = { }, } -// Common parameters for all Grafana tools interface GrafanaBaseParams { apiKey: string baseUrl: string organizationId?: string } -// Health Check types -interface GrafanaHealthCheckParams extends GrafanaBaseParams {} +export interface GrafanaHealthCheckParams extends GrafanaBaseParams {} -interface GrafanaHealthCheckResponse extends ToolResponse { +export interface GrafanaHealthCheckResponse extends ToolResponse { output: { commit: string database: string @@ -61,18 +58,17 @@ interface GrafanaHealthCheckResponse extends ToolResponse { } } -interface GrafanaDataSourceHealthParams extends GrafanaBaseParams { - dataSourceId: string +export interface GrafanaDataSourceHealthParams extends GrafanaBaseParams { + dataSourceUid: string } -interface GrafanaDataSourceHealthResponse extends ToolResponse { +export interface GrafanaDataSourceHealthResponse extends ToolResponse { output: { status: string message: string } } -// Dashboard types export interface GrafanaGetDashboardParams extends GrafanaBaseParams { dashboardUid: string } @@ -164,7 +160,7 @@ export interface GrafanaCreateDashboardParams extends GrafanaBaseParams { tags?: string timezone?: string refresh?: string - panels?: string // JSON string of panels array + panels?: string overwrite?: boolean message?: string } @@ -187,7 +183,7 @@ export interface GrafanaUpdateDashboardParams extends GrafanaBaseParams { tags?: string timezone?: string refresh?: string - panels?: string // JSON string of panels array + panels?: string overwrite?: boolean message?: string } @@ -215,7 +211,6 @@ export interface GrafanaDeleteDashboardResponse extends ToolResponse { } } -// Alert Rule types export interface GrafanaListAlertRulesParams extends GrafanaBaseParams {} interface GrafanaAlertRule { @@ -260,18 +255,18 @@ export interface GrafanaCreateAlertRuleParams extends GrafanaBaseParams { folderUid: string ruleGroup: string condition?: string - data: string // JSON string of data array + data: string forDuration?: string noDataState?: string execErrState?: string - annotations?: string // JSON string - labels?: string // JSON string + annotations?: string + labels?: string uid?: string isPaused?: boolean keepFiringFor?: string missingSeriesEvalsToResolve?: number - notificationSettings?: string // JSON string - record?: string // JSON string + notificationSettings?: string + record?: string disableProvenance?: boolean } @@ -285,17 +280,17 @@ export interface GrafanaUpdateAlertRuleParams extends GrafanaBaseParams { folderUid?: string ruleGroup?: string condition?: string - data?: string // JSON string of data array + data?: string forDuration?: string noDataState?: string execErrState?: string - annotations?: string // JSON string - labels?: string // JSON string + annotations?: string + labels?: string isPaused?: boolean keepFiringFor?: string missingSeriesEvalsToResolve?: number - notificationSettings?: string // JSON string - record?: string // JSON string + notificationSettings?: string + record?: string disableProvenance?: boolean } @@ -313,14 +308,13 @@ export interface GrafanaDeleteAlertRuleResponse extends ToolResponse { } } -// Annotation types export interface GrafanaCreateAnnotationParams extends GrafanaBaseParams { text: string - tags?: string // comma-separated + tags?: string dashboardUid?: string panelId?: number - time?: number // epoch ms - timeEnd?: number // epoch ms + time?: number + timeEnd?: number } interface GrafanaAnnotation { @@ -356,7 +350,7 @@ export interface GrafanaListAnnotationsParams extends GrafanaBaseParams { panelId?: number alertId?: number userId?: number - tags?: string // comma-separated + tags?: string type?: string limit?: number } @@ -370,7 +364,7 @@ export interface GrafanaListAnnotationsResponse extends ToolResponse { export interface GrafanaUpdateAnnotationParams extends GrafanaBaseParams { annotationId: number text?: string - tags?: string // comma-separated + tags?: string time?: number timeEnd?: number } @@ -392,7 +386,6 @@ export interface GrafanaDeleteAnnotationResponse extends ToolResponse { } } -// Data Source types export interface GrafanaListDataSourcesParams extends GrafanaBaseParams {} interface GrafanaDataSource { @@ -430,7 +423,6 @@ export interface GrafanaGetDataSourceResponse extends ToolResponse { output: GrafanaDataSource } -// Folder types export interface GrafanaListFoldersParams extends GrafanaBaseParams { limit?: number page?: number @@ -477,7 +469,35 @@ export interface GrafanaCreateFolderResponse extends ToolResponse { output: GrafanaFolder } -// Contact Points types +export interface GrafanaGetFolderParams extends GrafanaBaseParams { + folderUid: string +} + +export interface GrafanaGetFolderResponse extends ToolResponse { + output: GrafanaFolder +} + +export interface GrafanaUpdateFolderParams extends GrafanaBaseParams { + folderUid: string + title?: string +} + +export interface GrafanaUpdateFolderResponse extends ToolResponse { + output: GrafanaFolder +} + +export interface GrafanaDeleteFolderParams extends GrafanaBaseParams { + folderUid: string + forceDeleteRules?: boolean +} + +export interface GrafanaDeleteFolderResponse extends ToolResponse { + output: { + uid: string + message: string + } +} + export interface GrafanaListContactPointsParams extends GrafanaBaseParams { name?: string } @@ -497,7 +517,25 @@ export interface GrafanaListContactPointsResponse extends ToolResponse { } } -// Union type for all Grafana responses +export interface GrafanaCreateContactPointParams extends GrafanaBaseParams { + name: string + type: string + settings: string + disableResolveMessage?: boolean + disableProvenance?: boolean +} + +export interface GrafanaCreateContactPointResponse extends ToolResponse { + output: { + uid: string + name: string + type: string + settings: Record + disableResolveMessage: boolean + provenance: string + } +} + export type GrafanaResponse = | GrafanaHealthCheckResponse | GrafanaDataSourceHealthResponse @@ -519,4 +557,8 @@ export type GrafanaResponse = | GrafanaGetDataSourceResponse | GrafanaListFoldersResponse | GrafanaCreateFolderResponse + | GrafanaGetFolderResponse + | GrafanaUpdateFolderResponse + | GrafanaDeleteFolderResponse | GrafanaListContactPointsResponse + | GrafanaCreateContactPointResponse diff --git a/apps/sim/tools/grafana/update_alert_rule.ts b/apps/sim/tools/grafana/update_alert_rule.ts index ba474e2ed90..ee6ad5ee3e4 100644 --- a/apps/sim/tools/grafana/update_alert_rule.ts +++ b/apps/sim/tools/grafana/update_alert_rule.ts @@ -137,7 +137,7 @@ export const updateAlertRuleTool: ToolConfig - `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/v1/provisioning/alert-rules/${params.alertRuleUid.trim()}`, method: 'GET', headers: (params) => { const headers: Record = { @@ -266,7 +266,7 @@ export const updateAlertRuleTool: ToolConfig - `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid}`, + `${params.baseUrl.replace(/\/$/, '')}/api/dashboards/uid/${params.dashboardUid.trim()}`, method: 'GET', headers: (params) => { const headers: Record = { diff --git a/apps/sim/tools/grafana/update_folder.ts b/apps/sim/tools/grafana/update_folder.ts new file mode 100644 index 00000000000..fde0acb4c49 --- /dev/null +++ b/apps/sim/tools/grafana/update_folder.ts @@ -0,0 +1,192 @@ +import { + secureFetchWithPinnedIP, + validateUrlWithDNS, +} from '@/lib/core/security/input-validation.server' +import type { GrafanaUpdateFolderParams } from '@/tools/grafana/types' +import type { ToolConfig, ToolResponse } from '@/tools/types' + +export const updateFolderTool: ToolConfig = { + id: 'grafana_update_folder', + name: 'Grafana Update Folder', + description: 'Update (rename) a folder. Fetches the current folder and merges your changes.', + version: '1.0.0', + + params: { + apiKey: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana Service Account Token', + }, + baseUrl: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'Grafana instance URL (e.g., https://your-grafana.com)', + }, + organizationId: { + type: 'string', + required: false, + visibility: 'user-or-llm', + description: 'Organization ID for multi-org Grafana instances (e.g., 1, 2)', + }, + folderUid: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'The UID of the folder to update (e.g., folder-abc123)', + }, + title: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'New title for the folder', + }, + }, + + request: { + url: (params) => `${params.baseUrl.replace(/\/$/, '')}/api/folders/${params.folderUid.trim()}`, + method: 'GET', + headers: (params) => { + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + return headers + }, + }, + + transformResponse: async (response: Response) => { + const data = await response.json() + return { + success: true, + output: { _existingFolder: data }, + } + }, + + postProcess: async (result, params) => { + const existingFolder = result.output._existingFolder + + if (!existingFolder || !existingFolder.uid) { + return { success: false, output: {}, error: 'Failed to fetch existing folder' } + } + + const body: Record = { + title: params.title ?? existingFolder.title, + version: existingFolder.version, + overwrite: true, + } + + const headers: Record = { + 'Content-Type': 'application/json', + Authorization: `Bearer ${params.apiKey}`, + } + if (params.organizationId) { + headers['X-Grafana-Org-Id'] = params.organizationId + } + + const updateUrl = `${params.baseUrl.replace(/\/$/, '')}/api/folders/${params.folderUid.trim()}` + const urlValidation = await validateUrlWithDNS(updateUrl, 'baseUrl') + if (!urlValidation.isValid || !urlValidation.resolvedIP) { + return { + success: false, + output: {}, + error: `Invalid Grafana baseUrl: ${urlValidation.error}`, + } + } + + const updateResponse = await secureFetchWithPinnedIP(updateUrl, urlValidation.resolvedIP, { + method: 'PUT', + headers, + body: JSON.stringify(body), + }) + + if (!updateResponse.ok) { + const errorText = await updateResponse.text() + return { success: false, output: {}, error: `Failed to update folder: ${errorText}` } + } + + const data = (await updateResponse.json()) as Record + return { + success: true, + output: { + id: (data.id as number) ?? null, + uid: (data.uid as string) ?? null, + title: (data.title as string) ?? null, + url: (data.url as string) ?? null, + parentUid: (data.parentUid as string) ?? null, + parents: (data.parents as { uid: string; title: string; url: string }[]) ?? [], + hasAcl: (data.hasAcl as boolean) ?? null, + canSave: (data.canSave as boolean) ?? null, + canEdit: (data.canEdit as boolean) ?? null, + canAdmin: (data.canAdmin as boolean) ?? null, + createdBy: (data.createdBy as string) ?? null, + created: (data.created as string) ?? null, + updatedBy: (data.updatedBy as string) ?? null, + updated: (data.updated as string) ?? null, + version: (data.version as number) ?? null, + }, + } + }, + + outputs: { + id: { type: 'number', description: 'The numeric ID of the folder' }, + uid: { type: 'string', description: 'The UID of the folder' }, + title: { type: 'string', description: 'The updated title of the folder' }, + url: { type: 'string', description: 'The URL path to the folder', optional: true }, + parentUid: { + type: 'string', + description: 'Parent folder UID (nested folders only)', + optional: true, + }, + parents: { + type: 'array', + description: 'Ancestor folder hierarchy (nested folders only)', + optional: true, + }, + hasAcl: { + type: 'boolean', + description: 'Whether the folder has custom ACL permissions', + optional: true, + }, + canSave: { + type: 'boolean', + description: 'Whether the current user can save the folder', + optional: true, + }, + canEdit: { + type: 'boolean', + description: 'Whether the current user can edit the folder', + optional: true, + }, + canAdmin: { + type: 'boolean', + description: 'Whether the current user has admin rights on the folder', + optional: true, + }, + createdBy: { + type: 'string', + description: 'Username of who created the folder', + optional: true, + }, + created: { + type: 'string', + description: 'Timestamp when the folder was created', + optional: true, + }, + updatedBy: { + type: 'string', + description: 'Username of who last updated the folder', + optional: true, + }, + updated: { + type: 'string', + description: 'Timestamp when the folder was last updated', + optional: true, + }, + version: { type: 'number', description: 'Version number of the folder', optional: true }, + }, +} diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 1f069c3be05..997f6e64e57 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -1366,16 +1366,21 @@ import { listMattersTool, } from '@/tools/google_vault' import { + grafanaCheckDataSourceHealthTool, grafanaCreateAlertRuleTool, grafanaCreateAnnotationTool, + grafanaCreateContactPointTool, grafanaCreateDashboardTool, grafanaCreateFolderTool, grafanaDeleteAlertRuleTool, grafanaDeleteAnnotationTool, grafanaDeleteDashboardTool, + grafanaDeleteFolderTool, grafanaGetAlertRuleTool, grafanaGetDashboardTool, grafanaGetDataSourceTool, + grafanaGetFolderTool, + grafanaGetHealthTool, grafanaListAlertRulesTool, grafanaListAnnotationsTool, grafanaListContactPointsTool, @@ -1385,6 +1390,7 @@ import { grafanaUpdateAlertRuleTool, grafanaUpdateAnnotationTool, grafanaUpdateDashboardTool, + grafanaUpdateFolderTool, } from '@/tools/grafana' import { grainCreateHookTool, @@ -3998,14 +4004,20 @@ export const tools: Record = { grafana_update_alert_rule: grafanaUpdateAlertRuleTool, grafana_delete_alert_rule: grafanaDeleteAlertRuleTool, grafana_list_contact_points: grafanaListContactPointsTool, + grafana_create_contact_point: grafanaCreateContactPointTool, grafana_create_annotation: grafanaCreateAnnotationTool, grafana_list_annotations: grafanaListAnnotationsTool, grafana_update_annotation: grafanaUpdateAnnotationTool, grafana_delete_annotation: grafanaDeleteAnnotationTool, grafana_list_data_sources: grafanaListDataSourcesTool, grafana_get_data_source: grafanaGetDataSourceTool, + grafana_check_data_source_health: grafanaCheckDataSourceHealthTool, grafana_list_folders: grafanaListFoldersTool, grafana_create_folder: grafanaCreateFolderTool, + grafana_get_folder: grafanaGetFolderTool, + grafana_update_folder: grafanaUpdateFolderTool, + grafana_delete_folder: grafanaDeleteFolderTool, + grafana_get_health: grafanaGetHealthTool, google_search: googleSearchTool, greenhouse_list_candidates: greenhouseListCandidatesTool, greenhouse_get_candidate: greenhouseGetCandidateTool,