Provisioning: Ignore dashboard change warning after save (#115401)
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"k8s.io/apiserver/pkg/registry/rest"
|
||||
|
||||
"github.com/grafana/grafana-app-sdk/logging"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
grafanarest "github.com/grafana/grafana/pkg/apiserver/rest"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
"github.com/grafana/grafana/pkg/services/apiserver/endpoints/request"
|
||||
@@ -33,8 +34,11 @@ func (d dashboardStorageWrapper) Update(ctx context.Context, name string, objInf
|
||||
|
||||
obj, created, err := d.Storage.Update(ctx, name, objInfo, createValidation, updateValidation, forceAllowCreate, options)
|
||||
if err == nil && ns.OrgID > 0 && d.live != nil {
|
||||
if err := d.live.DashboardSaved(ns.OrgID, name); err != nil {
|
||||
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
|
||||
m, err := utils.MetaAccessor(obj)
|
||||
if err == nil {
|
||||
if err := d.live.DashboardSaved(ns.OrgID, name, m.GetResourceVersion()); err != nil {
|
||||
logging.FromContext(ctx).Info("live dashboard update failed", "err", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
return obj, created, err
|
||||
|
||||
@@ -277,6 +277,16 @@ func (r *DualReadWriter) createOrUpdate(ctx context.Context, create bool, opts D
|
||||
// FIXME: to make sure if behaves in the same way as in sync, we should
|
||||
// we should refactor the code to use the same function.
|
||||
if r.shouldUpdateGrafanaDB(opts, parsed) {
|
||||
// HACK: Get the has from repository -- this will avoid an additional RV increment
|
||||
// we should change the signature of Create and Update to return FileInfo instead
|
||||
info, _ = r.repo.Read(ctx, opts.Path, opts.Ref)
|
||||
if info != nil {
|
||||
parsed.Meta.SetSourceProperties(utils.SourceProperties{
|
||||
Path: opts.Path,
|
||||
Checksum: info.Hash,
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := r.folders.EnsureFolderPathExist(ctx, opts.Path); err != nil {
|
||||
return nil, fmt.Errorf("ensure folder path exists: %w", err)
|
||||
}
|
||||
|
||||
@@ -26,9 +26,10 @@ const (
|
||||
|
||||
// DashboardEvent events related to dashboards
|
||||
type dashboardEvent struct {
|
||||
UID string `json:"uid"`
|
||||
Action actionType `json:"action"` // saved, editing, deleted
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
UID string `json:"uid"`
|
||||
Action actionType `json:"action"` // saved, editing, deleted
|
||||
SessionID string `json:"sessionId,omitempty"`
|
||||
ResourceVersion string `json:"rv,omitempty"`
|
||||
}
|
||||
|
||||
// DashboardHandler manages all the `grafana/dashboard/*` channels
|
||||
@@ -105,10 +106,11 @@ func (h *DashboardHandler) publish(orgID int64, event dashboardEvent) error {
|
||||
}
|
||||
|
||||
// DashboardSaved will broadcast to all connected dashboards
|
||||
func (h *DashboardHandler) DashboardSaved(orgID int64, uid string) error {
|
||||
func (h *DashboardHandler) DashboardSaved(orgID int64, uid string, rv string) error {
|
||||
return h.publish(orgID, dashboardEvent{
|
||||
UID: uid,
|
||||
Action: ActionSaved,
|
||||
UID: uid,
|
||||
Action: ActionSaved,
|
||||
ResourceVersion: rv,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -482,7 +482,7 @@ type GrafanaLive struct {
|
||||
// DashboardActivityChannel is a service to advertise dashboard activity
|
||||
type DashboardActivityChannel interface {
|
||||
// Called when a dashboard is saved
|
||||
DashboardSaved(orgID int64, uid string) error
|
||||
DashboardSaved(orgID int64, uid string, rv string) error
|
||||
|
||||
// Called when a dashboard is deleted
|
||||
DashboardDeleted(orgID int64, uid string) error
|
||||
|
||||
@@ -2,6 +2,7 @@ package apistore
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"math/rand/v2"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
|
||||
authlib "github.com/grafana/authlib/types"
|
||||
dashv1 "github.com/grafana/grafana/apps/dashboard/pkg/apis/dashboard/v1beta1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
||||
"github.com/grafana/grafana/pkg/apimachinery/utils"
|
||||
)
|
||||
@@ -195,6 +197,42 @@ func TestPrepareObjectForStorage(t *testing.T) {
|
||||
require.Equal(t, int64(2), meta2.GetGeneration())
|
||||
})
|
||||
|
||||
t.Run("Update should skip incrementing generation when content is unchanged", func(t *testing.T) {
|
||||
dashboard := dashv1.Dashboard{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Name: "test",
|
||||
Generation: 123,
|
||||
Annotations: map[string]string{
|
||||
"A": "B",
|
||||
utils.AnnoKeyUpdatedTimestamp: "2025-12-17T01:01:00Z",
|
||||
},
|
||||
UID: "XXX",
|
||||
},
|
||||
Spec: v0alpha1.Unstructured{
|
||||
Object: map[string]any{
|
||||
"hello": "world",
|
||||
},
|
||||
},
|
||||
}
|
||||
dashboard.Name = "test-name"
|
||||
obj := dashboard.DeepCopyObject()
|
||||
tmp, err := utils.MetaAccessor(obj)
|
||||
tmp.SetGeneration(2)
|
||||
tmp.SetUpdatedTimestampMillis(12345)
|
||||
require.NoError(t, err)
|
||||
|
||||
v, err := s.prepareObjectForUpdate(ctx, obj, &dashboard)
|
||||
require.NoError(t, err)
|
||||
require.False(t, v.hasChanged, "no changes")
|
||||
|
||||
out := &unstructured.Unstructured{}
|
||||
err = json.Unmarshal(v.raw.Bytes(), out)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, int64(123), tmp.GetGeneration())
|
||||
require.Equal(t, "2025-12-17T01:01:00Z", tmp.GetAnnotation(utils.AnnoKeyUpdatedTimestamp))
|
||||
})
|
||||
|
||||
s.opts.RequireDeprecatedInternalID = true
|
||||
t.Run("Should generate internal id", func(t *testing.T) {
|
||||
dashboard := dashv1.Dashboard{}
|
||||
|
||||
@@ -25,9 +25,11 @@ import { DashboardEvent, DashboardEventAction } from './types';
|
||||
const sessionId = uuidv4();
|
||||
|
||||
class DashboardWatcher {
|
||||
private static readonly IGNORE_SAVE_WINDOW_MS = 5000;
|
||||
|
||||
channel?: LiveChannelAddress; // path to the channel
|
||||
uid?: string;
|
||||
ignoreSave?: boolean;
|
||||
ignoreSave = 0; // save any events until this time passes
|
||||
editing = false;
|
||||
lastEditing?: DashboardEvent;
|
||||
subscription?: Unsubscribable;
|
||||
@@ -84,8 +86,9 @@ class DashboardWatcher {
|
||||
this.uid = undefined;
|
||||
}
|
||||
|
||||
// ignore the next 5 seconds of save events
|
||||
ignoreNextSave() {
|
||||
this.ignoreSave = true;
|
||||
this.ignoreSave = Date.now() + DashboardWatcher.IGNORE_SAVE_WINDOW_MS;
|
||||
}
|
||||
|
||||
getRecentEditingEvent() {
|
||||
@@ -115,8 +118,11 @@ class DashboardWatcher {
|
||||
case DashboardEventAction.EditingStarted:
|
||||
case DashboardEventAction.Saved: {
|
||||
if (this.ignoreSave) {
|
||||
this.ignoreSave = false;
|
||||
return;
|
||||
if (this.ignoreSave < Date.now()) {
|
||||
this.ignoreSave = 0; // process the event
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const dash = getDashboardSrv().getCurrent();
|
||||
|
||||
@@ -11,4 +11,5 @@ export interface DashboardEvent {
|
||||
message?: string;
|
||||
sessionId?: string;
|
||||
timestamp?: number;
|
||||
rv?: string;
|
||||
}
|
||||
|
||||
+4
@@ -12,6 +12,7 @@ import kbn from 'app/core/utils/kbn';
|
||||
import { Resource } from 'app/features/apiserver/types';
|
||||
import { SaveDashboardFormCommonOptions } from 'app/features/dashboard-scene/saving/SaveDashboardForm';
|
||||
import { getDashboardUrl } from 'app/features/dashboard-scene/utils/getDashboardUrl';
|
||||
import { dashboardWatcher } from 'app/features/live/dashboard/dashboardWatcher';
|
||||
import { validationSrv } from 'app/features/manage-dashboards/services/ValidationSrv';
|
||||
import { PROVISIONING_URL } from 'app/features/provisioning/constants';
|
||||
import { useCreateOrUpdateRepositoryFile } from 'app/features/provisioning/hooks/useCreateOrUpdateRepositoryFile';
|
||||
@@ -204,6 +205,9 @@ export function SaveProvisionedDashboardForm({
|
||||
repositoryType: repository?.type ?? 'unknown',
|
||||
});
|
||||
|
||||
// ignore incoming save events
|
||||
dashboardWatcher.ignoreNextSave();
|
||||
|
||||
createOrUpdateFile({
|
||||
// Skip adding ref to the default branch request
|
||||
ref: ref === repository?.branch ? undefined : ref,
|
||||
|
||||
Reference in New Issue
Block a user