diff --git a/.github/helm-values.yml b/.github/helm-values.yml index c6619a5f..9d50177d 100644 --- a/.github/helm-values.yml +++ b/.github/helm-values.yml @@ -4,6 +4,8 @@ base_url_protocol: http env: - name: GRAFANA_CLOUD_NOTIFICATIONS_ENABLED value: "False" + - name: FEATURE_PROMETHEUS_EXPORTER_ENABLED + value: "True" image: tag: latest pullPolicy: Always @@ -104,3 +106,11 @@ service: type: NodePort port: 8080 nodePort: 30001 +prometheus: + enabled: true + extraScrapeConfigs: | + - job_name: 'oncall-exporter' + metrics_path: /metrics/ + static_configs: + - targets: + - oncall-dev-engine.default.svc.cluster.local:8080 diff --git a/CHANGELOG.md b/CHANGELOG.md index cecdeb86..8cbca088 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Move Insights to OnCall as a separate page ([#2382](https://github.com/grafana/oncall-private/issues/2382)) + ## v1.3.82 (2024-01-04) ### Added diff --git a/dev/helm-local.yml b/dev/helm-local.yml index 68674c37..7d6cf5b8 100644 --- a/dev/helm-local.yml +++ b/dev/helm-local.yml @@ -133,3 +133,11 @@ service: type: NodePort port: 8080 nodePort: 30001 +prometheus: + enabled: false + extraScrapeConfigs: | + - job_name: 'oncall-exporter' + metrics_path: /metrics/ + static_configs: + - targets: + - oncall-dev-engine.default.svc.cluster.local:8080 diff --git a/grafana-plugin/e2e-tests/insights/insights.test.ts b/grafana-plugin/e2e-tests/insights/insights.test.ts new file mode 100644 index 00000000..d83b9b59 --- /dev/null +++ b/grafana-plugin/e2e-tests/insights/insights.test.ts @@ -0,0 +1,65 @@ +import { test, expect } from '../fixtures'; +import { resolveFiringAlert } from '../utils/alertGroup'; +import { createEscalationChain, EscalationStep } from '../utils/escalationChain'; +import { clickButton, generateRandomValue } from '../utils/forms'; +import { createIntegrationAndSendDemoAlert } from '../utils/integrations'; +import { goToGrafanaPage, goToOnCallPage } from '../utils/navigation'; +import { createOnCallSchedule } from '../utils/schedule'; + +test.describe('Insights', () => { + test.beforeAll(async ({ adminRolePage: { page, userName } }) => { + const DATASOURCE_NAME = 'OnCall Prometheus'; + const DATASOURCE_URL = 'http://oncall-dev-prometheus-server.default.svc.cluster.local'; + + await goToGrafanaPage(page, '/connections/datasources'); + await page.waitForLoadState('networkidle'); + + // setup data source if it's not already connected + const isDataSourceAlreadyConnected = await page.getByText(DATASOURCE_NAME).isVisible(); + if (!isDataSourceAlreadyConnected) { + await page.getByRole('link', { name: 'Add data source' }).click(); + await clickButton({ page, buttonText: 'Prometheus' }); + await page.getByRole('textbox', { name: 'Data source settings page name input field' }).fill(DATASOURCE_NAME); + await page.getByPlaceholder('http://localhost:9090').fill(DATASOURCE_URL); + await clickButton({ page, buttonText: 'Save & test' }); + } + + // send alert and resolve to get some values in insights + const escalationChainName = generateRandomValue(); + const integrationName = generateRandomValue(); + const onCallScheduleName = generateRandomValue(); + await createOnCallSchedule(page, onCallScheduleName, userName); + await createEscalationChain( + page, + escalationChainName, + EscalationStep.NotifyUsersFromOnCallSchedule, + onCallScheduleName + ); + await createIntegrationAndSendDemoAlert(page, integrationName, escalationChainName); + await resolveFiringAlert(page); + }); + + test('Viewer can see all the panels in OnCall insights', async ({ viewerRolePage: { page } }) => { + await goToOnCallPage(page, 'insights'); + [ + 'Total alert groups', + 'Total alert groups by state', + 'New alert groups for selected period', + 'Mean time to respond \\(MTTR\\)', + 'MTTR changed for period', + 'New alert groups during time period', + 'Alert groups by Integration', + 'Mean time to respond \\(MTTR\\) by Integration', + ].forEach(async (panelTitle) => { + await expect(page.getByRole('heading', { name: new RegExp(`^${panelTitle}$`) }).first()).toBeVisible(); + }); + }); + + test('There is no panel that misses data', async ({ adminRolePage: { page } }) => { + await goToOnCallPage(page, 'insights'); + await page.getByText('Last 7 days').click(); + await page.getByText('Last 1 hour').click(); + await page.waitForTimeout(2000); + await expect(page.getByText('No data')).toBeHidden(); + }); +}); diff --git a/grafana-plugin/e2e-tests/utils/alertGroup.ts b/grafana-plugin/e2e-tests/utils/alertGroup.ts index 33ef2462..2711c896 100644 --- a/grafana-plugin/e2e-tests/utils/alertGroup.ts +++ b/grafana-plugin/e2e-tests/utils/alertGroup.ts @@ -98,3 +98,9 @@ export const verifyThatAlertGroupIsTriggered = async ( expect(await incidentTimelineContainsStep(page, triggeredStepText)).toBe(true); }; + +export const resolveFiringAlert = async (page: Page) => { + await goToOnCallPage(page, 'alert-groups'); + await page.getByText('Firing').nth(1).click(); + await page.getByLabel('Context menu').getByText('Resolve').click(); +}; diff --git a/grafana-plugin/e2e-tests/utils/navigation.ts b/grafana-plugin/e2e-tests/utils/navigation.ts index d7ac6fa8..2404eb65 100644 --- a/grafana-plugin/e2e-tests/utils/navigation.ts +++ b/grafana-plugin/e2e-tests/utils/navigation.ts @@ -2,12 +2,11 @@ import type { Page } from '@playwright/test'; import { BASE_URL } from './constants'; -type GrafanaPage = '/plugins/grafana-oncall-app'; -type OnCallPage = 'alert-groups' | 'integrations' | 'escalations' | 'schedules' | 'users'; +type OnCallPage = 'alert-groups' | 'integrations' | 'escalations' | 'schedules' | 'users' | 'insights'; const _goToPage = async (page: Page, url = '') => page.goto(`${BASE_URL}${url}`); -export const goToGrafanaPage = async (page: Page, url: GrafanaPage) => _goToPage(page, url); +export const goToGrafanaPage = async (page: Page, url = '') => _goToPage(page, url); export const goToOnCallPage = async (page: Page, onCallPage: OnCallPage) => { await _goToPage(page, `/a/grafana-oncall-app/${onCallPage}`); diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 5aed5207..24b7386a 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -124,6 +124,8 @@ "@grafana/faro-web-tracing": "^1.0.0-beta4", "@grafana/labels": "~1.4.4", "@grafana/runtime": "9.3.0-beta1", + "@grafana/scenes": "^1.28.0", + "@grafana/schema": "^10.2.2", "@grafana/ui": "^10.2.0", "@lifeomic/attempt": "^3.0.3", "@opentelemetry/api": "^1.3.0", diff --git a/grafana-plugin/src/containers/Alerts/Alerts.tsx b/grafana-plugin/src/containers/Alerts/Alerts.tsx index f14bdecc..2a343933 100644 --- a/grafana-plugin/src/containers/Alerts/Alerts.tsx +++ b/grafana-plugin/src/containers/Alerts/Alerts.tsx @@ -147,7 +147,7 @@ export default function Alerts() { function showMismatchWarning(): boolean { return ( - store.isOpenSource() && + store.isOpenSource && store.backendVersion && plugin?.version && store.backendVersion !== plugin?.version && diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.test.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.test.tsx index 321358e6..bd5c5461 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.test.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.test.tsx @@ -54,7 +54,7 @@ const mockUseStore = (rest?: any, connected = false, cloud_connected = true) => cloudConnectionStatus: { cloud_connection_status: cloud_connected }, } as unknown as CloudStore, hasFeature: jest.fn().mockReturnValue(true), - isOpenSource: jest.fn().mockReturnValue(true), + isOpenSource: true, } as unknown as RootStore; useStore.mockReturnValue(store); diff --git a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx index 9518a2b8..82ec2674 100644 --- a/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx +++ b/grafana-plugin/src/containers/MobileAppConnection/MobileAppConnection.tsx @@ -176,7 +176,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => { {isQRBlurry && } - {store.isOpenSource() && QRCodeDataParsed && ( + {store.isOpenSource && QRCodeDataParsed && ( Server URL embedded in this QR:
diff --git a/grafana-plugin/src/navbar/Header/Header.tsx b/grafana-plugin/src/navbar/Header/Header.tsx index c7878ae5..8b6d6764 100644 --- a/grafana-plugin/src/navbar/Header/Header.tsx +++ b/grafana-plugin/src/navbar/Header/Header.tsx @@ -35,7 +35,7 @@ const Header = observer(() => { ); function renderHeading() { - if (store.isOpenSource()) { + if (store.isOpenSource) { return (

Grafana OnCall

diff --git a/grafana-plugin/src/pages/index.tsx b/grafana-plugin/src/pages/index.tsx index 280c80e7..99278aa0 100644 --- a/grafana-plugin/src/pages/index.tsx +++ b/grafana-plugin/src/pages/index.tsx @@ -142,6 +142,12 @@ export const pages: { [id: string]: PageDefinition } = [ path: getPath('cloud'), action: UserActions.OtherSettingsWrite, }, + { + icon: 'cloud', + id: 'insights', + text: 'Insights', + path: getPath('insights'), + }, { icon: 'cog', id: 'test', @@ -180,6 +186,7 @@ export const ROUTES = { 'chat-ops': ['chat-ops'], 'live-settings': ['live-settings'], cloud: ['cloud'], + insights: ['insights'], test: ['test'], // backwards compatible to redirect to new alert-groups diff --git a/grafana-plugin/src/pages/insights/Insights.helpers.ts b/grafana-plugin/src/pages/insights/Insights.helpers.ts new file mode 100644 index 00000000..eb09252e --- /dev/null +++ b/grafana-plugin/src/pages/insights/Insights.helpers.ts @@ -0,0 +1,4 @@ +import { DataSourceRef } from '@grafana/schema'; + +export const getDataSource = (isOpenSource: boolean): DataSourceRef => + isOpenSource ? { uid: '$datasource' } : { uid: 'grafanacloud-usage' }; diff --git a/grafana-plugin/src/pages/insights/Insights.tsx b/grafana-plugin/src/pages/insights/Insights.tsx new file mode 100644 index 00000000..5441a5fb --- /dev/null +++ b/grafana-plugin/src/pages/insights/Insights.tsx @@ -0,0 +1,170 @@ +import React, { useMemo, useState } from 'react'; + +import { + EmbeddedScene, + SceneTimeRange, + SceneFlexLayout, + SceneControlsSpacer, + SceneRefreshPicker, + SceneTimePicker, + SceneVariableSet, + VariableValueSelectors, + NestedScene, +} from '@grafana/scenes'; +import { Alert } from '@grafana/ui'; +import { observer } from 'mobx-react'; + +import Text from 'components/Text/Text'; +import { useStore } from 'state/useStore'; +import { DOCS_ROOT } from 'utils/consts'; + +import { getDataSource } from './Insights.helpers'; +import { InsightsConfig } from './Insights.types'; +import getAlertGroupsByIntegrationScene from './scenes/AlertGroupsByIntegration'; +import getAlertGroupsByTeamScene from './scenes/AlertGroupsByTeam'; +import getMTTRScene from './scenes/MTTR'; +import getMTTRByIntegrationScene from './scenes/MTTRByIntegration'; +import getMTTRByTeamScene from './scenes/MTTRByTeam'; +import getMTTRChangedForPeriodStatScene from './scenes/MTTRChangedForPeriodStat'; +import getMTTRChangedForPeriodTimeseriesScene from './scenes/MTTRChangedForPeriodTimeseries'; +import getNewAlertGroupsDuringTimePeriodScene from './scenes/NewAlertGroupsDuringTimePeriod'; +import getNewAlertGroupsForSelectedPeriodScene from './scenes/NewAlertGroupsForSelectedPeriod'; +import getNewAlertGroupsNotificationsDuringTimePeriodScene from './scenes/NewAlertGroupsNotificationsDuringTimePeriod'; +import getNewAlertGroupsNotificationsForPeriodTableScene from './scenes/NewAlertGroupsNotificationsForPeriodTable'; +import getNewAlertGroupsNotificationsInTotalScene from './scenes/NewAlertGroupsNotificationsInTotal'; +import getTotalAlertGroupsScene from './scenes/TotalAlertGroups'; +import getTotalAlertGroupsByStateScene from './scenes/TotalAlertGroupsByState'; +import getVariables from './variables'; + +const Insights = observer(() => { + const { isOpenSource } = useStore(); + const [alertVisible, setAlertVisible] = useState(true); + + const rootScene = useMemo( + () => getRootScene({ isOpenSource, datasource: getDataSource(isOpenSource) }), + [isOpenSource] + ); + + return ( + <> + {isOpenSource && alertVisible && ( + setAlertVisible(false)} severity="info" title=""> + { + <> + In order to see insights you need to set up Prometheus, add it to your Grafana instance as a data source, + set FEATURE_PROMETHEUS_EXPORTER_ENABLED environment variable to true and then select your Data source in + the dropdown below. +
+
+ <> + You can find out more in + + documentation + + . + + + } +
+ )} + + + ); +}); + +const getRootScene = (config: InsightsConfig) => + new EmbeddedScene({ + $timeRange: new SceneTimeRange({ from: 'now-7d', to: 'now' }), + $variables: new SceneVariableSet({ + variables: getVariables(config), + }), + controls: [ + new VariableValueSelectors({}), + new SceneControlsSpacer(), + new SceneTimePicker({}), + new SceneRefreshPicker({}), + ], + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new NestedScene({ + title: 'Overview', + canCollapse: true, + isCollapsed: false, + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexLayout({ + height: 200, + children: [ + getTotalAlertGroupsScene(config), + getTotalAlertGroupsByStateScene(config), + getNewAlertGroupsForSelectedPeriodScene(config), + getMTTRScene(config), + getMTTRChangedForPeriodStatScene(config), + ], + }), + new SceneFlexLayout({ + height: 400, + children: [getNewAlertGroupsDuringTimePeriodScene(config)], + }), + new SceneFlexLayout({ + height: 400, + children: [getMTTRChangedForPeriodTimeseriesScene(config)], + }), + ], + }), + }), + new NestedScene({ + title: 'Integrations data', + canCollapse: true, + isCollapsed: false, + body: new SceneFlexLayout({ + height: 400, + children: [getAlertGroupsByIntegrationScene(config), getMTTRByIntegrationScene(config)], + }), + }), + new NestedScene({ + title: 'Notified alert groups by Users (based on all Integrations)', + canCollapse: true, + isCollapsed: false, + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexLayout({ + height: 400, + children: [getNewAlertGroupsNotificationsDuringTimePeriodScene(config)], + }), + new SceneFlexLayout({ + height: 400, + children: [ + getNewAlertGroupsNotificationsInTotalScene(config), + getNewAlertGroupsNotificationsForPeriodTableScene(config), + ], + }), + ], + }), + }), + new NestedScene({ + title: 'Teams data', + canCollapse: true, + isCollapsed: false, + body: new SceneFlexLayout({ + direction: 'column', + children: [ + new SceneFlexLayout({ + height: 400, + children: [getAlertGroupsByTeamScene(config), getMTTRByTeamScene(config)], + }), + ], + }), + }), + ], + }), + }); + +export default Insights; diff --git a/grafana-plugin/src/pages/insights/Insights.types.ts b/grafana-plugin/src/pages/insights/Insights.types.ts new file mode 100644 index 00000000..912585a9 --- /dev/null +++ b/grafana-plugin/src/pages/insights/Insights.types.ts @@ -0,0 +1,6 @@ +import { DataSourceRef } from '@grafana/schema'; + +export interface InsightsConfig { + isOpenSource: boolean; + datasource: DataSourceRef; +} diff --git a/grafana-plugin/src/pages/insights/scenes/AlertGroupsByIntegration.tsx b/grafana-plugin/src/pages/insights/scenes/AlertGroupsByIntegration.tsx new file mode 100644 index 00000000..07e2a102 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/AlertGroupsByIntegration.tsx @@ -0,0 +1,111 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getAlertGroupsByIntegrationScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(max_over_time(sum by(integration) (avg without(pod, instance)($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))[1d:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'Alert groups', + integration: 'Integration', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'Alert groups by Integration', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + mode: 'gradient', + type: 'gauge', + valueDisplayMode: 'color', + }, + filterable: false, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Integration', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/AlertGroupsByTeam.tsx b/grafana-plugin/src/pages/insights/scenes/AlertGroupsByTeam.tsx new file mode 100644 index 00000000..2df8f305 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/AlertGroupsByTeam.tsx @@ -0,0 +1,111 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getAlertGroupsByTeamScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(max_over_time(sum by(team) (avg without(pod, instance)($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))[1d:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'Alert groups', + team: 'Team', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'Alert groups by Team', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + mode: 'gradient', + type: 'gauge', + valueDisplayMode: 'color', + }, + filterable: false, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Team', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/MTTR.tsx b/grafana-plugin/src/pages/insights/scenes/MTTR.tsx new file mode 100644 index 00000000..a8849465 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/MTTR.tsx @@ -0,0 +1,74 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getMTTRScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'avg_over_time((sum($alert_groups_response_time_seconds_sum{slug=~"$instance", team=~"$team", integration=~"$integration"}) / sum($alert_groups_response_time_seconds_count{slug=~"$instance", team=~"$team", integration=~"$integration"}))[$__range:])', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'Mean time to respond (MTTR)', + description: 'Mean time between the start and first action of all alert groups for the last 7 days', + pluginId: 'stat', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'text', + value: null, + }, + ], + }, + unit: 's', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Value', + }, + properties: [ + { + id: 'displayName', + value: 'MTTR', + }, + ], + }, + ], + }, + options: { + colorMode: 'value', + graphMode: 'none', + justifyMode: 'center', + orientation: 'auto', + reduceOptions: { + calcs: ['lastNotNull'], + fields: '', + values: false, + }, + textMode: 'auto', + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/MTTRByIntegration.tsx b/grafana-plugin/src/pages/insights/scenes/MTTRByIntegration.tsx new file mode 100644 index 00000000..384a247c --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/MTTRByIntegration.tsx @@ -0,0 +1,126 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getMTTRByIntegrationScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(avg_over_time((sum by (integration)($alert_groups_response_time_seconds_sum{slug=~"$instance", team=~"$team", integration=~"$integration"}) / sum by (integration)($alert_groups_response_time_seconds_count{slug=~"$instance", team=~"$team", integration=~"$integration"}))[$__range:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + cluster: true, + container: true, + id: true, + instance: true, + job: true, + namespace: true, + org_id: true, + pod: true, + slug: true, + team: true, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'MTTR', + integration: 'Integration', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'Mean time to respond (MTTR) by Integration', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'continuous-GrYlRd', + }, + custom: { + align: 'auto', + cellOptions: { + mode: 'gradient', + type: 'gauge', + valueDisplayMode: 'text', + }, + filterable: false, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: 0, + }, + { + color: 'red', + value: 5400, + }, + ], + }, + unit: 's', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Integration', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/MTTRByTeam.tsx b/grafana-plugin/src/pages/insights/scenes/MTTRByTeam.tsx new file mode 100644 index 00000000..e193546f --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/MTTRByTeam.tsx @@ -0,0 +1,116 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getMTTRByTeamScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(avg_over_time((sum by(team) ($alert_groups_response_time_seconds_sum{slug=~"$instance", team=~"$team", integration=~"$integration"}) / sum by(team)($alert_groups_response_time_seconds_count{slug=~"$instance", team=~"$team", integration=~"$integration"}))[$__range:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'MTTR', + team: 'Team', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'Mean time to respond by Team (MTTR)', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'continuous-GrYlRd', + }, + custom: { + align: 'left', + cellOptions: { + mode: 'gradient', + type: 'gauge', + valueDisplayMode: 'text', + }, + filterable: false, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + { + color: 'red', + value: 5400, + }, + ], + }, + unit: 's', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Team', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodStat.tsx b/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodStat.tsx new file mode 100644 index 00000000..11000fd7 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodStat.tsx @@ -0,0 +1,68 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getMTTRChangedForPeriodStatScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'avg(sum($alert_groups_response_time_seconds_sum{slug=~"$instance", team=~"$team", integration=~"$integration"}) / sum($alert_groups_response_time_seconds_count{slug=~"$instance", team=~"$team", integration=~"$integration"}))', + instant: false, + legendFormat: '__auto', + range: true, + refId: 'A', + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'MTTR changed for period', + pluginId: 'stat', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'blue', + value: null, + }, + { + color: 'green', + value: -10000000, + }, + { + color: 'super-light-yellow', + value: 0, + }, + ], + }, + unit: 's', + }, + overrides: [], + }, + options: { + colorMode: 'value', + graphMode: 'none', + justifyMode: 'center', + orientation: 'auto', + reduceOptions: { + calcs: ['diff'], + fields: '', + values: false, + }, + textMode: 'auto', + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodTimeseries.tsx b/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodTimeseries.tsx new file mode 100644 index 00000000..c5f37f2c --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/MTTRChangedForPeriodTimeseries.tsx @@ -0,0 +1,107 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getMTTRChangedForPeriodTimeseriesScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'avg(sum($alert_groups_response_time_seconds_sum{slug=~"$instance", team=~"$team", integration=~"$integration"}) / sum($alert_groups_response_time_seconds_count{slug=~"$instance", team=~"$team", integration=~"$integration"}))', + instant: false, + legendFormat: '__auto', + range: true, + refId: 'A', + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'MTTR changed for period', + pluginId: 'timeseries', + fieldConfig: { + defaults: { + color: { + fixedColor: 'green', + mode: 'fixed', + seriesBy: 'min', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 54, + gradientMode: 'opacity', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineStyle: { + fill: 'solid', + }, + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: true, + stacking: { + group: 'A', + mode: 'none', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'text', + value: 0, + }, + ], + }, + unit: 's', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Value', + }, + properties: [ + { + id: 'displayName', + value: 'MTTR', + }, + ], + }, + ], + }, + options: { + legend: { + displayMode: 'list', + placement: 'bottom', + showLegend: false, + }, + tooltip: { + mode: 'single', + sort: 'none', + }, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsDuringTimePeriod.tsx b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsDuringTimePeriod.tsx new file mode 100644 index 00000000..16033da3 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsDuringTimePeriod.tsx @@ -0,0 +1,118 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getNewAlertGroupsDuringTimePeriodScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + disableTextWrap: false, + editorMode: 'code', + excludeNullMetadata: false, + exemplar: false, + expr: 'increase(max_over_time(sum by (integration) (avg without(pod, instance) ($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))[30m:])[1h:])', + fullMetaSearch: false, + instant: false, + legendFormat: '__auto', + range: true, + refId: 'A', + useBackend: false, + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'New alert groups during time period', + pluginId: 'timeseries', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 80, + gradientMode: 'opacity', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + lineInterpolation: 'linear', + lineStyle: { + fill: 'solid', + }, + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'normal', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + decimals: 0, + displayName: '${__field.labels.integration}', + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byValue', + options: { + op: 'gte', + reducer: 'allIsZero', + value: 0, + }, + }, + properties: [ + { + id: 'custom.hideFrom', + value: { + legend: true, + tooltip: true, + viz: true, + }, + }, + ], + }, + ], + }, + options: { + legend: { + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'multi', + sort: 'desc', + }, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsForSelectedPeriod.tsx b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsForSelectedPeriod.tsx new file mode 100644 index 00000000..f38abacb --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsForSelectedPeriod.tsx @@ -0,0 +1,80 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getNewAlertGroupsForSelectedPeriodScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + disableTextWrap: false, + editorMode: 'code', + excludeNullMetadata: false, + exemplar: false, + expr: 'increase(max_over_time(sum(avg without(pod, instance) ($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))[1d:])[$__range:])', + format: 'time_series', + fullMetaSearch: false, + includeNullMetadata: true, + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + useBackend: false, + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'New alert groups for selected period', + pluginId: 'stat', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + decimals: 0, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'text', + value: null, + }, + ], + }, + unit: 'none', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Value', + }, + properties: [ + { + id: 'displayName', + value: 'New alert groups', + }, + ], + }, + ], + }, + options: { + colorMode: 'value', + graphMode: 'none', + justifyMode: 'center', + orientation: 'auto', + reduceOptions: { + calcs: ['lastNotNull'], + fields: '', + values: false, + }, + textMode: 'auto', + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsDuringTimePeriod.tsx b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsDuringTimePeriod.tsx new file mode 100644 index 00000000..328da56b --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsDuringTimePeriod.tsx @@ -0,0 +1,118 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getNewAlertGroupsNotificationsDuringTimePeriodScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + disableTextWrap: false, + editorMode: 'code', + excludeNullMetadata: false, + exemplar: false, + expr: 'increase(max_over_time(sum by (username) (avg without(pod, instance) ($user_was_notified_of_alert_groups_total{slug=~"$instance"}))[30m:])[1h:])', + fullMetaSearch: false, + instant: false, + legendFormat: '__auto', + range: true, + refId: 'A', + useBackend: false, + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + title: 'New alert groups notifications during time period', + pluginId: 'timeseries', + fieldConfig: { + defaults: { + color: { + mode: 'palette-classic', + }, + custom: { + axisCenteredZero: false, + axisColorMode: 'text', + axisLabel: '', + axisPlacement: 'auto', + barAlignment: 0, + drawStyle: 'line', + fillOpacity: 80, + gradientMode: 'opacity', + hideFrom: { + legend: false, + tooltip: false, + viz: false, + }, + insertNulls: false, + lineInterpolation: 'linear', + lineStyle: { + fill: 'solid', + }, + lineWidth: 1, + pointSize: 5, + scaleDistribution: { + type: 'linear', + }, + showPoints: 'auto', + spanNulls: false, + stacking: { + group: 'A', + mode: 'normal', + }, + thresholdsStyle: { + mode: 'off', + }, + }, + decimals: 0, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byValue', + options: { + op: 'gte', + reducer: 'allIsZero', + value: 0, + }, + }, + properties: [ + { + id: 'custom.hideFrom', + value: { + legend: true, + tooltip: true, + viz: true, + }, + }, + ], + }, + ], + }, + options: { + legend: { + displayMode: 'list', + placement: 'bottom', + showLegend: true, + }, + tooltip: { + mode: 'multi', + sort: 'desc', + }, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsForPeriodTable.tsx b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsForPeriodTable.tsx new file mode 100644 index 00000000..b7c4d722 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsForPeriodTable.tsx @@ -0,0 +1,113 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getNewAlertGroupsNotificationsForPeriodTableScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(increase(max_over_time(sum by (username) (avg without(pod, instance) ($user_was_notified_of_alert_groups_total{slug=~"$instance"}))[1h:])[$__range:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + username: false, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'Alert groups', + team: 'Team', + username: 'Username', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'New alert groups notifications for period', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + type: 'gauge', + }, + filterable: false, + inspect: false, + }, + decimals: 0, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + unit: 'none', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Username', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsInTotal.tsx b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsInTotal.tsx new file mode 100644 index 00000000..2e6564a6 --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/NewAlertGroupsNotificationsInTotal.tsx @@ -0,0 +1,113 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getNewAlertGroupsNotificationsInTotalScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + editorMode: 'code', + exemplar: false, + expr: 'sort_desc(max_over_time(sum by(username) (avg without(pod, instance)($user_was_notified_of_alert_groups_total{slug=~"$instance"}))[1d:]))', + format: 'table', + instant: true, + legendFormat: '__auto', + range: false, + refId: 'A', + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'seriesToRows', + options: {}, + }, + { + id: 'organize', + options: { + excludeByName: { + Time: true, + username: false, + }, + indexByName: {}, + renameByName: { + Metric: 'Integration', + Value: 'Alert groups', + team: 'Team', + username: 'Username', + }, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'New alert groups notifications in total', + pluginId: 'table', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + custom: { + align: 'auto', + cellOptions: { + mode: 'gradient', + type: 'gauge', + valueDisplayMode: 'color', + }, + filterable: false, + inspect: false, + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Username', + }, + properties: [ + { + id: 'custom.cellOptions', + value: { + type: 'auto', + }, + }, + { + id: 'custom.width', + value: 300, + }, + ], + }, + ], + }, + options: { + cellHeight: 'sm', + footer: { + countRows: false, + fields: '', + reducer: ['sum'], + show: false, + }, + showHeader: true, + }, + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/TotalAlertGroups.tsx b/grafana-plugin/src/pages/insights/scenes/TotalAlertGroups.tsx new file mode 100644 index 00000000..ebd3facd --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/TotalAlertGroups.tsx @@ -0,0 +1,79 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getTotalAlertGroupsScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + disableTextWrap: false, + editorMode: 'code', + excludeNullMetadata: false, + exemplar: false, + expr: 'max_over_time(sum(avg without(pod, instance) ($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))[1d:])', + format: 'time_series', + fullMetaSearch: false, + instant: false, + legendFormat: '__auto', + range: true, + refId: 'A', + useBackend: false, + }, + ], + }); + + return new SceneFlexItem({ + $data: query, + body: new VizPanel({ + pluginId: 'stat', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'text', + value: null, + }, + ], + }, + unit: 'none', + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'Value', + }, + properties: [ + { + id: 'displayName', + value: 'Total alert groups', + }, + ], + }, + ], + }, + options: { + colorMode: 'value', + graphMode: 'none', + justifyMode: 'center', + orientation: 'auto', + reduceOptions: { + calcs: ['lastNotNull'], + fields: '', + values: false, + }, + textMode: 'auto', + }, + pluginVersion: '9.5.2', + title: 'Total alert groups', + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/scenes/TotalAlertGroupsByState.tsx b/grafana-plugin/src/pages/insights/scenes/TotalAlertGroupsByState.tsx new file mode 100644 index 00000000..7d36795b --- /dev/null +++ b/grafana-plugin/src/pages/insights/scenes/TotalAlertGroupsByState.tsx @@ -0,0 +1,133 @@ +import { ThresholdsMode } from '@grafana/data'; +import { SceneDataTransformer, SceneFlexItem, SceneQueryRunner, VizPanel } from '@grafana/scenes'; + +import { InsightsConfig } from 'pages/insights/Insights.types'; + +export default function getTotalAlertGroupsByStateScene({ datasource }: InsightsConfig) { + const query = new SceneQueryRunner({ + datasource, + queries: [ + { + disableTextWrap: false, + editorMode: 'code', + excludeNullMetadata: false, + expr: 'sum by (state) (avg without(pod, instance) ($alert_groups_total{slug=~"$instance", team=~"$team", integration=~"$integration"}))', + fullMetaSearch: false, + legendFormat: '__auto', + range: true, + refId: 'A', + useBackend: false, + }, + ], + }); + + const transformedData = new SceneDataTransformer({ + $data: query, + transformations: [ + { + id: 'joinByLabels', + options: { + value: 'state', + }, + }, + { + id: 'organize', + options: { + excludeByName: {}, + indexByName: { + acknowledged: 1, + firing: 0, + resolved: 2, + silenced: 3, + }, + renameByName: {}, + }, + }, + ], + }); + + return new SceneFlexItem({ + $data: transformedData, + body: new VizPanel({ + title: 'Total alert groups by state', + pluginId: 'bargauge', + fieldConfig: { + defaults: { + color: { + mode: 'thresholds', + }, + mappings: [], + thresholds: { + mode: ThresholdsMode.Absolute, + steps: [ + { + color: 'green', + value: null, + }, + ], + }, + }, + overrides: [ + { + matcher: { + id: 'byName', + options: 'firing', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'red', + mode: 'fixed', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'acknowledged', + }, + properties: [ + { + id: 'color', + value: { + fixedColor: 'dark-yellow', + mode: 'fixed', + }, + }, + ], + }, + { + matcher: { + id: 'byName', + options: 'silenced', + }, + properties: [ + { + id: 'color', + value: { + mode: 'fixed', + }, + }, + ], + }, + ], + }, + options: { + displayMode: 'gradient', + minVizHeight: 10, + minVizWidth: 0, + orientation: 'vertical', + reduceOptions: { + calcs: ['lastNotNull'], + fields: '', + values: false, + }, + showUnfilled: true, + valueMode: 'color', + }, + pluginVersion: '9.5.2', + }), + }); +} diff --git a/grafana-plugin/src/pages/insights/variables.ts b/grafana-plugin/src/pages/insights/variables.ts new file mode 100644 index 00000000..1d121789 --- /dev/null +++ b/grafana-plugin/src/pages/insights/variables.ts @@ -0,0 +1,138 @@ +import { DataSourceVariable, QueryVariable } from '@grafana/scenes'; + +import { InsightsConfig } from './Insights.types'; + +const DEFAULT_VARIABLE_CONFIG: Partial[0]> = { + hide: 0, + includeAll: true, + isMulti: true, + options: [], + refresh: 1, + regex: '', + skipUrlSync: false, + sort: 0, + type: 'query', +}; + +const getVariables = ({ isOpenSource, datasource }: InsightsConfig) => [ + // Selectable + ...(isOpenSource + ? [ + new DataSourceVariable({ + name: 'datasource', + label: 'Data source', + pluginId: 'prometheus', + value: 'grafanacloud-usage', + }), + ] + : []), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'instance', + label: 'Instance', + text: ['All'], + value: ['$__all'], + datasource, + definition: 'label_values(${alert_groups_total},slug)', + query: { + query: 'label_values(${alert_groups_total},slug)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'team', + label: 'Team', + text: ['All'], + value: ['$__all'], + datasource, + definition: 'label_values(${alert_groups_total}{slug=~"$instance"},team)', + query: { + query: 'label_values(${alert_groups_total}{slug=~"$instance"},team)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + refresh: 2, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'integration', + label: 'Integration', + text: ['All'], + value: ['$__all'], + datasource, + definition: 'label_values(${alert_groups_total}{team=~"$team",slug=~"$instance"},integration)', + query: { + query: 'label_values(${alert_groups_total}{team=~"$team",slug=~"$instance"},integration)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + refresh: 2, + }), + + // Non-selectable + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'alert_groups_total', + label: 'alert_groups_total', + datasource, + query: { + query: 'metrics(alert_groups_total)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + text: ['oncall_alert_groups_total', 'grafanacloud_oncall_instance_alert_groups_total'], + value: ['oncall_alert_groups_total', 'grafanacloud_oncall_instance_alert_groups_total'], + definition: 'metrics(alert_groups_total)', + hide: 2, + includeAll: false, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'user_was_notified_of_alert_groups_total', + label: 'user_was_notified_of_alert_groups_total', + datasource, + definition: 'metrics(user_was_notified_of_alert_groups_total)', + query: { + query: 'metrics(user_was_notified_of_alert_groups_total)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + hide: 2, + refresh: 2, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'alert_groups_response_time_seconds_bucket', + label: 'alert_groups_response_time_seconds_bucket', + datasource, + definition: 'metrics(alert_groups_response_time_seconds_bucket)', + query: { + query: 'metrics(alert_groups_response_time_seconds_bucket)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + hide: 2, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'alert_groups_response_time_seconds_sum', + label: 'alert_groups_response_time_seconds_sum', + datasource, + definition: 'metrics(alert_groups_response_time_seconds_sum)', + query: { + query: 'metrics(alert_groups_response_time_seconds_sum)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + hide: 2, + }), + new QueryVariable({ + ...DEFAULT_VARIABLE_CONFIG, + name: 'alert_groups_response_time_seconds_count', + label: 'alert_groups_response_time_seconds_count', + datasource, + definition: 'metrics(alert_groups_response_time_seconds_count)', + query: { + query: 'metrics(alert_groups_response_time_seconds_count)', + refId: 'PrometheusVariableQueryEditor-VariableQuery', + }, + hide: 2, + }), +]; + +export default getVariables; diff --git a/grafana-plugin/src/pages/routes.tsx b/grafana-plugin/src/pages/routes.tsx index a224529d..048c83ec 100644 --- a/grafana-plugin/src/pages/routes.tsx +++ b/grafana-plugin/src/pages/routes.tsx @@ -1,6 +1,7 @@ import EscalationsChainsPage from 'pages/escalation-chains/EscalationChains'; import IncidentPage from 'pages/incident/Incident'; import IncidentsPage from 'pages/incidents/Incidents'; +import Insights from 'pages/insights/Insights'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; import SchedulePage from 'pages/schedule/Schedule'; import SchedulesPage from 'pages/schedules/Schedules'; @@ -66,6 +67,10 @@ export const routes: { [id: string]: NavRoute } = [ component: CloudPage, id: 'cloud', }, + { + component: Insights, + id: 'insights', + }, ].reduce((prev, current) => { prev[current.id] = { id: current.id, diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx index 824e9191..c4cb87b7 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/ChatOps.tsx @@ -47,7 +47,7 @@ class ChatOpsPage extends React.Component { const { activeTab } = this.state; const { store } = this.props; - if (!this.isChatOpsConfigured() && store.isOpenSource()) { + if (!this.isChatOpsConfigured() && store.isOpenSource) { return this.renderNoChatOpsBannerInfo(); } diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index 2dc75d5b..8fe77354 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -94,6 +94,14 @@ "action": "grafana-oncall-app.other-settings:read", "addToNav": true }, + { + "type": "page", + "name": "Insights", + "path": "/a/grafana-oncall-app/insights", + "role": "Viewer", + "action": "grafana-oncall-app.other-settings:read", + "addToNav": true + }, { "type": "dashboard", "path": "dashboards/oncall_metrics_dashboard.json", diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index ec89e331..8f87baaa 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -26,6 +26,7 @@ import NoMatch from 'pages/NoMatch'; import EscalationChains from 'pages/escalation-chains/EscalationChains'; import Incident from 'pages/incident/Incident'; import Incidents from 'pages/incidents/Incidents'; +import Insights from 'pages/insights/Insights'; import Integration from 'pages/integration/Integration'; import Integrations from 'pages/integrations/Integrations'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; @@ -181,6 +182,9 @@ export const Root = observer((props: AppRootProps) => { + + + {/* Backwards compatibility redirect routes */}