Compare commits
1 Commits
serge/disp
...
alerting/m
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8595844334 |
@@ -28,7 +28,7 @@ type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginContextProvider PluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
PluginRepo repo.Service
|
||||
GrafanaVersion string
|
||||
pluginCanBeInstalledCache map[string]bool
|
||||
pluginExistsCacheMu sync.RWMutex
|
||||
@@ -39,7 +39,7 @@ func New(
|
||||
pluginStore pluginstore.Store,
|
||||
pluginContextProvider PluginContextProvider,
|
||||
pluginClient plugins.Client,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
pluginRepo repo.Service,
|
||||
grafanaVersion string,
|
||||
) checks.Check {
|
||||
return &check{
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type missingPluginStep struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
PluginRepo repo.Service
|
||||
GrafanaVersion string
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
advisorv0alpha1 "github.com/grafana/grafana/apps/advisor/pkg/apis/advisor/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/plugins/repo"
|
||||
)
|
||||
|
||||
// Check returns metadata about the check being executed and the list of Steps
|
||||
@@ -38,10 +37,3 @@ type Step interface {
|
||||
// Run executes the step for an item and returns a report
|
||||
Run(ctx context.Context, log logging.Logger, obj *advisorv0alpha1.CheckSpec, item any) ([]advisorv0alpha1.CheckReportFailure, error)
|
||||
}
|
||||
|
||||
// PluginInfoGetter is a minimal interface for retrieving plugin information from a repository.
|
||||
// It contains only the GetPluginsInfo method used by plugincheck and datasourcecheck.
|
||||
type PluginInfoGetter interface {
|
||||
// GetPluginsInfo will return a list of plugins from grafana.com/api/plugins.
|
||||
GetPluginsInfo(ctx context.Context, options repo.GetPluginsInfoOptions, compatOpts repo.CompatOpts) ([]repo.PluginInfo, error)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
|
||||
func New(
|
||||
pluginStore pluginstore.Store,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
pluginRepo repo.Service,
|
||||
updateChecker pluginchecker.PluginUpdateChecker,
|
||||
pluginErrorResolver plugins.ErrorResolver,
|
||||
grafanaVersion string,
|
||||
@@ -33,7 +33,7 @@ func New(
|
||||
|
||||
type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
PluginRepo repo.Service
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
GrafanaVersion string
|
||||
|
||||
@@ -83,7 +83,6 @@ Most [generally available](https://grafana.com/docs/release-life-cycle/#general-
|
||||
| `reportingRetries` | Enables rendering retries for the reporting feature |
|
||||
| `externalServiceAccounts` | Automatic service account and token setup for plugins |
|
||||
| `cloudWatchBatchQueries` | Runs CloudWatch metrics queries as separate batches |
|
||||
| `dashboardNewLayouts` | Enables new dashboard layouts |
|
||||
| `pdfTables` | Enables generating table data as PDF in reporting |
|
||||
| `canvasPanelPanZoom` | Allow pan and zoom in canvas panel |
|
||||
| `alertingSaveStateCompressed` | Enables the compressed protobuf-based alert state storage. Default is enabled. |
|
||||
|
||||
@@ -30,9 +30,7 @@ refs:
|
||||
|
||||
# Datagrid
|
||||
|
||||
{{< admonition type="caution" >}}
|
||||
Starting with Grafana 12.4, Datagrid is deprecated. It will be removed in version 13.0.
|
||||
{{< /admonition >}}
|
||||
{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}}
|
||||
|
||||
Datagrids offer you the ability to create, edit, and fine-tune data within Grafana. As such, this panel can act as a data source for other panels
|
||||
inside a dashboard.
|
||||
|
||||
@@ -1337,11 +1337,6 @@
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/api/onCallApi.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/components/AnnotationDetailsField.tsx": {
|
||||
"@typescript-eslint/consistent-type-assertions": {
|
||||
"count": 1
|
||||
@@ -1627,11 +1622,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/configure.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/mocks/server/handlers/plugins.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
@@ -1662,16 +1652,6 @@
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.test.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 6
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/config.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 1
|
||||
}
|
||||
},
|
||||
"public/app/features/alerting/unified/utils/datasource.ts": {
|
||||
"no-restricted-syntax": {
|
||||
"count": 2
|
||||
|
||||
@@ -293,8 +293,8 @@
|
||||
"@grafana/plugin-ui": "^0.11.1",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "6.52.2",
|
||||
"@grafana/scenes-react": "6.52.2",
|
||||
"@grafana/scenes": "v6.52.1",
|
||||
"@grafana/scenes-react": "v6.52.1",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -844,6 +844,7 @@ export {
|
||||
DataLinkConfigOrigin,
|
||||
SupportedTransformationType,
|
||||
type InternalDataLink,
|
||||
type LinkTarget,
|
||||
type LinkModel,
|
||||
type LinkModelSupplier,
|
||||
VariableOrigin,
|
||||
@@ -851,7 +852,6 @@ export {
|
||||
VariableSuggestionsScope,
|
||||
OneClickMode,
|
||||
} from './types/dataLink';
|
||||
export { type LinkTarget } from './types/linkTarget';
|
||||
export {
|
||||
type Action,
|
||||
type ActionModel,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ScopedVars } from './ScopedVars';
|
||||
import { ExploreCorrelationHelperData, ExplorePanelsState } from './explore';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
import { InterpolateFunction } from './panel';
|
||||
import { DataQuery } from './query';
|
||||
import { TimeRange } from './time';
|
||||
@@ -89,6 +88,8 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
||||
range?: TimeRange;
|
||||
}
|
||||
|
||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||
|
||||
/**
|
||||
* Processed Link Model. The values are ready to use
|
||||
*/
|
||||
|
||||
@@ -356,7 +356,7 @@ export interface FeatureToggles {
|
||||
*/
|
||||
dashboardScene?: boolean;
|
||||
/**
|
||||
* Enables new dashboard layouts
|
||||
* Enables experimental new dashboard layouts
|
||||
*/
|
||||
dashboardNewLayouts?: boolean;
|
||||
/**
|
||||
@@ -1251,8 +1251,4 @@ export interface FeatureToggles {
|
||||
* Enables profiles exemplars support in profiles drilldown
|
||||
*/
|
||||
profilesExemplars?: boolean;
|
||||
/**
|
||||
* Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||
*/
|
||||
alertingSyncDispatchTimer?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
/**
|
||||
* Target for links - controls whether link opens in new tab or same tab
|
||||
*/
|
||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ComponentType } from 'react';
|
||||
|
||||
import { LinkTarget } from './dataLink';
|
||||
import { IconName } from './icon';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
|
||||
export interface NavLinkDTO {
|
||||
id?: string;
|
||||
|
||||
@@ -11,7 +11,6 @@ import { DataFrame } from './dataFrame';
|
||||
import { DataQueryError, DataQueryRequest, DataQueryTimings } from './datasource';
|
||||
import { FieldConfigSource } from './fieldOverrides';
|
||||
import { IconName } from './icon';
|
||||
import { LinkTarget } from './linkTarget';
|
||||
import { OptionEditorConfig } from './options';
|
||||
import { PluginMeta } from './plugin';
|
||||
import { AbsoluteTimeRange, TimeRange, TimeZone } from './time';
|
||||
@@ -192,7 +191,6 @@ export interface PanelMenuItem {
|
||||
onClick?: (event: React.MouseEvent) => void;
|
||||
shortcut?: string;
|
||||
href?: string;
|
||||
target?: LinkTarget;
|
||||
subMenu?: PanelMenuItem[];
|
||||
}
|
||||
|
||||
|
||||
@@ -574,8 +574,8 @@ var (
|
||||
},
|
||||
{
|
||||
Name: "dashboardNewLayouts",
|
||||
Description: "Enables new dashboard layouts",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
Description: "Enables experimental new dashboard layouts",
|
||||
Stage: FeatureStageExperimental,
|
||||
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
@@ -981,8 +981,7 @@ var (
|
||||
Stage: FeatureStageDeprecated,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "true", // Enabled by default for now
|
||||
},
|
||||
{
|
||||
}, {
|
||||
Name: "alertingFilterV2",
|
||||
Description: "Enable the new alerting search experience",
|
||||
Stage: FeatureStageExperimental,
|
||||
@@ -2070,14 +2069,6 @@ var (
|
||||
Owner: grafanaObservabilityTracesAndProfilingSquad,
|
||||
FrontendOnly: false,
|
||||
},
|
||||
{
|
||||
Name: "alertingSyncDispatchTimer",
|
||||
Description: "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
RequiresRestart: true,
|
||||
HideFromDocs: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
3
pkg/services/featuremgmt/toggles_gen.csv
generated
3
pkg/services/featuremgmt/toggles_gen.csv
generated
@@ -79,7 +79,7 @@ annotationPermissionUpdate,GA,@grafana/identity-access-team,false,false,false
|
||||
dashboardSceneForViewers,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardSceneSolo,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardScene,GA,@grafana/dashboards-squad,false,false,true
|
||||
dashboardNewLayouts,preview,@grafana/dashboards-squad,false,false,false
|
||||
dashboardNewLayouts,experimental,@grafana/dashboards-squad,false,false,false
|
||||
dashboardUndoRedo,experimental,@grafana/dashboards-squad,false,false,true
|
||||
unlimitedLayoutsNesting,experimental,@grafana/dashboards-squad,false,false,true
|
||||
drilldownRecommendations,experimental,@grafana/dashboards-squad,false,false,true
|
||||
@@ -280,4 +280,3 @@ multiPropsVariables,experimental,@grafana/dashboards-squad,false,false,true
|
||||
smoothingTransformation,experimental,@grafana/datapro,false,false,true
|
||||
secretsManagementAppPlatformAwsKeeper,experimental,@grafana/grafana-operator-experience-squad,false,false,false
|
||||
profilesExemplars,experimental,@grafana/observability-traces-and-profiling,false,false,false
|
||||
alertingSyncDispatchTimer,experimental,@grafana/alerting-squad,false,true,false
|
||||
|
||||
|
6
pkg/services/featuremgmt/toggles_gen.go
generated
6
pkg/services/featuremgmt/toggles_gen.go
generated
@@ -260,7 +260,7 @@ const (
|
||||
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
|
||||
|
||||
// FlagDashboardNewLayouts
|
||||
// Enables new dashboard layouts
|
||||
// Enables experimental new dashboard layouts
|
||||
FlagDashboardNewLayouts = "dashboardNewLayouts"
|
||||
|
||||
// FlagPdfTables
|
||||
@@ -789,8 +789,4 @@ const (
|
||||
// FlagProfilesExemplars
|
||||
// Enables profiles exemplars support in profiles drilldown
|
||||
FlagProfilesExemplars = "profilesExemplars"
|
||||
|
||||
// FlagAlertingSyncDispatchTimer
|
||||
// Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods
|
||||
FlagAlertingSyncDispatchTimer = "alertingSyncDispatchTimer"
|
||||
)
|
||||
|
||||
28
pkg/services/featuremgmt/toggles_gen.json
generated
28
pkg/services/featuremgmt/toggles_gen.json
generated
@@ -511,20 +511,6 @@
|
||||
"frontend": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingSyncDispatchTimer",
|
||||
"resourceVersion": "1766161788928",
|
||||
"creationTimestamp": "2025-12-19T16:29:48Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Use synchronized dispatch timer to minimize duplicate notifications across alertmanager HA pods",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad",
|
||||
"requiresRestart": true,
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingTriage",
|
||||
@@ -676,8 +662,7 @@
|
||||
"metadata": {
|
||||
"name": "auditLoggingAppPlatform",
|
||||
"resourceVersion": "1767013056996",
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z",
|
||||
"deletionTimestamp": "2026-01-06T09:18:36Z"
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable audit logging with Kubernetes under app platform",
|
||||
@@ -1030,15 +1015,12 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dashboardNewLayouts",
|
||||
"resourceVersion": "1768382835527",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC"
|
||||
}
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables new dashboard layouts",
|
||||
"stage": "preview",
|
||||
"description": "Enables experimental new dashboard layouts",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/dashboards-squad"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -213,9 +213,6 @@ func (ng *AlertNG) init() error {
|
||||
SkipVerify: ng.Cfg.Smtp.SkipVerify,
|
||||
StaticHeaders: ng.Cfg.Smtp.StaticHeaders,
|
||||
}
|
||||
runtimeConfig := remoteClient.RuntimeConfig{
|
||||
DispatchTimer: notifier.GetDispatchTimer(ng.FeatureToggles).String(),
|
||||
}
|
||||
|
||||
cfg := remote.AlertmanagerConfig{
|
||||
BasicAuthPassword: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Password,
|
||||
@@ -225,7 +222,6 @@ func (ng *AlertNG) init() error {
|
||||
ExternalURL: ng.Cfg.AppURL,
|
||||
SmtpConfig: smtpCfg,
|
||||
Timeout: ng.Cfg.UnifiedAlerting.RemoteAlertmanager.Timeout,
|
||||
RuntimeConfig: runtimeConfig,
|
||||
}
|
||||
autogenFn := func(ctx context.Context, logger log.Logger, orgID int64, cfg *definitions.PostableApiAlertingConfig, invalidReceiverAction notifier.InvalidReceiversAction) error {
|
||||
return notifier.AddAutogenConfig(ctx, logger, ng.store, orgID, cfg, invalidReceiverAction, ng.FeatureToggles)
|
||||
|
||||
@@ -33,9 +33,6 @@ const (
|
||||
|
||||
// How long we keep silences in the kvstore after they've expired.
|
||||
silenceRetention = 5 * 24 * time.Hour
|
||||
|
||||
// How long we keep flushes in the kvstore after they've expired.
|
||||
flushRetention = 5 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type AlertingStore interface {
|
||||
@@ -47,10 +44,8 @@ type AlertingStore interface {
|
||||
type stateStore interface {
|
||||
SaveSilences(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
SaveNotificationLog(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error)
|
||||
GetSilences(ctx context.Context) (string, error)
|
||||
GetNotificationLog(ctx context.Context) (string, error)
|
||||
GetFlushLog(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type alertmanager struct {
|
||||
@@ -106,10 +101,6 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
flushLog, err := stateStore.GetFlushLog(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
silencesOptions := maintenanceOptions{
|
||||
initialState: silences,
|
||||
@@ -132,29 +123,12 @@ func NewAlertmanager(ctx context.Context, orgID int64, cfg *setting.Cfg, store A
|
||||
}
|
||||
l := log.New("ngalert.notifier")
|
||||
|
||||
dispatchTimer := GetDispatchTimer(featureToggles)
|
||||
|
||||
var flushLogOptions *maintenanceOptions
|
||||
if dispatchTimer == alertingNotify.DispatchTimerSync {
|
||||
flushLogOptions = &maintenanceOptions{
|
||||
initialState: flushLog,
|
||||
retention: flushRetention,
|
||||
maintenanceFrequency: maintenanceInterval,
|
||||
maintenanceFunc: func(state alertingNotify.State) (int64, error) {
|
||||
// Detached context here is to make sure that when the service is shut down the persist operation is executed.
|
||||
return stateStore.SaveFlushLog(context.Background(), state)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
opts := alertingNotify.GrafanaAlertmanagerOpts{
|
||||
ExternalURL: cfg.AppURL,
|
||||
AlertStoreCallback: nil,
|
||||
PeerTimeout: cfg.UnifiedAlerting.HAPeerTimeout,
|
||||
Silences: silencesOptions,
|
||||
Nflog: nflogOptions,
|
||||
FlushLog: flushLogOptions,
|
||||
DispatchTimer: dispatchTimer,
|
||||
Limits: alertingNotify.Limits{
|
||||
MaxSilences: cfg.UnifiedAlerting.AlertmanagerMaxSilencesCount,
|
||||
MaxSilenceSizeBytes: cfg.UnifiedAlerting.AlertmanagerMaxSilenceSizeBytes,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
)
|
||||
|
||||
// GetDispatchTimer returns the appropriate dispatch timer based on feature toggles.
|
||||
func GetDispatchTimer(features featuremgmt.FeatureToggles) (dt alertingNotify.DispatchTimer) {
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
enabled := features.IsEnabledGlobally(featuremgmt.FlagAlertingSyncDispatchTimer)
|
||||
if enabled {
|
||||
dt = alertingNotify.DispatchTimerSync
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package notifier
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
alertingNotify "github.com/grafana/alerting/notify"
|
||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestGetDispatchTimer(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
featureFlagValue bool
|
||||
expected alertingNotify.DispatchTimer
|
||||
}{
|
||||
{
|
||||
name: "feature flag enabled returns sync timer",
|
||||
featureFlagValue: true,
|
||||
expected: alertingNotify.DispatchTimerSync,
|
||||
},
|
||||
{
|
||||
name: "feature flag disabled returns default timer",
|
||||
featureFlagValue: false,
|
||||
expected: alertingNotify.DispatchTimerDefault,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
features := featuremgmt.WithFeatures(featuremgmt.FlagAlertingSyncDispatchTimer, tt.featureFlagValue)
|
||||
result := GetDispatchTimer(features)
|
||||
require.Equal(t, tt.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,6 @@ const (
|
||||
KVNamespace = "alertmanager"
|
||||
NotificationLogFilename = "notifications"
|
||||
SilencesFilename = "silences"
|
||||
FlushLogFilename = "flushes"
|
||||
)
|
||||
|
||||
// FileStore is in charge of persisting the alertmanager files to the database.
|
||||
@@ -43,10 +42,6 @@ func (fileStore *FileStore) GetNotificationLog(ctx context.Context) (string, err
|
||||
return fileStore.contentFor(ctx, NotificationLogFilename)
|
||||
}
|
||||
|
||||
func (fileStore *FileStore) GetFlushLog(ctx context.Context) (string, error) {
|
||||
return fileStore.contentFor(ctx, FlushLogFilename)
|
||||
}
|
||||
|
||||
// contentFor returns the content for the given Alertmanager kvstore key.
|
||||
func (fileStore *FileStore) contentFor(ctx context.Context, filename string) (string, error) {
|
||||
// Then, let's attempt to read it from the database.
|
||||
@@ -79,11 +74,6 @@ func (fileStore *FileStore) SaveNotificationLog(ctx context.Context, st alerting
|
||||
return fileStore.persist(ctx, NotificationLogFilename, st)
|
||||
}
|
||||
|
||||
// SaveFlushLog saves the flush log to the database and returns the size of the unencoded state.
|
||||
func (fileStore *FileStore) SaveFlushLog(ctx context.Context, st alertingNotify.State) (int64, error) {
|
||||
return fileStore.persist(ctx, FlushLogFilename, st)
|
||||
}
|
||||
|
||||
// persist takes care of persisting the binary representation of internal state to the database as a base64 encoded string.
|
||||
func (fileStore *FileStore) persist(ctx context.Context, filename string, st alertingNotify.State) (int64, error) {
|
||||
var size int64
|
||||
|
||||
@@ -106,48 +106,3 @@ func TestFileStore_NotificationLog(t *testing.T) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileStore_FlushLog(t *testing.T) {
|
||||
store := fakes.NewFakeKVStore(t)
|
||||
ctx := context.Background()
|
||||
var orgId int64 = 1
|
||||
|
||||
// Initialize kvstore with empty flush log state.
|
||||
initialState := flushLogState{} // FlushLog uses the same structure as nflog
|
||||
decodedState, err := initialState.MarshalBinary()
|
||||
require.NoError(t, err)
|
||||
encodedState := base64.StdEncoding.EncodeToString(decodedState)
|
||||
err = store.Set(ctx, orgId, KVNamespace, FlushLogFilename, encodedState)
|
||||
require.NoError(t, err)
|
||||
|
||||
fs := NewFileStore(orgId, store)
|
||||
|
||||
// Load initial (empty).
|
||||
flushLog, err := fs.GetFlushLog(ctx)
|
||||
require.NoError(t, err)
|
||||
decoded, err := decodeFlushLogState(strings.NewReader(flushLog))
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(initialState, decoded) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(initialState, decoded))
|
||||
}
|
||||
|
||||
// Save new flush log state.
|
||||
now := time.Now()
|
||||
oneHour := now.Add(time.Hour)
|
||||
|
||||
v1 := createFlushLog(1, now, oneHour)
|
||||
v2 := createFlushLog(2, now, oneHour)
|
||||
newState := flushLogState{1: v1, 2: v2}
|
||||
size, err := fs.SaveFlushLog(ctx, newState)
|
||||
require.NoError(t, err)
|
||||
require.Greater(t, size, int64(0))
|
||||
|
||||
// Load new.
|
||||
flushLog, err = fs.GetFlushLog(ctx)
|
||||
require.NoError(t, err)
|
||||
decoded, err = decodeFlushLogState(strings.NewReader(flushLog))
|
||||
require.NoError(t, err)
|
||||
if !cmp.Equal(newState, decoded) {
|
||||
t.Errorf("Unexpected Diff: %v", cmp.Diff(newState, decoded))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +82,6 @@ type Alertmanager interface {
|
||||
type ExternalState struct {
|
||||
Silences []byte
|
||||
Nflog []byte
|
||||
FlushLog []byte
|
||||
}
|
||||
|
||||
// StateMerger describes a type that is able to merge external state (nflog, silences) with its own.
|
||||
@@ -379,7 +378,7 @@ func (moa *MultiOrgAlertmanager) SyncAlertmanagersForOrgs(ctx context.Context, o
|
||||
func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context,
|
||||
activeOrganizations map[int64]struct{},
|
||||
) {
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename, FlushLogFilename}
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename}
|
||||
for _, fileName := range storedFiles {
|
||||
keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,8 +5,5 @@ func (am *alertmanager) MergeState(state ExternalState) error {
|
||||
if err := am.Base.MergeNflog(state.Nflog); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := am.Base.MergeSilences(state.Silences); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.Base.MergeFlushLog(state.FlushLog)
|
||||
return am.Base.MergeSilences(state.Silences)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/matttproud/golang_protobuf_extensions/pbutil"
|
||||
"github.com/prometheus/alertmanager/flushlog/flushlogpb"
|
||||
"github.com/prometheus/alertmanager/nflog/nflogpb"
|
||||
"github.com/prometheus/alertmanager/silence/silencepb"
|
||||
"github.com/prometheus/common/model"
|
||||
@@ -229,13 +228,15 @@ func (f *FakeOrgStore) FetchOrgIds(_ context.Context) ([]int64, error) {
|
||||
return f.orgs, nil
|
||||
}
|
||||
|
||||
type NoValidation struct{}
|
||||
type NoValidation struct {
|
||||
}
|
||||
|
||||
func (n NoValidation) Validate(_ models.NotificationSettings) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type RejectingValidation struct{}
|
||||
type RejectingValidation struct {
|
||||
}
|
||||
|
||||
func (n RejectingValidation) Validate(s models.NotificationSettings) error {
|
||||
return ErrorReceiverDoesNotExist{ErrorReferenceInvalid: ErrorReferenceInvalid{Reference: s.Receiver}}
|
||||
@@ -364,51 +365,6 @@ func createNotificationLog(groupKey string, receiverName string, sentAt, expires
|
||||
}
|
||||
}
|
||||
|
||||
// https://github.com/grafana/prometheus-alertmanager/blob/main/flushlog/flushlog.go#L136-L136
|
||||
type flushLogState map[uint64]*flushlogpb.MeshFlushLog
|
||||
|
||||
func (s flushLogState) MarshalBinary() ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
for _, e := range s {
|
||||
if _, err := pbutil.WriteDelimited(&buf, e); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func createFlushLog(groupFingerprint uint64, ts, expiresAt time.Time) *flushlogpb.MeshFlushLog {
|
||||
return &flushlogpb.MeshFlushLog{
|
||||
FlushLog: &flushlogpb.FlushLog{
|
||||
GroupFingerprint: groupFingerprint,
|
||||
Timestamp: ts,
|
||||
},
|
||||
ExpiresAt: expiresAt,
|
||||
}
|
||||
}
|
||||
|
||||
// decodeFlushLogState copied from decodeState in prometheus-alertmanager/flushlog/flushlog.go
|
||||
func decodeFlushLogState(r io.Reader) (flushLogState, error) {
|
||||
st := flushLogState{}
|
||||
for {
|
||||
var e flushlogpb.MeshFlushLog
|
||||
_, err := pbutil.ReadDelimited(r, &e)
|
||||
if err == nil {
|
||||
if e.FlushLog == nil || e.FlushLog.GroupFingerprint == 0 || e.FlushLog.Timestamp.IsZero() {
|
||||
return nil, errInvalidState
|
||||
}
|
||||
st[e.FlushLog.GroupFingerprint] = &e
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, io.EOF) {
|
||||
break
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return st, nil
|
||||
}
|
||||
|
||||
type call struct {
|
||||
Method string
|
||||
Args []interface{}
|
||||
|
||||
@@ -47,7 +47,6 @@ import (
|
||||
type stateStore interface {
|
||||
GetSilences(ctx context.Context) (string, error)
|
||||
GetNotificationLog(ctx context.Context) (string, error)
|
||||
GetFlushLog(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
// AutogenFn is a function that adds auto-generated routes to a configuration.
|
||||
@@ -87,8 +86,6 @@ type Alertmanager struct {
|
||||
|
||||
promoteConfig bool
|
||||
externalURL string
|
||||
|
||||
runtimeConfig remoteClient.RuntimeConfig
|
||||
}
|
||||
|
||||
type AlertmanagerConfig struct {
|
||||
@@ -114,9 +111,6 @@ type AlertmanagerConfig struct {
|
||||
|
||||
// Timeout for the HTTP client.
|
||||
Timeout time.Duration
|
||||
|
||||
// RuntimeConfig specifies runtime behavior settings for the remote Alertmanager.
|
||||
RuntimeConfig remoteClient.RuntimeConfig
|
||||
}
|
||||
|
||||
func (cfg *AlertmanagerConfig) Validate() error {
|
||||
@@ -209,7 +203,6 @@ func NewAlertmanager(ctx context.Context, cfg AlertmanagerConfig, store stateSto
|
||||
externalURL: cfg.ExternalURL,
|
||||
promoteConfig: cfg.PromoteConfig,
|
||||
smtp: cfg.SmtpConfig,
|
||||
runtimeConfig: cfg.RuntimeConfig,
|
||||
}
|
||||
|
||||
// Parse the default configuration once and remember its hash so we can compare it later.
|
||||
@@ -338,11 +331,10 @@ func (am *Alertmanager) buildConfiguration(ctx context.Context, raw []byte, crea
|
||||
AlertmanagerConfig: mergeResult.Config,
|
||||
Templates: templates,
|
||||
},
|
||||
CreatedAt: createdAtEpoch,
|
||||
Promoted: am.promoteConfig,
|
||||
ExternalURL: am.externalURL,
|
||||
SmtpConfig: am.smtp,
|
||||
RuntimeConfig: am.runtimeConfig,
|
||||
CreatedAt: createdAtEpoch,
|
||||
Promoted: am.promoteConfig,
|
||||
ExternalURL: am.externalURL,
|
||||
SmtpConfig: am.smtp,
|
||||
}
|
||||
|
||||
cfgHash, err := calculateUserGrafanaConfigHash(payload)
|
||||
@@ -396,8 +388,6 @@ func (am *Alertmanager) GetRemoteState(ctx context.Context) (notifier.ExternalSt
|
||||
rs.Silences = p.Data
|
||||
case "nfl":
|
||||
rs.Nflog = p.Data
|
||||
case "fls":
|
||||
rs.FlushLog = p.Data
|
||||
default:
|
||||
return rs, fmt.Errorf("unknown part key %q", p.Key)
|
||||
}
|
||||
@@ -687,12 +677,6 @@ func (am *Alertmanager) getFullState(ctx context.Context) (string, error) {
|
||||
}
|
||||
parts = append(parts, alertingClusterPB.Part{Key: notifier.NotificationLogFilename, Data: []byte(notificationLog)})
|
||||
|
||||
flushLog, err := am.state.GetFlushLog(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error getting flush log: %w", err)
|
||||
}
|
||||
parts = append(parts, alertingClusterPB.Part{Key: notifier.FlushLogFilename, Data: []byte(flushLog)})
|
||||
|
||||
fs := alertingClusterPB.FullState{
|
||||
Parts: parts,
|
||||
}
|
||||
|
||||
@@ -29,10 +29,6 @@ func (u *GrafanaAlertmanagerConfig) MarshalJSON() ([]byte, error) {
|
||||
return definition.MarshalJSONWithSecrets((*cfg)(u))
|
||||
}
|
||||
|
||||
type RuntimeConfig struct {
|
||||
DispatchTimer string `json:"dispatch_timer"`
|
||||
}
|
||||
|
||||
type UserGrafanaConfig struct {
|
||||
GrafanaAlertmanagerConfig GrafanaAlertmanagerConfig `json:"configuration"`
|
||||
Hash string `json:"configuration_hash"`
|
||||
@@ -41,7 +37,6 @@ type UserGrafanaConfig struct {
|
||||
Promoted bool `json:"promoted"`
|
||||
ExternalURL string `json:"external_url"`
|
||||
SmtpConfig SmtpConfig `json:"smtp_config"`
|
||||
RuntimeConfig RuntimeConfig `json:"runtime_config"`
|
||||
}
|
||||
|
||||
func (mc *Mimir) GetGrafanaAlertmanagerConfig(ctx context.Context) (*UserGrafanaConfig, error) {
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { getIrmIfPresentOrIncidentPluginId } from '../utils/config';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
interface IncidentsPluginConfigDto {
|
||||
@@ -7,13 +5,13 @@ interface IncidentsPluginConfigDto {
|
||||
isIncidentCreated: boolean;
|
||||
}
|
||||
|
||||
const getProxyApiUrl = (path: string) => `/api/plugins/${getIrmIfPresentOrIncidentPluginId()}/resources${path}`;
|
||||
const getProxyApiUrl = (path: string, pluginId: string) => `/api/plugins/${pluginId}/resources${path}`;
|
||||
|
||||
export const incidentsApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker'),
|
||||
getIncidentsPluginConfig: build.query<IncidentsPluginConfigDto, { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/api/ConfigurationTrackerService.GetConfigurationTracker', pluginId),
|
||||
data: {},
|
||||
method: 'POST',
|
||||
showErrorAlert: false,
|
||||
|
||||
@@ -1,26 +1,16 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { getProxyApiUrl } from './onCallApi';
|
||||
|
||||
describe('getProxyApiUrl', () => {
|
||||
it('should return URL with IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
|
||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||
it('should return URL with IRM plugin ID when IRM plugin ID is passed', () => {
|
||||
expect(getProxyApiUrl('/alert_receive_channels/', SupportedPlugin.Irm)).toBe(
|
||||
'/api/plugins/grafana-irm-app/resources/alert_receive_channels/'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return URL with OnCall plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
|
||||
expect(getProxyApiUrl('/alert_receive_channels/')).toBe(
|
||||
it('should return URL with OnCall plugin ID when OnCall plugin ID is passed', () => {
|
||||
expect(getProxyApiUrl('/alert_receive_channels/', SupportedPlugin.OnCall)).toBe(
|
||||
'/api/plugins/grafana-oncall-app/resources/alert_receive_channels/'
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { FetchError, isFetchError } from '@grafana/runtime';
|
||||
|
||||
import { GRAFANA_ONCALL_INTEGRATION_TYPE } from '../components/receivers/grafanaAppReceivers/onCall/onCall';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../utils/config';
|
||||
|
||||
import { alertingApi } from './alertingApi';
|
||||
|
||||
@@ -38,15 +37,15 @@ export interface OnCallConfigChecks {
|
||||
is_integration_chatops_connected: boolean;
|
||||
}
|
||||
|
||||
export function getProxyApiUrl(path: string) {
|
||||
return `/api/plugins/${getIrmIfPresentOrOnCallPluginId()}/resources${path}`;
|
||||
export function getProxyApiUrl(path: string, pluginId: string) {
|
||||
return `/api/plugins/${pluginId}/resources${path}`;
|
||||
}
|
||||
|
||||
export const onCallApi = alertingApi.injectEndpoints({
|
||||
endpoints: (build) => ({
|
||||
grafanaOnCallIntegrations: build.query<OnCallIntegrationDTO[], void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/'),
|
||||
grafanaOnCallIntegrations: build.query<OnCallIntegrationDTO[], { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/', pluginId),
|
||||
// legacy_grafana_alerting is necessary for OnCall.
|
||||
// We do NOT need to differentiate between these two on our side
|
||||
params: {
|
||||
@@ -64,31 +63,31 @@ export const onCallApi = alertingApi.injectEndpoints({
|
||||
},
|
||||
providesTags: ['OnCallIntegrations'],
|
||||
}),
|
||||
validateIntegrationName: build.query<boolean, string>({
|
||||
query: (name) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/validate_name/'),
|
||||
validateIntegrationName: build.query<boolean, { name: string; pluginId: string }>({
|
||||
query: ({ name, pluginId }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/validate_name/', pluginId),
|
||||
params: { verbal_name: name },
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO>({
|
||||
query: (integration) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/'),
|
||||
createIntegration: build.mutation<NewOnCallIntegrationDTO, CreateIntegrationDTO & { pluginId: string }>({
|
||||
query: ({ pluginId, ...integration }) => ({
|
||||
url: getProxyApiUrl('/alert_receive_channels/', pluginId),
|
||||
data: integration,
|
||||
method: 'POST',
|
||||
showErrorAlert: true,
|
||||
}),
|
||||
invalidatesTags: ['OnCallIntegrations'],
|
||||
}),
|
||||
features: build.query<OnCallFeature[], void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/features/'),
|
||||
features: build.query<OnCallFeature[], { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/features/', pluginId),
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
onCallConfigChecks: build.query<OnCallConfigChecks, void>({
|
||||
query: () => ({
|
||||
url: getProxyApiUrl('/organization/config-checks/'),
|
||||
onCallConfigChecks: build.query<OnCallConfigChecks, { pluginId: string }>({
|
||||
query: ({ pluginId }) => ({
|
||||
url: getProxyApiUrl('/organization/config-checks/', pluginId),
|
||||
showErrorAlert: false,
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { Button, LinkButton, Menu, Tooltip } from '@grafana/ui';
|
||||
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrIncidentPluginId } from '../../utils/config';
|
||||
import { useIrmPlugin } from '../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import { createBridgeURL } from '../PluginBridge';
|
||||
|
||||
interface Props {
|
||||
@@ -11,20 +11,18 @@ interface Props {
|
||||
url?: string;
|
||||
}
|
||||
|
||||
const pluginId = getIrmIfPresentOrIncidentPluginId();
|
||||
|
||||
export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: Props) => {
|
||||
const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.Incident);
|
||||
|
||||
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
|
||||
title,
|
||||
severity,
|
||||
url,
|
||||
});
|
||||
|
||||
const { loading, installed, settings } = usePluginBridge(pluginId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading === true && (
|
||||
{loading && (
|
||||
<Button icon="fire" size="sm" type="button" disabled>
|
||||
<Trans i18nKey="alerting.declare-incident-button.declare-incident">Declare Incident</Trans>
|
||||
</Button>
|
||||
@@ -51,17 +49,17 @@ export const DeclareIncidentButton = ({ title = '', severity = '', url = '' }: P
|
||||
};
|
||||
|
||||
export const DeclareIncidentMenuItem = ({ title = '', severity = '', url = '' }: Props) => {
|
||||
const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.Incident);
|
||||
|
||||
const bridgeURL = createBridgeURL(pluginId, '/incidents/declare', {
|
||||
title,
|
||||
severity,
|
||||
url,
|
||||
});
|
||||
|
||||
const { loading, installed, settings } = usePluginBridge(pluginId);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loading === true && (
|
||||
{loading && (
|
||||
<Menu.Item
|
||||
label={t('alerting.declare-incident-menu-item.label-declare-incident', 'Declare incident')}
|
||||
icon="fire"
|
||||
|
||||
@@ -18,10 +18,10 @@ import { getAPINamespace } from '../../../../../api/utils';
|
||||
import { alertmanagerApi } from '../../api/alertmanagerApi';
|
||||
import { onCallApi } from '../../api/onCallApi';
|
||||
import { useAsync } from '../../hooks/useAsync';
|
||||
import { usePluginBridge } from '../../hooks/usePluginBridge';
|
||||
import { useIrmPlugin } from '../../hooks/usePluginBridge';
|
||||
import { useProduceNewAlertmanagerConfiguration } from '../../hooks/useProduceNewAlertmanagerConfig';
|
||||
import { addReceiverAction, deleteReceiverAction, updateReceiverAction } from '../../reducers/alertmanager/receivers';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from '../../utils/config';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
|
||||
import { enhanceContactPointsWithMetadata } from './utils';
|
||||
|
||||
@@ -61,8 +61,8 @@ const defaultOptions = {
|
||||
* Otherwise, returns no data
|
||||
*/
|
||||
const useOnCallIntegrations = ({ skip }: Skippable = {}) => {
|
||||
const { installed, loading } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery(undefined, { skip: skip || !installed });
|
||||
const { pluginId, installed, loading } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
const oncallIntegrationsResponse = useGrafanaOnCallIntegrationsQuery({ pluginId }, { skip: skip || !installed });
|
||||
|
||||
return useMemo(() => {
|
||||
if (installed) {
|
||||
@@ -126,6 +126,10 @@ export const useGrafanaContactPoints = ({
|
||||
}: GrafanaFetchOptions & Skippable = {}) => {
|
||||
const namespace = getAPINamespace();
|
||||
const potentiallySkip = { skip };
|
||||
|
||||
// Get the IRM/OnCall plugin information
|
||||
const irmOrOnCallPlugin = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const onCallResponse = useOnCallIntegrations(potentiallySkip);
|
||||
const alertNotifiers = useGrafanaNotifiersQuery(undefined, potentiallySkip);
|
||||
const contactPointsListResponse = useK8sContactPoints({ namespace }, potentiallySkip);
|
||||
@@ -158,6 +162,7 @@ export const useGrafanaContactPoints = ({
|
||||
status: contactPointsStatusResponse.data,
|
||||
notifiers: alertNotifiers.data,
|
||||
onCallIntegrations: onCallResponse?.data,
|
||||
onCallPluginId: irmOrOnCallPlugin.pluginId,
|
||||
contactPoints: contactPointsListResponse.data || [],
|
||||
alertmanagerConfiguration: alertmanagerConfigResponse.data,
|
||||
});
|
||||
@@ -172,6 +177,7 @@ export const useGrafanaContactPoints = ({
|
||||
contactPointsListResponse,
|
||||
contactPointsStatusResponse,
|
||||
onCallResponse,
|
||||
irmOrOnCallPlugin.pluginId,
|
||||
]);
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
} from 'app/plugins/datasource/alertmanager/types';
|
||||
|
||||
import { OnCallIntegrationDTO } from '../../api/onCallApi';
|
||||
import { SupportedPlugin } from '../../types/pluginBridges';
|
||||
import { extractReceivers } from '../../utils/receivers';
|
||||
import { routeAdapter } from '../../utils/routeAdapter';
|
||||
import { ReceiverTypes } from '../receivers/grafanaAppReceivers/onCall/onCall';
|
||||
@@ -113,7 +114,8 @@ export interface ContactPointWithMetadata extends GrafanaManagedContactPoint {
|
||||
type EnhanceContactPointsArgs = {
|
||||
status?: ReceiversStateDTO[];
|
||||
notifiers?: NotifierDTO[];
|
||||
onCallIntegrations?: OnCallIntegrationDTO[] | undefined | null;
|
||||
onCallIntegrations?: OnCallIntegrationDTO[];
|
||||
onCallPluginId?: SupportedPlugin;
|
||||
contactPoints: Receiver[];
|
||||
alertmanagerConfiguration?: AlertManagerCortexConfig;
|
||||
};
|
||||
@@ -130,6 +132,7 @@ export function enhanceContactPointsWithMetadata({
|
||||
status = [],
|
||||
notifiers = [],
|
||||
onCallIntegrations,
|
||||
onCallPluginId,
|
||||
contactPoints,
|
||||
alertmanagerConfiguration,
|
||||
}: EnhanceContactPointsArgs): ContactPointWithMetadata[] {
|
||||
@@ -162,7 +165,7 @@ export function enhanceContactPointsWithMetadata({
|
||||
[RECEIVER_META_KEY]: getNotifierMetadata(notifiers, receiver),
|
||||
// if OnCall plugin is installed, we'll add it to the receiver's plugin metadata
|
||||
[RECEIVER_PLUGIN_META_KEY]: isOnCallReceiver
|
||||
? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration))
|
||||
? getOnCallMetadata(onCallIntegrations, receiver, Boolean(alertmanagerConfiguration), onCallPluginId)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -6,12 +6,12 @@ import { t } from '@grafana/i18n';
|
||||
import { isFetchError } from '@grafana/runtime';
|
||||
import { Badge } from '@grafana/ui';
|
||||
import { NotifierDTO } from 'app/features/alerting/unified/types/alerting';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
|
||||
import { useAppNotification } from '../../../../../../../core/copy/appNotification';
|
||||
import { Receiver } from '../../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { ONCALL_INTEGRATION_V2_FEATURE, onCallApi } from '../../../../api/onCallApi';
|
||||
import { usePluginBridge } from '../../../../hooks/usePluginBridge';
|
||||
import { useIrmPlugin } from '../../../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../../../types/pluginBridges';
|
||||
import { option } from '../../../../utils/notifier-types';
|
||||
|
||||
import { GRAFANA_ONCALL_INTEGRATION_TYPE, ReceiverTypes } from './onCall';
|
||||
@@ -39,16 +39,17 @@ enum OnCallIntegrationStatus {
|
||||
|
||||
function useOnCallPluginStatus() {
|
||||
const {
|
||||
pluginId,
|
||||
installed: isOnCallEnabled,
|
||||
loading: isPluginBridgeLoading,
|
||||
error: pluginError,
|
||||
} = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
} = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const {
|
||||
data: onCallFeatures = [],
|
||||
error: onCallFeaturesError,
|
||||
isLoading: isOnCallFeaturesLoading,
|
||||
} = onCallApi.endpoints.features.useQuery(undefined, { skip: !isOnCallEnabled });
|
||||
} = onCallApi.endpoints.features.useQuery({ pluginId }, { skip: !isOnCallEnabled });
|
||||
|
||||
const integrationStatus = useMemo((): OnCallIntegrationStatus => {
|
||||
if (!isOnCallEnabled) {
|
||||
@@ -67,6 +68,7 @@ function useOnCallPluginStatus() {
|
||||
);
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
isOnCallEnabled,
|
||||
integrationStatus,
|
||||
isAlertingV2IntegrationEnabled,
|
||||
@@ -78,8 +80,14 @@ function useOnCallPluginStatus() {
|
||||
export function useOnCallIntegration() {
|
||||
const notifyApp = useAppNotification();
|
||||
|
||||
const { isOnCallEnabled, integrationStatus, isAlertingV2IntegrationEnabled, isOnCallStatusLoading, onCallError } =
|
||||
useOnCallPluginStatus();
|
||||
const {
|
||||
pluginId,
|
||||
isOnCallEnabled,
|
||||
integrationStatus,
|
||||
isAlertingV2IntegrationEnabled,
|
||||
isOnCallStatusLoading,
|
||||
onCallError,
|
||||
} = useOnCallPluginStatus();
|
||||
|
||||
const { useCreateIntegrationMutation, useGrafanaOnCallIntegrationsQuery, useLazyValidateIntegrationNameQuery } =
|
||||
onCallApi;
|
||||
@@ -91,13 +99,13 @@ export function useOnCallIntegration() {
|
||||
data: grafanaOnCallIntegrations = [],
|
||||
isLoading: isLoadingOnCallIntegrations,
|
||||
isError: isIntegrationsQueryError,
|
||||
} = useGrafanaOnCallIntegrationsQuery(undefined, { skip: !isAlertingV2IntegrationEnabled });
|
||||
} = useGrafanaOnCallIntegrationsQuery({ pluginId }, { skip: !isAlertingV2IntegrationEnabled });
|
||||
|
||||
const onCallFormValidators = useMemo(() => {
|
||||
return {
|
||||
integration_name: async (value: string) => {
|
||||
try {
|
||||
await validateIntegrationNameQuery(value).unwrap();
|
||||
await validateIntegrationNameQuery({ name: value, pluginId }).unwrap();
|
||||
return true;
|
||||
} catch (error) {
|
||||
if (isFetchError(error) && error.status === 409) {
|
||||
@@ -126,7 +134,7 @@ export function useOnCallIntegration() {
|
||||
: t('alerting.irm-integration.integration-required', 'Selection of existing IRM integration is required');
|
||||
},
|
||||
};
|
||||
}, [grafanaOnCallIntegrations, validateIntegrationNameQuery, isAlertingV2IntegrationEnabled, notifyApp]);
|
||||
}, [grafanaOnCallIntegrations, validateIntegrationNameQuery, isAlertingV2IntegrationEnabled, notifyApp, pluginId]);
|
||||
|
||||
const extendOnCallReceivers = useCallback(
|
||||
(receiver: Receiver): Receiver => {
|
||||
@@ -159,6 +167,7 @@ export function useOnCallIntegration() {
|
||||
|
||||
const createNewOnCallIntegrationJobs = newOnCallIntegrations.map(async (c) => {
|
||||
const newIntegration = await createIntegrationMutation({
|
||||
pluginId,
|
||||
integration: GRAFANA_ONCALL_INTEGRATION_TYPE,
|
||||
verbal_name: c.settings[OnCallIntegrationSetting.IntegrationName],
|
||||
}).unwrap();
|
||||
@@ -180,7 +189,7 @@ export function useOnCallIntegration() {
|
||||
});
|
||||
});
|
||||
},
|
||||
[isAlertingV2IntegrationEnabled, createIntegrationMutation]
|
||||
[isAlertingV2IntegrationEnabled, createIntegrationMutation, pluginId]
|
||||
);
|
||||
|
||||
const extendOnCallNotifierFeatures = useCallback(
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { GrafanaManagedReceiverConfig } from '../../../../../../plugins/datasource/alertmanager/types';
|
||||
import { OnCallIntegrationDTO } from '../../../api/onCallApi';
|
||||
import { getIrmIfPresentOrOnCallPluginId, getIsIrmPluginPresent } from '../../../utils/config';
|
||||
import { useIrmPlugin } from '../../../hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from '../../../types/pluginBridges';
|
||||
import { createBridgeURL } from '../../PluginBridge';
|
||||
|
||||
import { GRAFANA_APP_RECEIVERS_SOURCE_IMAGE } from './types';
|
||||
@@ -13,20 +16,34 @@ export interface ReceiverPluginMetadata {
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
const onCallReceiverICon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[getIrmIfPresentOrOnCallPluginId()];
|
||||
const onCallReceiverTitle = 'Grafana OnCall';
|
||||
|
||||
export const onCallReceiverMeta: ReceiverPluginMetadata = {
|
||||
title: onCallReceiverTitle,
|
||||
icon: onCallReceiverICon,
|
||||
};
|
||||
|
||||
export function getOnCallMetadata(
|
||||
export function useOnCallMetadata(
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
|
||||
receiver: GrafanaManagedReceiverConfig,
|
||||
hasAlertManagerConfigData = true
|
||||
): ReceiverPluginMetadata {
|
||||
const pluginName = getIsIrmPluginPresent() ? 'IRM' : 'OnCall';
|
||||
const { pluginId } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
return useMemo(
|
||||
() => getOnCallMetadata(onCallIntegrations, receiver, hasAlertManagerConfigData, pluginId),
|
||||
[onCallIntegrations, receiver, hasAlertManagerConfigData, pluginId]
|
||||
);
|
||||
}
|
||||
|
||||
export function getOnCallMetadata(
|
||||
onCallIntegrations: OnCallIntegrationDTO[] | undefined | null,
|
||||
receiver: GrafanaManagedReceiverConfig,
|
||||
hasAlertManagerConfigData = true,
|
||||
onCallPluginId?: SupportedPlugin
|
||||
): ReceiverPluginMetadata {
|
||||
const pluginId = onCallPluginId || SupportedPlugin.OnCall;
|
||||
const pluginName = pluginId === SupportedPlugin.Irm ? 'IRM' : 'OnCall';
|
||||
const onCallReceiverIcon = GRAFANA_APP_RECEIVERS_SOURCE_IMAGE[pluginId];
|
||||
const onCallReceiverTitle = pluginId === SupportedPlugin.Irm ? 'Grafana IRM' : 'Grafana OnCall';
|
||||
|
||||
const onCallReceiverMeta: ReceiverPluginMetadata = {
|
||||
title: onCallReceiverTitle,
|
||||
icon: onCallReceiverIcon,
|
||||
};
|
||||
|
||||
if (!hasAlertManagerConfigData) {
|
||||
return onCallReceiverMeta;
|
||||
@@ -57,7 +74,7 @@ export function getOnCallMetadata(
|
||||
...onCallReceiverMeta,
|
||||
description: matchingOnCallIntegration?.display_name,
|
||||
externalUrl: matchingOnCallIntegration
|
||||
? createBridgeURL(getIrmIfPresentOrOnCallPluginId(), `/integrations/${matchingOnCallIntegration.value}`)
|
||||
? createBridgeURL(pluginId, `/integrations/${matchingOnCallIntegration.value}`)
|
||||
: undefined,
|
||||
warning: matchingOnCallIntegration ? undefined : `${pluginName} Integration no longer exists`,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { pluginMeta } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { useIrmPlugin } from './usePluginBridge';
|
||||
|
||||
jest.mock('app/features/plugins/pluginSettings');
|
||||
|
||||
const mockedGetPluginSettings = jest.mocked(getPluginSettings);
|
||||
|
||||
describe('useIrmPlugin', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when IRM plugin is installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve({ ...pluginMeta[SupportedPlugin.OnCall], enabled: false });
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return OnCall plugin ID when IRM plugin is not installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.OnCall]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.OnCall);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return Incident plugin ID when IRM plugin is not installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Incident) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Incident]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.Incident));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Incident);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toBeDefined();
|
||||
});
|
||||
|
||||
it('should return loading state while fetching plugins', () => {
|
||||
mockedGetPluginSettings.mockImplementation(
|
||||
() => new Promise((resolve) => setTimeout(() => resolve(pluginMeta[SupportedPlugin.Irm]), 100))
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(result.current.installed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return installed undefined when neither plugin is installed', async () => {
|
||||
mockedGetPluginSettings.mockRejectedValue(new Error('Plugin not found'));
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.OnCall);
|
||||
expect(result.current.installed).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when both IRM and OnCall are installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.OnCall) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.OnCall]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.OnCall));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toEqual(pluginMeta[SupportedPlugin.Irm]);
|
||||
});
|
||||
|
||||
it('should return IRM plugin ID when both IRM and Incident are installed', async () => {
|
||||
mockedGetPluginSettings.mockImplementation((pluginId) => {
|
||||
if (pluginId === SupportedPlugin.Irm) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Irm]);
|
||||
}
|
||||
if (pluginId === SupportedPlugin.Incident) {
|
||||
return Promise.resolve(pluginMeta[SupportedPlugin.Incident]);
|
||||
}
|
||||
return Promise.reject(new Error('Plugin not found'));
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useIrmPlugin(SupportedPlugin.Incident));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.loading).toBe(false);
|
||||
});
|
||||
|
||||
expect(result.current.pluginId).toBe(SupportedPlugin.Irm);
|
||||
expect(result.current.installed).toBe(true);
|
||||
expect(result.current.settings).toEqual(pluginMeta[SupportedPlugin.Irm]);
|
||||
});
|
||||
});
|
||||
@@ -4,6 +4,8 @@ import { PluginMeta } from '@grafana/data';
|
||||
import { getPluginSettings } from 'app/features/plugins/pluginSettings';
|
||||
|
||||
import { PluginID } from '../components/PluginBridge';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
interface PluginBridgeHookResponse {
|
||||
loading: boolean;
|
||||
installed?: boolean;
|
||||
@@ -14,17 +16,54 @@ interface PluginBridgeHookResponse {
|
||||
export function usePluginBridge(plugin: PluginID): PluginBridgeHookResponse {
|
||||
const { loading, error, value } = useAsync(() => getPluginSettings(plugin, { showErrorAlert: false }));
|
||||
|
||||
const installed = value && !error && !loading;
|
||||
const enabled = value?.enabled;
|
||||
const isLoading = loading && !value;
|
||||
|
||||
if (isLoading) {
|
||||
if (loading) {
|
||||
return { loading: true };
|
||||
}
|
||||
|
||||
if (!installed || !enabled) {
|
||||
return { loading: false, installed: false };
|
||||
if (error) {
|
||||
return { loading, error };
|
||||
}
|
||||
|
||||
return { loading, installed: true, settings: value };
|
||||
if (value) {
|
||||
return { loading, installed: value.enabled ?? false, settings: value };
|
||||
}
|
||||
|
||||
return { loading, installed: false };
|
||||
}
|
||||
|
||||
type FallbackPlugin = SupportedPlugin.OnCall | SupportedPlugin.Incident;
|
||||
type IrmWithFallback = SupportedPlugin.Irm | FallbackPlugin;
|
||||
|
||||
export interface PluginBridgeResult {
|
||||
pluginId: IrmWithFallback;
|
||||
loading: boolean;
|
||||
installed?: boolean;
|
||||
error?: Error;
|
||||
settings?: PluginMeta<{}>;
|
||||
}
|
||||
/**
|
||||
* Hook that checks for IRM plugin first, falls back to specified plugin.
|
||||
* IRM replaced both OnCall and Incident - this provides backward compatibility.
|
||||
*
|
||||
* @param fallback - The plugin to use if IRM is not installed (OnCall or Incident)
|
||||
* @returns Bridge result with the active plugin data
|
||||
*
|
||||
* @example
|
||||
* const { pluginId, loading, installed, settings } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
*/
|
||||
export function useIrmPlugin(fallback: FallbackPlugin): PluginBridgeResult {
|
||||
const irmBridge = usePluginBridge(SupportedPlugin.Irm);
|
||||
const fallbackBridge = usePluginBridge(fallback);
|
||||
|
||||
const loading = irmBridge.loading || fallbackBridge.loading;
|
||||
const pluginId = irmBridge.installed ? SupportedPlugin.Irm : fallback;
|
||||
const activeBridge = irmBridge.installed ? irmBridge : fallbackBridge;
|
||||
|
||||
return {
|
||||
pluginId,
|
||||
loading,
|
||||
installed: activeBridge.installed,
|
||||
error: activeBridge.error,
|
||||
settings: activeBridge.settings,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { type DefaultBodyType, HttpResponse, HttpResponseResolver, PathParams, http } from 'msw';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import server from '@grafana/test-utils/server';
|
||||
import { mockDataSource, mockFolder } from 'app/features/alerting/unified/mocks';
|
||||
import {
|
||||
@@ -10,10 +9,7 @@ import {
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/alertmanagers';
|
||||
import { getFolderHandler } from 'app/features/alerting/unified/mocks/server/handlers/folders';
|
||||
import { listNamespacedTimeIntervalHandler } from 'app/features/alerting/unified/mocks/server/handlers/k8s/timeIntervals.k8s';
|
||||
import {
|
||||
getDisabledPluginHandler,
|
||||
getPluginMissingHandler,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/plugins';
|
||||
import { getDisabledPluginHandler } from 'app/features/alerting/unified/mocks/server/handlers/plugins';
|
||||
import {
|
||||
ALERTING_API_SERVER_BASE_URL,
|
||||
getK8sResponse,
|
||||
@@ -212,12 +208,6 @@ export function setGrafanaPromRules(groups: GrafanaPromRuleGroupDTO[]) {
|
||||
server.use(http.get(`/api/prometheus/grafana/api/v1/rules`, paginatedHandlerFor(groups)));
|
||||
}
|
||||
|
||||
/** Make a given plugin ID respond with a 404, as if it isn't installed at all */
|
||||
export const removePlugin = (pluginId: string) => {
|
||||
delete config.apps[pluginId];
|
||||
server.use(getPluginMissingHandler(pluginId));
|
||||
};
|
||||
|
||||
/** Make a plugin respond with `enabled: false`, as if its installed but disabled */
|
||||
export const disablePlugin = (pluginId: SupportedPlugin) => {
|
||||
clearPluginSettingsCache(pluginId);
|
||||
|
||||
@@ -1,14 +1,6 @@
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { pluginMeta, pluginMetaToPluginConfig } from '../testSetup/plugins';
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import {
|
||||
checkEvaluationIntervalGlobalLimit,
|
||||
getIrmIfPresentOrIncidentPluginId,
|
||||
getIrmIfPresentOrOnCallPluginId,
|
||||
getIsIrmPluginPresent,
|
||||
} from './config';
|
||||
import { checkEvaluationIntervalGlobalLimit } from './config';
|
||||
|
||||
describe('checkEvaluationIntervalGlobalLimit', () => {
|
||||
it('should NOT exceed limit if evaluate every is not valid duration', () => {
|
||||
@@ -59,48 +51,3 @@ describe('checkEvaluationIntervalGlobalLimit', () => {
|
||||
expect(exceedsLimit).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIsIrmPluginPresent', () => {
|
||||
it('should return true when IRM plugin is present in config.apps', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIsIrmPluginPresent()).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false when IRM plugin is not present in config.apps', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIsIrmPluginPresent()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIrmIfPresentOrIncidentPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return Incident plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIrmIfPresentOrIncidentPluginId()).toBe(SupportedPlugin.Incident);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIrmIfPresentOrOnCallPluginId', () => {
|
||||
it('should return IRM plugin ID when IRM plugin is present', () => {
|
||||
config.apps = { [SupportedPlugin.Irm]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Irm]) };
|
||||
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.Irm);
|
||||
});
|
||||
|
||||
it('should return OnCall plugin ID when IRM plugin is not present', () => {
|
||||
config.apps = {
|
||||
[SupportedPlugin.OnCall]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.OnCall]),
|
||||
[SupportedPlugin.Incident]: pluginMetaToPluginConfig(pluginMeta[SupportedPlugin.Incident]),
|
||||
};
|
||||
expect(getIrmIfPresentOrOnCallPluginId()).toBe(SupportedPlugin.OnCall);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { DataSourceInstanceSettings, DataSourceJsonData } from '@grafana/data';
|
||||
import { config } from '@grafana/runtime';
|
||||
|
||||
import { SupportedPlugin } from '../types/pluginBridges';
|
||||
|
||||
import { isValidPrometheusDuration, safeParsePrometheusDuration } from './time';
|
||||
|
||||
export function getAllDataSources(): Array<DataSourceInstanceSettings<DataSourceJsonData>> {
|
||||
@@ -28,15 +26,3 @@ export function checkEvaluationIntervalGlobalLimit(alertGroupEvaluateEvery?: str
|
||||
|
||||
return { globalLimit: evaluateEveryGlobalLimitMs, exceedsLimit };
|
||||
}
|
||||
|
||||
export function getIsIrmPluginPresent() {
|
||||
return SupportedPlugin.Irm in config.apps;
|
||||
}
|
||||
|
||||
export function getIrmIfPresentOrIncidentPluginId() {
|
||||
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.Incident;
|
||||
}
|
||||
|
||||
export function getIrmIfPresentOrOnCallPluginId() {
|
||||
return getIsIrmPluginPresent() ? SupportedPlugin.Irm : SupportedPlugin.OnCall;
|
||||
}
|
||||
|
||||
@@ -95,7 +95,7 @@ export const ImportDashboardFormV2 = ({
|
||||
|
||||
return (
|
||||
<Field
|
||||
label={input.name}
|
||||
label={input.pluginId}
|
||||
description={input.description}
|
||||
key={input.pluginId}
|
||||
invalid={!!errors[dataSourceOption]}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { incidentsApi } from 'app/features/alerting/unified/api/incidentsApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrIncidentPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
import { useIrmPlugin } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
interface IncidentsPluginConfig {
|
||||
isInstalled: boolean;
|
||||
@@ -10,11 +10,13 @@ interface IncidentsPluginConfig {
|
||||
}
|
||||
|
||||
export function useGetIncidentPluginConfig(): IncidentsPluginConfig {
|
||||
const { installed: incidentPluginInstalled, loading: loadingPluginSettings } = usePluginBridge(
|
||||
getIrmIfPresentOrIncidentPluginId()
|
||||
);
|
||||
const {
|
||||
pluginId,
|
||||
installed: incidentPluginInstalled,
|
||||
loading: loadingPluginSettings,
|
||||
} = useIrmPlugin(SupportedPlugin.Incident);
|
||||
const { data: incidentsConfig, isLoading: loadingPluginConfig } =
|
||||
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery();
|
||||
incidentsApi.endpoints.getIncidentsPluginConfig.useQuery({ pluginId });
|
||||
|
||||
return {
|
||||
isInstalled: incidentPluginInstalled ?? false,
|
||||
|
||||
@@ -1,26 +1,34 @@
|
||||
import { onCallApi } from 'app/features/alerting/unified/api/onCallApi';
|
||||
import { usePluginBridge } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { getIrmIfPresentOrOnCallPluginId } from 'app/features/alerting/unified/utils/config';
|
||||
import { useIrmPlugin } from 'app/features/alerting/unified/hooks/usePluginBridge';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
|
||||
export function useGetOnCallIntegrations() {
|
||||
const { installed: onCallPluginInstalled } = usePluginBridge(getIrmIfPresentOrOnCallPluginId());
|
||||
const { pluginId, installed: onCallPluginInstalled } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(undefined, {
|
||||
skip: !onCallPluginInstalled,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
const { data: onCallIntegrations } = onCallApi.endpoints.grafanaOnCallIntegrations.useQuery(
|
||||
{ pluginId },
|
||||
{
|
||||
skip: !onCallPluginInstalled,
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
return onCallIntegrations ?? [];
|
||||
}
|
||||
|
||||
function useGetOnCallConfigurationChecks() {
|
||||
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(undefined, {
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
});
|
||||
const { pluginId } = useIrmPlugin(SupportedPlugin.OnCall);
|
||||
|
||||
const { data: onCallConfigChecks, isLoading } = onCallApi.endpoints.onCallConfigChecks.useQuery(
|
||||
{ pluginId },
|
||||
{
|
||||
refetchOnFocus: true,
|
||||
refetchOnReconnect: true,
|
||||
refetchOnMountOrArgChange: true,
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading,
|
||||
|
||||
@@ -121,7 +121,7 @@ export const ImportDashboardForm = ({
|
||||
const current = watchDataSources ?? [];
|
||||
return (
|
||||
<Field
|
||||
label={input.name}
|
||||
label={input.pluginId}
|
||||
description={input.description}
|
||||
key={dataSourceOption}
|
||||
invalid={errors.dataSources && !!errors.dataSources[index]}
|
||||
|
||||
@@ -141,7 +141,6 @@ export const getPluginExtensions: GetExtensions = ({
|
||||
description: overrides?.description || addedLink.description || '',
|
||||
path: isString(path) ? getLinkExtensionPathWithTracking(pluginId, path, extensionPointId) : undefined,
|
||||
category: overrides?.category || addedLink.category,
|
||||
openInNewTab: overrides?.openInNewTab ?? addedLink.openInNewTab,
|
||||
};
|
||||
|
||||
extensions.push(extension);
|
||||
|
||||
@@ -420,7 +420,6 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
iconClassName: extension.icon,
|
||||
target: extension.openInNewTab ? '_blank' : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -434,7 +433,6 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
iconClassName: extension.icon,
|
||||
target: extension.openInNewTab ? '_blank' : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"type": "panel",
|
||||
"name": "Datagrid",
|
||||
"id": "datagrid",
|
||||
"state": "deprecated",
|
||||
"state": "beta",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
|
||||
22
yarn.lock
22
yarn.lock
@@ -3789,11 +3789,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes-react@npm:6.52.2"
|
||||
"@grafana/scenes-react@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes-react@npm:6.52.1"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
"@grafana/scenes": "npm:6.52.1"
|
||||
lru-cache: "npm:^10.2.2"
|
||||
react-use: "npm:^17.4.0"
|
||||
peerDependencies:
|
||||
@@ -3805,7 +3805,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/c393faf6612e78254dab79b15cc970448d74ba9784ccda623953c5dbc21d91a8da94b7ad7d0d294eac51314cc193c419a7cb48295fd50b1f9c4472699669eb3e
|
||||
checksum: 10/2f7c6ca8e26befd331808afb0cb934e2991e889a4de78be1122c536219676261c59c6204510761a1d4250fd44a3767818f0f225d23b2e7243cfc17baf8ca6ca3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3835,9 +3835,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes@npm:6.52.2"
|
||||
"@grafana/scenes@npm:6.52.1, @grafana/scenes@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes@npm:6.52.1"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:^0.26.16"
|
||||
"@leeoniya/ufuzzy": "npm:^1.0.16"
|
||||
@@ -3857,7 +3857,7 @@ __metadata:
|
||||
react: ^18.0.0
|
||||
react-dom: ^18.0.0
|
||||
react-router-dom: ^6.28.0
|
||||
checksum: 10/f6dbe20db78bb1aa09cc38025534917887713d73119a172febb44700837ed859363ee0436b5f4bda6bc063f9432115e32519ab4c8da7834cf1fc22d43fea7711
|
||||
checksum: 10/d6172b51121e03c7dcbf30046772f99fc45922c1f7b360a7c3d2c0391300e378f306cb78251dda3b30895679379c38db30e4d52fee67a56cd95f18f38aadf3fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -19790,8 +19790,8 @@ __metadata:
|
||||
"@grafana/plugin-ui": "npm:^0.11.1"
|
||||
"@grafana/prometheus": "workspace:*"
|
||||
"@grafana/runtime": "workspace:*"
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
"@grafana/scenes-react": "npm:6.52.2"
|
||||
"@grafana/scenes": "npm:v6.52.1"
|
||||
"@grafana/scenes-react": "npm:v6.52.1"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user