Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 32975dd0a8 | |||
| f7b7c53a09 | |||
| 170ac31c5a | |||
| 0d1e0bc21c | |||
| afd84f0335 | |||
| d680537ea1 | |||
| 78d507d285 | |||
| 9d1d0e72c2 |
@@ -28,7 +28,7 @@ type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginContextProvider PluginContextProvider
|
||||
PluginClient plugins.Client
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
GrafanaVersion string
|
||||
pluginCanBeInstalledCache map[string]bool
|
||||
pluginExistsCacheMu sync.RWMutex
|
||||
@@ -39,7 +39,7 @@ func New(
|
||||
pluginStore pluginstore.Store,
|
||||
pluginContextProvider PluginContextProvider,
|
||||
pluginClient plugins.Client,
|
||||
pluginRepo repo.Service,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
grafanaVersion string,
|
||||
) checks.Check {
|
||||
return &check{
|
||||
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
type missingPluginStep struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
GrafanaVersion string
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ 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
|
||||
@@ -37,3 +38,10 @@ 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 repo.Service,
|
||||
pluginRepo checks.PluginInfoGetter,
|
||||
updateChecker pluginchecker.PluginUpdateChecker,
|
||||
pluginErrorResolver plugins.ErrorResolver,
|
||||
grafanaVersion string,
|
||||
@@ -33,7 +33,7 @@ func New(
|
||||
|
||||
type check struct {
|
||||
PluginStore pluginstore.Store
|
||||
PluginRepo repo.Service
|
||||
PluginRepo checks.PluginInfoGetter
|
||||
updateChecker pluginchecker.PluginUpdateChecker
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
GrafanaVersion string
|
||||
|
||||
@@ -83,6 +83,7 @@ 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,7 +30,9 @@ refs:
|
||||
|
||||
# Datagrid
|
||||
|
||||
{{< docs/experimental product="The datagrid visualization" featureFlag="`enableDatagridEditing`" >}}
|
||||
{{< admonition type="caution" >}}
|
||||
Starting with Grafana 12.4, Datagrid is deprecated. It will be removed in version 13.0.
|
||||
{{< /admonition >}}
|
||||
|
||||
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.
|
||||
|
||||
+2
-2
@@ -293,8 +293,8 @@
|
||||
"@grafana/plugin-ui": "^0.11.1",
|
||||
"@grafana/prometheus": "workspace:*",
|
||||
"@grafana/runtime": "workspace:*",
|
||||
"@grafana/scenes": "v6.52.1",
|
||||
"@grafana/scenes-react": "v6.52.1",
|
||||
"@grafana/scenes": "6.52.2",
|
||||
"@grafana/scenes-react": "6.52.2",
|
||||
"@grafana/schema": "workspace:*",
|
||||
"@grafana/sql": "workspace:*",
|
||||
"@grafana/ui": "workspace:*",
|
||||
|
||||
@@ -844,7 +844,6 @@ export {
|
||||
DataLinkConfigOrigin,
|
||||
SupportedTransformationType,
|
||||
type InternalDataLink,
|
||||
type LinkTarget,
|
||||
type LinkModel,
|
||||
type LinkModelSupplier,
|
||||
VariableOrigin,
|
||||
@@ -852,6 +851,7 @@ export {
|
||||
VariableSuggestionsScope,
|
||||
OneClickMode,
|
||||
} from './types/dataLink';
|
||||
export { type LinkTarget } from './types/linkTarget';
|
||||
export {
|
||||
type Action,
|
||||
type ActionModel,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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';
|
||||
@@ -88,8 +89,6 @@ export interface InternalDataLink<T extends DataQuery = any> {
|
||||
range?: TimeRange;
|
||||
}
|
||||
|
||||
export type LinkTarget = '_blank' | '_self' | undefined;
|
||||
|
||||
/**
|
||||
* Processed Link Model. The values are ready to use
|
||||
*/
|
||||
|
||||
+9
-1
@@ -356,7 +356,7 @@ export interface FeatureToggles {
|
||||
*/
|
||||
dashboardScene?: boolean;
|
||||
/**
|
||||
* Enables experimental new dashboard layouts
|
||||
* Enables new dashboard layouts
|
||||
*/
|
||||
dashboardNewLayouts?: boolean;
|
||||
/**
|
||||
@@ -531,6 +531,10 @@ export interface FeatureToggles {
|
||||
*/
|
||||
alertingListViewV2?: boolean;
|
||||
/**
|
||||
* Enables the new Alerting navigation structure with improved menu grouping
|
||||
*/
|
||||
alertingNavigationV2?: boolean;
|
||||
/**
|
||||
* Enables saved searches for alert rules list
|
||||
*/
|
||||
alertingSavedSearches?: boolean;
|
||||
@@ -1251,4 +1255,8 @@ 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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* 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,6 +11,7 @@ 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';
|
||||
@@ -191,6 +192,7 @@ 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 experimental new dashboard layouts",
|
||||
Stage: FeatureStageExperimental,
|
||||
Description: "Enables new dashboard layouts",
|
||||
Stage: FeatureStagePublicPreview,
|
||||
FrontendOnly: false, // The restore backend feature changes behavior based on this flag
|
||||
Owner: grafanaDashboardsSquad,
|
||||
},
|
||||
@@ -879,6 +879,13 @@ var (
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "alertingNavigationV2",
|
||||
Description: "Enables the new Alerting navigation structure with improved menu grouping",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaAlertingSquad,
|
||||
FrontendOnly: false,
|
||||
},
|
||||
{
|
||||
Name: "alertingSavedSearches",
|
||||
Description: "Enables saved searches for alert rules list",
|
||||
@@ -981,7 +988,8 @@ var (
|
||||
Stage: FeatureStageDeprecated,
|
||||
Owner: grafanaPartnerPluginsSquad,
|
||||
Expression: "true", // Enabled by default for now
|
||||
}, {
|
||||
},
|
||||
{
|
||||
Name: "alertingFilterV2",
|
||||
Description: "Enable the new alerting search experience",
|
||||
Stage: FeatureStageExperimental,
|
||||
@@ -2069,6 +2077,14 @@ 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,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Generated
+3
-1
@@ -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,experimental,@grafana/dashboards-squad,false,false,false
|
||||
dashboardNewLayouts,preview,@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
|
||||
@@ -121,6 +121,7 @@ dashboardLibrary,experimental,@grafana/sharing-squad,false,false,false
|
||||
suggestedDashboards,experimental,@grafana/sharing-squad,false,false,false
|
||||
dashboardTemplates,preview,@grafana/sharing-squad,false,false,false
|
||||
alertingListViewV2,privatePreview,@grafana/alerting-squad,false,false,true
|
||||
alertingNavigationV2,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertingSavedSearches,experimental,@grafana/alerting-squad,false,false,true
|
||||
alertingDisableSendAlertsExternal,experimental,@grafana/alerting-squad,false,false,false
|
||||
preserveDashboardStateWhenNavigating,experimental,@grafana/dashboards-squad,false,false,false
|
||||
@@ -280,3 +281,4 @@ 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
|
||||
|
||||
|
Generated
+9
-1
@@ -260,7 +260,7 @@ const (
|
||||
FlagAnnotationPermissionUpdate = "annotationPermissionUpdate"
|
||||
|
||||
// FlagDashboardNewLayouts
|
||||
// Enables experimental new dashboard layouts
|
||||
// Enables new dashboard layouts
|
||||
FlagDashboardNewLayouts = "dashboardNewLayouts"
|
||||
|
||||
// FlagPdfTables
|
||||
@@ -371,6 +371,10 @@ const (
|
||||
// Enables a flow to get started with a new dashboard from a template
|
||||
FlagDashboardTemplates = "dashboardTemplates"
|
||||
|
||||
// FlagAlertingNavigationV2
|
||||
// Enables the new Alerting navigation structure with improved menu grouping
|
||||
FlagAlertingNavigationV2 = "alertingNavigationV2"
|
||||
|
||||
// FlagAlertingDisableSendAlertsExternal
|
||||
// Disables the ability to send alerts to an external Alertmanager datasource.
|
||||
FlagAlertingDisableSendAlertsExternal = "alertingDisableSendAlertsExternal"
|
||||
@@ -789,4 +793,8 @@ 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"
|
||||
)
|
||||
|
||||
+35
-5
@@ -348,6 +348,18 @@
|
||||
"expression": "true"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNavigationV2",
|
||||
"resourceVersion": "1768320918269",
|
||||
"creationTimestamp": "2026-01-13T16:15:18Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables the new Alerting navigation structure with improved menu grouping",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/alerting-squad"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "alertingNotificationHistory",
|
||||
@@ -511,6 +523,20 @@
|
||||
"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",
|
||||
@@ -662,7 +688,8 @@
|
||||
"metadata": {
|
||||
"name": "auditLoggingAppPlatform",
|
||||
"resourceVersion": "1767013056996",
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z"
|
||||
"creationTimestamp": "2025-12-29T12:57:36Z",
|
||||
"deletionTimestamp": "2026-01-06T09:18:36Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enable audit logging with Kubernetes under app platform",
|
||||
@@ -1015,12 +1042,15 @@
|
||||
{
|
||||
"metadata": {
|
||||
"name": "dashboardNewLayouts",
|
||||
"resourceVersion": "1764664939750",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z"
|
||||
"resourceVersion": "1768382835527",
|
||||
"creationTimestamp": "2024-10-23T08:55:45Z",
|
||||
"annotations": {
|
||||
"grafana.app/updatedTimestamp": "2026-01-14 09:27:15.527103 +0000 UTC"
|
||||
}
|
||||
},
|
||||
"spec": {
|
||||
"description": "Enables experimental new dashboard layouts",
|
||||
"stage": "experimental",
|
||||
"description": "Enables new dashboard layouts",
|
||||
"stage": "preview",
|
||||
"codeowner": "@grafana/dashboards-squad"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -446,9 +446,32 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
}
|
||||
|
||||
if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionAlertingRuleRead), ac.EvalPermission(ac.ActionAlertingRuleExternalRead))) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
|
||||
})
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingNavigationV2) {
|
||||
// New navigation: Alert rules parent (tabs managed on frontend)
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-rules", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
|
||||
})
|
||||
|
||||
// Add child nav items for permission checking (tabs will be rendered by frontend)
|
||||
// Alert rules list tab
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules", Id: "alert-rules-list", Url: s.cfg.AppSubURL + "/alerting/list",
|
||||
})
|
||||
|
||||
// Recently deleted tab (check additional feature flags)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted", Id: "alert-rules-recently-deleted", Url: s.cfg.AppSubURL + "/alerting/recently-deleted",
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// Legacy navigation
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Alert rules", SubTitle: "Rules that determine whether an alert will fire", Id: "alert-list", Url: s.cfg.AppSubURL + "/alerting/list", Icon: "list-ul",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
contactPointsPerms := []ac.Evaluator{
|
||||
@@ -508,12 +531,16 @@ func (s *ServiceImpl) buildAlertNavLinks(c *contextmodel.ReqContext) *navtree.Na
|
||||
}
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if c.GetOrgRole() == org.RoleAdmin && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertRuleRestore) && s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingRuleRecoverDeleted) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
|
||||
Id: "alerts/recently-deleted",
|
||||
Url: s.cfg.AppSubURL + "/alerting/recently-deleted",
|
||||
})
|
||||
// Only show as standalone item in legacy navigation (V2 shows it as a tab under Alert rules)
|
||||
//nolint:staticcheck // not yet migrated to OpenFeature
|
||||
if !s.features.IsEnabled(c.Req.Context(), featuremgmt.FlagAlertingNavigationV2) {
|
||||
alertChildNavs = append(alertChildNavs, &navtree.NavLink{
|
||||
Text: "Recently deleted",
|
||||
SubTitle: "Any items listed here for more than 30 days will be automatically deleted.",
|
||||
Id: "alerts/recently-deleted",
|
||||
Url: s.cfg.AppSubURL + "/alerting/recently-deleted",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if c.GetOrgRole() == org.RoleAdmin {
|
||||
|
||||
@@ -213,6 +213,9 @@ 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,
|
||||
@@ -222,6 +225,7 @@ 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,6 +33,9 @@ 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 {
|
||||
@@ -44,8 +47,10 @@ 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 {
|
||||
@@ -101,6 +106,10 @@ 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,
|
||||
@@ -123,12 +132,29 @@ 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,
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
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,6 +15,7 @@ const (
|
||||
KVNamespace = "alertmanager"
|
||||
NotificationLogFilename = "notifications"
|
||||
SilencesFilename = "silences"
|
||||
FlushLogFilename = "flushes"
|
||||
)
|
||||
|
||||
// FileStore is in charge of persisting the alertmanager files to the database.
|
||||
@@ -42,6 +43,10 @@ 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.
|
||||
@@ -74,6 +79,11 @@ 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,3 +106,48 @@ 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,6 +82,7 @@ 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.
|
||||
@@ -378,7 +379,7 @@ func (moa *MultiOrgAlertmanager) SyncAlertmanagersForOrgs(ctx context.Context, o
|
||||
func (moa *MultiOrgAlertmanager) cleanupOrphanLocalOrgState(ctx context.Context,
|
||||
activeOrganizations map[int64]struct{},
|
||||
) {
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename}
|
||||
storedFiles := []string{NotificationLogFilename, SilencesFilename, FlushLogFilename}
|
||||
for _, fileName := range storedFiles {
|
||||
keys, err := moa.kvStore.Keys(ctx, kvstore.AllOrganizations, KVNamespace, fileName)
|
||||
if err != nil {
|
||||
|
||||
@@ -5,5 +5,8 @@ func (am *alertmanager) MergeState(state ExternalState) error {
|
||||
if err := am.Base.MergeNflog(state.Nflog); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.Base.MergeSilences(state.Silences)
|
||||
if err := am.Base.MergeSilences(state.Silences); err != nil {
|
||||
return err
|
||||
}
|
||||
return am.Base.MergeFlushLog(state.FlushLog)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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"
|
||||
@@ -228,15 +229,13 @@ 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}}
|
||||
@@ -365,6 +364,51 @@ 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,6 +47,7 @@ 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.
|
||||
@@ -86,6 +87,8 @@ type Alertmanager struct {
|
||||
|
||||
promoteConfig bool
|
||||
externalURL string
|
||||
|
||||
runtimeConfig remoteClient.RuntimeConfig
|
||||
}
|
||||
|
||||
type AlertmanagerConfig struct {
|
||||
@@ -111,6 +114,9 @@ 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 {
|
||||
@@ -203,6 +209,7 @@ 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.
|
||||
@@ -331,10 +338,11 @@ 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,
|
||||
CreatedAt: createdAtEpoch,
|
||||
Promoted: am.promoteConfig,
|
||||
ExternalURL: am.externalURL,
|
||||
SmtpConfig: am.smtp,
|
||||
RuntimeConfig: am.runtimeConfig,
|
||||
}
|
||||
|
||||
cfgHash, err := calculateUserGrafanaConfigHash(payload)
|
||||
@@ -388,6 +396,8 @@ 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)
|
||||
}
|
||||
@@ -677,6 +687,12 @@ 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,6 +29,10 @@ 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"`
|
||||
@@ -37,6 +41,7 @@ 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) {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { useLocation } from 'react-router-dom-v5-compat';
|
||||
|
||||
import { NavModelItem } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { useSelector } from 'app/types/store';
|
||||
|
||||
export function useAlertRulesNav() {
|
||||
const location = useLocation();
|
||||
const navIndex = useSelector((state) => state.navIndex);
|
||||
|
||||
// Check if V2 navigation is enabled
|
||||
const useV2Nav = config.featureToggles.alertingNavigationV2;
|
||||
|
||||
if (!useV2Nav) {
|
||||
// Legacy navigation: return simple navId
|
||||
return {
|
||||
navId: 'alert-list',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// V2 Navigation: Create tabs structure
|
||||
const alertRulesNav = navIndex['alert-rules'];
|
||||
|
||||
if (!alertRulesNav) {
|
||||
// Fallback to legacy if nav item doesn't exist
|
||||
return {
|
||||
navId: 'alert-list',
|
||||
pageNav: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// All available tabs
|
||||
const allTabs = [
|
||||
{
|
||||
id: 'alert-rules-list',
|
||||
text: t('alerting.navigation.alert-rules', 'Alert rules'),
|
||||
url: '/alerting/list',
|
||||
active: location.pathname === '/alerting/list',
|
||||
icon: 'list-ul',
|
||||
parentItem: alertRulesNav,
|
||||
},
|
||||
{
|
||||
id: 'alert-rules-recently-deleted',
|
||||
text: t('alerting.navigation.recently-deleted', 'Recently deleted'),
|
||||
url: '/alerting/recently-deleted',
|
||||
active: location.pathname === '/alerting/recently-deleted',
|
||||
icon: 'history',
|
||||
parentItem: alertRulesNav,
|
||||
},
|
||||
].filter((tab) => {
|
||||
// Filter based on permissions - if nav item doesn't exist, user doesn't have permission
|
||||
const navItem = navIndex[tab.id];
|
||||
return navItem !== undefined;
|
||||
});
|
||||
|
||||
// Create pageNav that represents the Alert rules page with tabs as children
|
||||
const pageNav: NavModelItem = {
|
||||
...alertRulesNav,
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
|
||||
children: allTabs as NavModelItem[],
|
||||
};
|
||||
|
||||
return {
|
||||
navId: 'alert-rules',
|
||||
pageNav,
|
||||
};
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import { shouldUsePrometheusRulesPrimary } from '../featureToggles';
|
||||
import { useCombinedRuleNamespaces } from '../hooks/useCombinedRuleNamespaces';
|
||||
import { useFilteredRules, useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { useUnifiedAlertingSelector } from '../hooks/useUnifiedAlertingSelector';
|
||||
import { useAlertRulesNav } from '../navigation/useAlertRulesNav';
|
||||
import { fetchAllPromAndRulerRulesAction, fetchAllPromRulesAction, fetchRulerRulesAction } from '../state/actions';
|
||||
import { RULE_LIST_POLL_INTERVAL_MS } from '../utils/constants';
|
||||
import { GRAFANA_RULES_SOURCE_NAME, getAllRulesSourceNames } from '../utils/datasource';
|
||||
@@ -38,6 +39,7 @@ const LIMIT_ALERTS = INSTANCES_DISPLAY_LIMIT + 1;
|
||||
const prometheusRulesPrimary = shouldUsePrometheusRulesPrimary();
|
||||
|
||||
const RuleListV1 = () => {
|
||||
const { navId, pageNav } = useAlertRulesNav();
|
||||
const dispatch = useDispatch();
|
||||
const rulesDataSourceNames = useMemo(getAllRulesSourceNames, []);
|
||||
const [expandAll, setExpandAll] = useState(false);
|
||||
@@ -119,7 +121,8 @@ const RuleListV1 = () => {
|
||||
// We don't want to show the Loading... indicator for the whole page.
|
||||
// We show separate indicators for Grafana-managed and Cloud rules
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
navId={navId}
|
||||
pageNav={pageNav}
|
||||
isLoading={false}
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
actions={<RuleListActionButtons hasAlertRulesCreated={hasAlertRulesCreated} />}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { useListViewMode } from '../components/rules/Filter/RulesViewModeSelecto
|
||||
import { AIAlertRuleButtonComponent } from '../enterprise-components/AI/AIGenAlertRuleButton/addAIAlertRuleButton';
|
||||
import { AlertingAction, useAlertingAbility } from '../hooks/useAbilities';
|
||||
import { useRulesFilter } from '../hooks/useFilteredRules';
|
||||
import { useAlertRulesNav } from '../navigation/useAlertRulesNav';
|
||||
import { getRulesDataSources } from '../utils/datasource';
|
||||
|
||||
import { FilterView } from './FilterView';
|
||||
@@ -123,10 +124,12 @@ export function RuleListActions() {
|
||||
|
||||
export default function RuleListPage() {
|
||||
const { isApplying } = useApplyDefaultSearch();
|
||||
const { navId, pageNav } = useAlertRulesNav();
|
||||
|
||||
return (
|
||||
<AlertingPageWrapper
|
||||
navId="alert-list"
|
||||
navId={navId}
|
||||
pageNav={pageNav}
|
||||
renderTitle={(title) => <RuleListPageTitle title={title} />}
|
||||
isLoading={isApplying}
|
||||
actions={<RuleListActions />}
|
||||
|
||||
@@ -141,6 +141,7 @@ 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,6 +420,7 @@ export function createExtensionSubMenu(extensions: PluginExtensionLink[]): Panel
|
||||
href: extension.path,
|
||||
onClick: extension.onClick,
|
||||
iconClassName: extension.icon,
|
||||
target: extension.openInNewTab ? '_blank' : undefined,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -433,6 +434,7 @@ 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": "beta",
|
||||
"state": "deprecated",
|
||||
|
||||
"info": {
|
||||
"author": {
|
||||
|
||||
@@ -1929,6 +1929,10 @@
|
||||
"select-group": "Select group",
|
||||
"select-namespace": "Select namespace"
|
||||
},
|
||||
"navigation": {
|
||||
"alert-rules": "Alert rules",
|
||||
"recently-deleted": "Recently deleted"
|
||||
},
|
||||
"need-help-info": {
|
||||
"need-help": "Need help?"
|
||||
},
|
||||
|
||||
@@ -3789,11 +3789,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@grafana/scenes-react@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes-react@npm:6.52.1"
|
||||
"@grafana/scenes-react@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes-react@npm:6.52.2"
|
||||
dependencies:
|
||||
"@grafana/scenes": "npm:6.52.1"
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
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/2f7c6ca8e26befd331808afb0cb934e2991e889a4de78be1122c536219676261c59c6204510761a1d4250fd44a3767818f0f225d23b2e7243cfc17baf8ca6ca3
|
||||
checksum: 10/c393faf6612e78254dab79b15cc970448d74ba9784ccda623953c5dbc21d91a8da94b7ad7d0d294eac51314cc193c419a7cb48295fd50b1f9c4472699669eb3e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3835,9 +3835,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@grafana/scenes@npm:6.52.1, @grafana/scenes@npm:v6.52.1":
|
||||
version: 6.52.1
|
||||
resolution: "@grafana/scenes@npm:6.52.1"
|
||||
"@grafana/scenes@npm:6.52.2":
|
||||
version: 6.52.2
|
||||
resolution: "@grafana/scenes@npm:6.52.2"
|
||||
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/d6172b51121e03c7dcbf30046772f99fc45922c1f7b360a7c3d2c0391300e378f306cb78251dda3b30895679379c38db30e4d52fee67a56cd95f18f38aadf3fb
|
||||
checksum: 10/f6dbe20db78bb1aa09cc38025534917887713d73119a172febb44700837ed859363ee0436b5f4bda6bc063f9432115e32519ab4c8da7834cf1fc22d43fea7711
|
||||
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:v6.52.1"
|
||||
"@grafana/scenes-react": "npm:v6.52.1"
|
||||
"@grafana/scenes": "npm:6.52.2"
|
||||
"@grafana/scenes-react": "npm:6.52.2"
|
||||
"@grafana/schema": "workspace:*"
|
||||
"@grafana/sql": "workspace:*"
|
||||
"@grafana/test-utils": "workspace:*"
|
||||
|
||||
Reference in New Issue
Block a user