Brojd/implement insights (#3583)

# What this PR does
- Use Grafana Scenes to add Insights as a separate page in OnCall
- Add an option to run Prometheus instance via helm so that Prometheus
Exporter feature can be used easily without the need of setting up
Prometheus separately

## Which issue(s) this PR fixes
https://github.com/grafana/oncall-private/issues/2382

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Dominik Broj 2024-01-05 08:05:31 +01:00 committed by GitHub
parent 74b68a8d27
commit 960dcae608
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
39 changed files with 1993 additions and 12 deletions

View file

@ -4,6 +4,8 @@ base_url_protocol: http
env: env:
- name: GRAFANA_CLOUD_NOTIFICATIONS_ENABLED - name: GRAFANA_CLOUD_NOTIFICATIONS_ENABLED
value: "False" value: "False"
- name: FEATURE_PROMETHEUS_EXPORTER_ENABLED
value: "True"
image: image:
tag: latest tag: latest
pullPolicy: Always pullPolicy: Always
@ -104,3 +106,11 @@ service:
type: NodePort type: NodePort
port: 8080 port: 8080
nodePort: 30001 nodePort: 30001
prometheus:
enabled: true
extraScrapeConfigs: |
- job_name: 'oncall-exporter'
metrics_path: /metrics/
static_configs:
- targets:
- oncall-dev-engine.default.svc.cluster.local:8080

View file

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## 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) ## v1.3.82 (2024-01-04)
### Added ### Added

View file

@ -133,3 +133,11 @@ service:
type: NodePort type: NodePort
port: 8080 port: 8080
nodePort: 30001 nodePort: 30001
prometheus:
enabled: false
extraScrapeConfigs: |
- job_name: 'oncall-exporter'
metrics_path: /metrics/
static_configs:
- targets:
- oncall-dev-engine.default.svc.cluster.local:8080

View file

@ -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();
});
});

View file

@ -98,3 +98,9 @@ export const verifyThatAlertGroupIsTriggered = async (
expect(await incidentTimelineContainsStep(page, triggeredStepText)).toBe(true); 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();
};

View file

@ -2,12 +2,11 @@ import type { Page } from '@playwright/test';
import { BASE_URL } from './constants'; import { BASE_URL } from './constants';
type GrafanaPage = '/plugins/grafana-oncall-app'; type OnCallPage = 'alert-groups' | 'integrations' | 'escalations' | 'schedules' | 'users' | 'insights';
type OnCallPage = 'alert-groups' | 'integrations' | 'escalations' | 'schedules' | 'users';
const _goToPage = async (page: Page, url = '') => page.goto(`${BASE_URL}${url}`); 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) => { export const goToOnCallPage = async (page: Page, onCallPage: OnCallPage) => {
await _goToPage(page, `/a/grafana-oncall-app/${onCallPage}`); await _goToPage(page, `/a/grafana-oncall-app/${onCallPage}`);

View file

@ -124,6 +124,8 @@
"@grafana/faro-web-tracing": "^1.0.0-beta4", "@grafana/faro-web-tracing": "^1.0.0-beta4",
"@grafana/labels": "~1.4.4", "@grafana/labels": "~1.4.4",
"@grafana/runtime": "9.3.0-beta1", "@grafana/runtime": "9.3.0-beta1",
"@grafana/scenes": "^1.28.0",
"@grafana/schema": "^10.2.2",
"@grafana/ui": "^10.2.0", "@grafana/ui": "^10.2.0",
"@lifeomic/attempt": "^3.0.3", "@lifeomic/attempt": "^3.0.3",
"@opentelemetry/api": "^1.3.0", "@opentelemetry/api": "^1.3.0",

View file

@ -147,7 +147,7 @@ export default function Alerts() {
function showMismatchWarning(): boolean { function showMismatchWarning(): boolean {
return ( return (
store.isOpenSource() && store.isOpenSource &&
store.backendVersion && store.backendVersion &&
plugin?.version && plugin?.version &&
store.backendVersion !== plugin?.version && store.backendVersion !== plugin?.version &&

View file

@ -54,7 +54,7 @@ const mockUseStore = (rest?: any, connected = false, cloud_connected = true) =>
cloudConnectionStatus: { cloud_connection_status: cloud_connected }, cloudConnectionStatus: { cloud_connection_status: cloud_connected },
} as unknown as CloudStore, } as unknown as CloudStore,
hasFeature: jest.fn().mockReturnValue(true), hasFeature: jest.fn().mockReturnValue(true),
isOpenSource: jest.fn().mockReturnValue(true), isOpenSource: true,
} as unknown as RootStore; } as unknown as RootStore;
useStore.mockReturnValue(store); useStore.mockReturnValue(store);

View file

@ -176,7 +176,7 @@ const MobileAppConnection = observer(({ userPk }: Props) => {
<QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} /> <QRCode className={cx({ 'qr-code': true, blurry: isQRBlurry })} value={QRCodeValue} />
{isQRBlurry && <QRLoading />} {isQRBlurry && <QRLoading />}
</div> </div>
{store.isOpenSource() && QRCodeDataParsed && ( {store.isOpenSource && QRCodeDataParsed && (
<Text type="secondary"> <Text type="secondary">
Server URL embedded in this QR: Server URL embedded in this QR:
<br /> <br />

View file

@ -35,7 +35,7 @@ const Header = observer(() => {
); );
function renderHeading() { function renderHeading() {
if (store.isOpenSource()) { if (store.isOpenSource) {
return ( return (
<div className={cx('heading')}> <div className={cx('heading')}>
<h1 className={cx('page-header__title')}>Grafana OnCall</h1> <h1 className={cx('page-header__title')}>Grafana OnCall</h1>

View file

@ -142,6 +142,12 @@ export const pages: { [id: string]: PageDefinition } = [
path: getPath('cloud'), path: getPath('cloud'),
action: UserActions.OtherSettingsWrite, action: UserActions.OtherSettingsWrite,
}, },
{
icon: 'cloud',
id: 'insights',
text: 'Insights',
path: getPath('insights'),
},
{ {
icon: 'cog', icon: 'cog',
id: 'test', id: 'test',
@ -180,6 +186,7 @@ export const ROUTES = {
'chat-ops': ['chat-ops'], 'chat-ops': ['chat-ops'],
'live-settings': ['live-settings'], 'live-settings': ['live-settings'],
cloud: ['cloud'], cloud: ['cloud'],
insights: ['insights'],
test: ['test'], test: ['test'],
// backwards compatible to redirect to new alert-groups // backwards compatible to redirect to new alert-groups

View file

@ -0,0 +1,4 @@
import { DataSourceRef } from '@grafana/schema';
export const getDataSource = (isOpenSource: boolean): DataSourceRef =>
isOpenSource ? { uid: '$datasource' } : { uid: 'grafanacloud-usage' };

View file

@ -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 && (
<Alert onRemove={() => 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.
<br />
<br />
<>
You can find out more in
<a
href={`${DOCS_ROOT}/insights-and-metrics/#for-open-source-customers`}
target="_blank"
rel="noreferrer"
>
<Text type="link"> documentation</Text>
</a>
.
</>
</>
}
</Alert>
)}
<rootScene.Component model={rootScene} />
</>
);
});
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;

View file

@ -0,0 +1,6 @@
import { DataSourceRef } from '@grafana/schema';
export interface InsightsConfig {
isOpenSource: boolean;
datasource: DataSourceRef;
}

View file

@ -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,
},
}),
});
}

View file

@ -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,
},
}),
});
}

View file

@ -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',
},
}),
});
}

View file

@ -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,
},
}),
});
}

View file

@ -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,
},
}),
});
}

View file

@ -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',
},
}),
});
}

View file

@ -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',
},
},
}),
});
}

View file

@ -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',
},
},
}),
});
}

View file

@ -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',
},
}),
});
}

View file

@ -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',
},
},
}),
});
}

View file

@ -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,
},
}),
});
}

View file

@ -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,
},
}),
});
}

View file

@ -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',
}),
});
}

View file

@ -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',
}),
});
}

View file

@ -0,0 +1,138 @@
import { DataSourceVariable, QueryVariable } from '@grafana/scenes';
import { InsightsConfig } from './Insights.types';
const DEFAULT_VARIABLE_CONFIG: Partial<ConstructorParameters<typeof QueryVariable>[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;

View file

@ -1,6 +1,7 @@
import EscalationsChainsPage from 'pages/escalation-chains/EscalationChains'; import EscalationsChainsPage from 'pages/escalation-chains/EscalationChains';
import IncidentPage from 'pages/incident/Incident'; import IncidentPage from 'pages/incident/Incident';
import IncidentsPage from 'pages/incidents/Incidents'; import IncidentsPage from 'pages/incidents/Incidents';
import Insights from 'pages/insights/Insights';
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
import SchedulePage from 'pages/schedule/Schedule'; import SchedulePage from 'pages/schedule/Schedule';
import SchedulesPage from 'pages/schedules/Schedules'; import SchedulesPage from 'pages/schedules/Schedules';
@ -66,6 +67,10 @@ export const routes: { [id: string]: NavRoute } = [
component: CloudPage, component: CloudPage,
id: 'cloud', id: 'cloud',
}, },
{
component: Insights,
id: 'insights',
},
].reduce((prev, current) => { ].reduce((prev, current) => {
prev[current.id] = { prev[current.id] = {
id: current.id, id: current.id,

View file

@ -47,7 +47,7 @@ class ChatOpsPage extends React.Component<ChatOpsProps, ChatOpsState> {
const { activeTab } = this.state; const { activeTab } = this.state;
const { store } = this.props; const { store } = this.props;
if (!this.isChatOpsConfigured() && store.isOpenSource()) { if (!this.isChatOpsConfigured() && store.isOpenSource) {
return this.renderNoChatOpsBannerInfo(); return this.renderNoChatOpsBannerInfo();
} }

View file

@ -94,6 +94,14 @@
"action": "grafana-oncall-app.other-settings:read", "action": "grafana-oncall-app.other-settings:read",
"addToNav": true "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", "type": "dashboard",
"path": "dashboards/oncall_metrics_dashboard.json", "path": "dashboards/oncall_metrics_dashboard.json",

View file

@ -26,6 +26,7 @@ import NoMatch from 'pages/NoMatch';
import EscalationChains from 'pages/escalation-chains/EscalationChains'; import EscalationChains from 'pages/escalation-chains/EscalationChains';
import Incident from 'pages/incident/Incident'; import Incident from 'pages/incident/Incident';
import Incidents from 'pages/incidents/Incidents'; import Incidents from 'pages/incidents/Incidents';
import Insights from 'pages/insights/Insights';
import Integration from 'pages/integration/Integration'; import Integration from 'pages/integration/Integration';
import Integrations from 'pages/integrations/Integrations'; import Integrations from 'pages/integrations/Integrations';
import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks'; import OutgoingWebhooks from 'pages/outgoing_webhooks/OutgoingWebhooks';
@ -181,6 +182,9 @@ export const Root = observer((props: AppRootProps) => {
<Route path={getRoutesForPage('cloud')} exact> <Route path={getRoutesForPage('cloud')} exact>
<CloudPage /> <CloudPage />
</Route> </Route>
<Route path={getRoutesForPage('insights')} exact>
<Insights />
</Route>
{/* Backwards compatibility redirect routes */} {/* Backwards compatibility redirect routes */}
<Route <Route

View file

@ -1,6 +1,6 @@
import { locationService } from '@grafana/runtime'; import { locationService } from '@grafana/runtime';
import { contextSrv } from 'grafana/app/core/core'; import { contextSrv } from 'grafana/app/core/core';
import { action, makeObservable, observable, runInAction } from 'mobx'; import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import moment from 'moment-timezone'; import moment from 'moment-timezone';
import qs from 'query-string'; import qs from 'query-string';
import { OnCallAppPluginMeta } from 'types'; import { OnCallAppPluginMeta } from 'types';
@ -188,7 +188,7 @@ export class RootBaseStore {
return this.setupPluginError('🚫 Plugin has not been initialized'); return this.setupPluginError('🚫 Plugin has not been initialized');
} }
if (this.isOpenSource() && !meta.secureJsonFields?.onCallApiToken) { if (this.isOpenSource && !meta.secureJsonFields?.onCallApiToken) {
// Reinstall plugin if onCallApiToken is missing // Reinstall plugin if onCallApiToken is missing
const errorMsg = await PluginState.selfHostedInstallPlugin(this.onCallApiUrl, true); const errorMsg = await PluginState.selfHostedInstallPlugin(this.onCallApiUrl, true);
if (errorMsg) { if (errorMsg) {
@ -296,7 +296,8 @@ export class RootBaseStore {
return GRAFANA_LICENSE_OSS; return GRAFANA_LICENSE_OSS;
} }
isOpenSource(): boolean { @computed
get isOpenSource(): boolean {
return this.license === GRAFANA_LICENSE_OSS; return this.license === GRAFANA_LICENSE_OSS;
} }

View file

@ -1863,6 +1863,15 @@
uplot "1.6.22" uplot "1.6.22"
xss "1.0.13" xss "1.0.13"
"@grafana/e2e-selectors@10.0.2":
version "10.0.2"
resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-10.0.2.tgz#98cd7fa01ca21b416db8827980508195554fb5a4"
integrity sha512-3dc+2hL/AJLkOMXiN2UmWU3kOHO4Eqv10AJVOTkpDwecQvWoSS5vtflyPCEWshDqDSE/6k2gB9N2rlZk9O/R5g==
dependencies:
"@grafana/tsconfig" "^1.2.0-rc1"
tslib "2.5.0"
typescript "4.8.4"
"@grafana/e2e-selectors@10.1.4": "@grafana/e2e-selectors@10.1.4":
version "10.1.4" version "10.1.4"
resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-10.1.4.tgz#150eab282458da06bc0fb44ce46602e47b14ebed" resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-10.1.4.tgz#150eab282458da06bc0fb44ce46602e47b14ebed"
@ -2055,6 +2064,17 @@
systemjs "0.20.19" systemjs "0.20.19"
tslib "2.4.1" tslib "2.4.1"
"@grafana/scenes@^1.28.0":
version "1.28.0"
resolved "https://registry.yarnpkg.com/@grafana/scenes/-/scenes-1.28.0.tgz#e5cf42fffd2f6f1ec37c338b884d93f0041e72bd"
integrity sha512-AWuR7+47hoB/yrqwNzOf9uaZrND3LJdYxPMEfbpvvfKTS1o3WGAq3JXG5xS25i0Bj/dfZCfMysHQXbTa0oBLVQ==
dependencies:
"@grafana/e2e-selectors" "10.0.2"
react-grid-layout "1.3.4"
react-use "17.4.0"
react-virtualized-auto-sizer "1.0.7"
uuid "^9.0.0"
"@grafana/schema@10.1.4": "@grafana/schema@10.1.4":
version "10.1.4" version "10.1.4"
resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-10.1.4.tgz#b6bb598de15fbdd73be62cd4b06e11c49b497c7c" resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-10.1.4.tgz#b6bb598de15fbdd73be62cd4b06e11c49b497c7c"
@ -2090,6 +2110,13 @@
dependencies: dependencies:
tslib "2.5.0" tslib "2.5.0"
"@grafana/schema@^10.2.2":
version "10.2.2"
resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-10.2.2.tgz#7bcb44ffccd72c2a30f99c85bcc188dd4bddfd19"
integrity sha512-VDMO2Ev/mSsQxOkwo2u3uQocoyfxJGaGTfAMRGgeejmYJVQsK3Ka6/ImGmqvCViE5uade5/rx7kKfLnj6Yc0Yg==
dependencies:
tslib "2.6.0"
"@grafana/toolkit@^9.5.2": "@grafana/toolkit@^9.5.2":
version "9.5.2" version "9.5.2"
resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-9.5.2.tgz#39f58b32050bbc7ca6860e1ebfe7378f68a0abca" resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-9.5.2.tgz#39f58b32050bbc7ca6860e1ebfe7378f68a0abca"
@ -10663,7 +10690,7 @@ lodash.get@^4.4.2:
resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" resolved "https://registry.yarnpkg.com/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99"
integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ== integrity sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==
lodash.isequal@^4.5.0: lodash.isequal@^4.0.0, lodash.isequal@^4.5.0:
version "4.5.0" version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ== integrity sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==
@ -13351,6 +13378,14 @@ react-dom@18.2.0, react-dom@^18.0.0:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.0"
react-draggable@^4.0.0, react-draggable@^4.0.3:
version "4.4.6"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.6.tgz#63343ee945770881ca1256a5b6fa5c9f5983fe1e"
integrity sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==
dependencies:
clsx "^1.1.1"
prop-types "^15.8.1"
react-draggable@^4.4.5: react-draggable@^4.4.5:
version "4.4.5" version "4.4.5"
resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c" resolved "https://registry.yarnpkg.com/react-draggable/-/react-draggable-4.4.5.tgz#9e37fe7ce1a4cf843030f521a0a4cc41886d7e7c"
@ -13394,6 +13429,17 @@ react-from-dom@^0.6.2:
resolved "https://registry.yarnpkg.com/react-from-dom/-/react-from-dom-0.6.2.tgz#9da903a508c91c013b55afcd59348b8b0a39bdb4" resolved "https://registry.yarnpkg.com/react-from-dom/-/react-from-dom-0.6.2.tgz#9da903a508c91c013b55afcd59348b8b0a39bdb4"
integrity sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ== integrity sha512-qvWWTL/4xw4k/Dywd41RBpLQUSq97csuv15qrxN+izNeLYlD9wn5W8LspbfYe5CWbaSdkZ72BsaYBPQf2x4VbQ==
react-grid-layout@1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/react-grid-layout/-/react-grid-layout-1.3.4.tgz#4fa819be24a1ba9268aa11b82d63afc4762a32ff"
integrity sha512-sB3rNhorW77HUdOjB4JkelZTdJGQKuXLl3gNg+BI8gJkTScspL1myfZzW/EM0dLEn+1eH+xW+wNqk0oIM9o7cw==
dependencies:
clsx "^1.1.1"
lodash.isequal "^4.0.0"
prop-types "^15.8.1"
react-draggable "^4.0.0"
react-resizable "^3.0.4"
react-highlight-words@0.18.0: react-highlight-words@0.18.0:
version "0.18.0" version "0.18.0"
resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.18.0.tgz#ff3b3ef7cb497fa2e8fa4d54c1a1a98ac6390d0e" resolved "https://registry.yarnpkg.com/react-highlight-words/-/react-highlight-words-0.18.0.tgz#ff3b3ef7cb497fa2e8fa4d54c1a1a98ac6390d0e"
@ -13512,6 +13558,14 @@ react-redux@^7.2.0:
prop-types "^15.7.2" prop-types "^15.7.2"
react-is "^17.0.2" react-is "^17.0.2"
react-resizable@^3.0.4:
version "3.0.5"
resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-3.0.5.tgz#362721f2efbd094976f1780ae13f1ad7739786c1"
integrity sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==
dependencies:
prop-types "15.x"
react-draggable "^4.0.3"
react-responsive@^8.1.0: react-responsive@^8.1.0:
version "8.2.0" version "8.2.0"
resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-8.2.0.tgz#e0ffb306cfd8f38c9c12e26725b9e1245fa9debc" resolved "https://registry.yarnpkg.com/react-responsive/-/react-responsive-8.2.0.tgz#e0ffb306cfd8f38c9c12e26725b9e1245fa9debc"
@ -13704,6 +13758,11 @@ react-use@17.4.0:
ts-easing "^0.2.0" ts-easing "^0.2.0"
tslib "^2.1.0" tslib "^2.1.0"
react-virtualized-auto-sizer@1.0.7:
version "1.0.7"
resolved "https://registry.yarnpkg.com/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.7.tgz#bfb8414698ad1597912473de3e2e5f82180c1195"
integrity sha512-Mxi6lwOmjwIjC1X4gABXMJcKHsOo0xWl3E3ugOgufB8GJU+MqrtY35aBuvCYv/razQ1Vbp7h1gWJjGjoNN5pmA==
react-window@1.8.8: react-window@1.8.8:
version "1.8.8" version "1.8.8"
resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243" resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243"
@ -15723,6 +15782,11 @@ uuid@^8.3.2:
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
uuid@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
v8-compile-cache-lib@^3.0.1: v8-compile-cache-lib@^3.0.1:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

View file

@ -33,3 +33,7 @@ dependencies:
version: 4.1.4 version: 4.1.4
repository: https://kubernetes.github.io/ingress-nginx repository: https://kubernetes.github.io/ingress-nginx
condition: ingress-nginx.enabled condition: ingress-nginx.enabled
- name: prometheus
version: 25.8.2
repository: https://prometheus-community.github.io/helm-charts
condition: prometheus.enabled

Binary file not shown.

View file

@ -708,3 +708,12 @@ ui:
tag: dev tag: dev
# Additional env vars for the ui container # Additional env vars for the ui container
env: {} env: {}
prometheus:
enabled: false
# extraScrapeConfigs: |
# - job_name: 'oncall-exporter'
# metrics_path: /metrics/
# static_configs:
# - targets:
# - oncall-dev-engine.default.svc.cluster.local:8080