diff --git a/pkg/server/wire.go b/pkg/server/wire.go index 61a10765e42..74418497d83 100644 --- a/pkg/server/wire.go +++ b/pkg/server/wire.go @@ -65,13 +65,13 @@ import ( plugindashboardsservice "github.com/grafana/grafana/pkg/services/plugindashboards/service" "github.com/grafana/grafana/pkg/services/pluginsettings" pluginSettings "github.com/grafana/grafana/pkg/services/pluginsettings/service" + "github.com/grafana/grafana/pkg/services/preference/prefimpl" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/queryhistory" "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/services/schemaloader" "github.com/grafana/grafana/pkg/services/search" - "github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/secrets" secretsDatabase "github.com/grafana/grafana/pkg/services/secrets/database" @@ -235,6 +235,7 @@ var wireBasicSet = wire.NewSet( wire.Bind(new(alerting.DashAlertExtractor), new(*alerting.DashAlertExtractorService)), comments.ProvideService, guardian.ProvideService, + prefimpl.ProvideService, avatar.ProvideAvatarCacheServer, authproxy.ProvideAuthProxy, statscollector.ProvideService, diff --git a/pkg/services/preference/model.go b/pkg/services/preference/model.go new file mode 100644 index 00000000000..ae8b5ca665b --- /dev/null +++ b/pkg/services/preference/model.go @@ -0,0 +1,100 @@ +package pref + +import ( + "bytes" + "encoding/json" + "errors" + "time" +) + +var ErrPrefNotFound = errors.New("preference not found") + +type Preference struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id"` + UserID int64 `xorm:"user_id"` + TeamID int64 `xorm:"team_id"` + Teams []int64 `xorm:"extends"` + Version int + HomeDashboardID int64 `xorm:"home_dashboard_id"` + Timezone string + WeekStart string + Theme string + Created time.Time + Updated time.Time + JSONData *PreferenceJSONData `xorm:"json_data"` +} + +type GetPreferenceWithDefaultsQuery struct { + Teams []int64 + OrgID int64 + UserID int64 +} + +type GetPreferenceQuery struct { + OrgID int64 + UserID int64 + TeamID int64 +} + +type SavePreferenceCommand struct { + UserID int64 + OrgID int64 + TeamID int64 + + HomeDashboardID int64 `json:"homeDashboardId,omitempty"` + Timezone string `json:"timezone,omitempty"` + WeekStart string `json:"weekStart,omitempty"` + Theme string `json:"theme,omitempty"` + Navbar *NavbarPreference `json:"navbar,omitempty"` + QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"` +} + +type PatchPreferenceCommand struct { + UserID int64 + OrgID int64 + TeamID int64 + + HomeDashboardID *int64 `json:"homeDashboardId,omitempty"` + Timezone *string `json:"timezone,omitempty"` + WeekStart *string `json:"weekStart,omitempty"` + Theme *string `json:"theme,omitempty"` + Navbar *NavbarPreference `json:"navbar,omitempty"` + QueryHistory *QueryHistoryPreference `json:"queryHistory,omitempty"` +} + +type NavLink struct { + ID string `json:"id,omitempty"` + Text string `json:"text,omitempty"` + Url string `json:"url,omitempty"` + Target string `json:"target,omitempty"` +} + +type NavbarPreference struct { + SavedItems []NavLink `json:"savedItems"` +} + +type PreferenceJSONData struct { + Navbar NavbarPreference `json:"navbar"` + QueryHistory QueryHistoryPreference `json:"queryHistory"` +} + +type QueryHistoryPreference struct { + HomeTab string `json:"homeTab"` +} + +func (j *PreferenceJSONData) FromDB(data []byte) error { + dec := json.NewDecoder(bytes.NewBuffer(data)) + dec.UseNumber() + return dec.Decode(j) +} + +func (j *PreferenceJSONData) ToDB() ([]byte, error) { + if j == nil { + return nil, nil + } + + return json.Marshal(j) +} + +func (p Preference) TableName() string { return "preferences" } diff --git a/pkg/services/preference/pref.go b/pkg/services/preference/pref.go new file mode 100644 index 00000000000..5cf17789a25 --- /dev/null +++ b/pkg/services/preference/pref.go @@ -0,0 +1,13 @@ +package pref + +import ( + "context" +) + +type Service interface { + GetWithDefaults(context.Context, *GetPreferenceWithDefaultsQuery) (*Preference, error) + Get(context.Context, *GetPreferenceQuery) (*Preference, error) + Save(context.Context, *SavePreferenceCommand) error + Patch(ctx context.Context, cmd *PatchPreferenceCommand) error + GetDefaults() *Preference +} diff --git a/pkg/services/preference/prefimpl/pref.go b/pkg/services/preference/prefimpl/pref.go new file mode 100644 index 00000000000..c43fb7d98c5 --- /dev/null +++ b/pkg/services/preference/prefimpl/pref.go @@ -0,0 +1,203 @@ +package prefimpl + +import ( + "context" + "errors" + "time" + + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore/db" + "github.com/grafana/grafana/pkg/setting" +) + +type Service struct { + store store + cfg *setting.Cfg +} + +func ProvideService(db db.DB, cfg *setting.Cfg) *Service { + return &Service{ + store: &sqlStore{ + db: db, + }, + cfg: cfg, + } +} + +func (s *Service) GetWithDefaults(ctx context.Context, query *pref.GetPreferenceWithDefaultsQuery) (*pref.Preference, error) { + listQuery := &pref.Preference{ + Teams: query.Teams, + OrgID: query.OrgID, + UserID: query.UserID, + } + prefs, err := s.store.List(ctx, listQuery) + if err != nil { + return nil, err + } + + res := s.GetDefaults() + for _, p := range prefs { + if p.Theme != "" { + res.Theme = p.Theme + } + if p.Timezone != "" { + res.Timezone = p.Timezone + } + if p.WeekStart != "" { + res.WeekStart = p.WeekStart + } + if p.HomeDashboardID != 0 { + res.HomeDashboardID = p.HomeDashboardID + } + if p.JSONData != nil { + res.JSONData = p.JSONData + } + } + + return res, err +} + +func (s *Service) Get(ctx context.Context, query *pref.GetPreferenceQuery) (*pref.Preference, error) { + getPref := &pref.Preference{ + OrgID: query.OrgID, + UserID: query.UserID, + TeamID: query.TeamID, + } + prefs, err := s.store.Get(ctx, getPref) + if err != nil && !errors.Is(err, pref.ErrPrefNotFound) { + return nil, err + } + return prefs, nil +} + +func (s *Service) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) error { + preference, err := s.store.Get(ctx, &pref.Preference{ + OrgID: cmd.OrgID, + UserID: cmd.UserID, + TeamID: cmd.TeamID, + }) + if err != nil { + if errors.Is(err, pref.ErrPrefNotFound) { + preference := &pref.Preference{ + UserID: cmd.UserID, + OrgID: cmd.OrgID, + TeamID: cmd.TeamID, + HomeDashboardID: cmd.HomeDashboardID, + Timezone: cmd.Timezone, + WeekStart: cmd.WeekStart, + Theme: cmd.Theme, + Created: time.Now(), + Updated: time.Now(), + } + _, err = s.store.Insert(ctx, preference) + if err != nil { + return err + } + } + return err + } + preference.Timezone = cmd.Timezone + preference.WeekStart = cmd.WeekStart + preference.Theme = cmd.Theme + preference.Updated = time.Now() + preference.Version += 1 + preference.JSONData = &pref.PreferenceJSONData{} + + if cmd.Navbar != nil { + preference.JSONData.Navbar = *cmd.Navbar + } + if cmd.QueryHistory != nil { + preference.JSONData.QueryHistory = *cmd.QueryHistory + } + return s.store.Update(ctx, preference) +} + +func (s *Service) Patch(ctx context.Context, cmd *pref.PatchPreferenceCommand) error { + var exists bool + preference, err := s.store.Get(ctx, &pref.Preference{ + OrgID: cmd.OrgID, + UserID: cmd.UserID, + TeamID: cmd.TeamID, + }) + if err != nil && !errors.Is(err, pref.ErrPrefNotFound) { + return err + } + + if errors.Is(err, pref.ErrPrefNotFound) { + preference = &pref.Preference{ + UserID: cmd.UserID, + OrgID: cmd.OrgID, + TeamID: cmd.TeamID, + Created: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + } + } else { + exists = true + } + + if cmd.Navbar != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.Navbar.SavedItems != nil { + preference.JSONData.Navbar.SavedItems = cmd.Navbar.SavedItems + } + } + + if cmd.QueryHistory != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.QueryHistory.HomeTab != "" { + preference.JSONData.QueryHistory.HomeTab = cmd.QueryHistory.HomeTab + } + } + + if cmd.HomeDashboardID != nil { + preference.HomeDashboardID = *cmd.HomeDashboardID + } + + if cmd.Timezone != nil { + preference.Timezone = *cmd.Timezone + } + + if cmd.WeekStart != nil { + preference.WeekStart = *cmd.WeekStart + } + + if cmd.Theme != nil { + preference.Theme = *cmd.Theme + } + + preference.Updated = time.Now() + preference.Version += 1 + + // Wrap this in an if statement to maintain backwards compatibility + if cmd.Navbar != nil { + if preference.JSONData == nil { + preference.JSONData = &pref.PreferenceJSONData{} + } + if cmd.Navbar.SavedItems != nil { + preference.JSONData.Navbar.SavedItems = cmd.Navbar.SavedItems + } + } + + if exists { + err = s.store.Update(ctx, preference) + } else { + _, err = s.store.Insert(ctx, preference) + } + return err +} + +func (s *Service) GetDefaults() *pref.Preference { + defaults := &pref.Preference{ + Theme: s.cfg.DefaultTheme, + Timezone: s.cfg.DateFormats.DefaultTimezone, + WeekStart: s.cfg.DateFormats.DefaultWeekStart, + HomeDashboardID: 0, + JSONData: &pref.PreferenceJSONData{}, + } + + return defaults +} diff --git a/pkg/services/preference/prefimpl/pref_test.go b/pkg/services/preference/prefimpl/pref_test.go new file mode 100644 index 00000000000..b1e439b6830 --- /dev/null +++ b/pkg/services/preference/prefimpl/pref_test.go @@ -0,0 +1,446 @@ +package prefimpl + +import ( + "context" + "testing" + + "github.com/google/go-cmp/cmp" + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestPreferencesService(t *testing.T) { + prefStoreFake := newPreferenceStoreFake() + prefService := &Service{ + store: prefStoreFake, + } + + emptyNavbarPreferences := pref.NavbarPreference{} + userNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "explore", + Text: "Explore", + Url: "/explore", + }}, + } + orgNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "alerting", + Text: "Alerting", + Url: "/alerting", + }}, + } + team1NavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "dashboards", + Text: "Dashboards", + Url: "/dashboards", + }}, + } + team2NavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "home", + Text: "Home", + Url: "/home", + }}, + } + + emptyQueryPreference := pref.QueryHistoryPreference{} + + queryPreference := pref.QueryHistoryPreference{ + HomeTab: "hometab", + } + + queryPreference2 := pref.QueryHistoryPreference{ + HomeTab: "hometab", + } + + emptyPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: emptyNavbarPreferences, + } + userPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: userNavbarPreferences, + QueryHistory: queryPreference, + } + orgPreferencesJsonData := pref.PreferenceJSONData{ + Navbar: orgNavbarPreferences, + } + team2PreferencesJsonData := pref.PreferenceJSONData{ + Navbar: team2NavbarPreferences, + } + team1PreferencesJsonData := pref.PreferenceJSONData{ + Navbar: team1NavbarPreferences, + } + + t.Run("GetDefaults should return defaults", func(t *testing.T) { + prefService.cfg = setting.NewCfg() + prefService.cfg.DefaultTheme = "light" + prefService.cfg.DateFormats.DefaultTimezone = "UTC" + + preferences := prefService.GetDefaults() + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + HomeDashboardID: 0, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetDefaults with no saved preferences should return defaults", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + Theme: "light", + Timezone: "UTC", + } + query := &pref.GetPreferenceWithDefaultsQuery{} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + HomeDashboardID: 0, + JSONData: &emptyPreferencesJsonData, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetWithDefaults with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "dark", + Timezone: "UTC", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + WeekStart: "1", + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "UTC", + WeekStart: "1", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetWithDefaults with saved org and other user home dashboard should return org home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "dark", + Timezone: "UTC", + WeekStart: "1", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + Timezone: "browser", + WeekStart: "2", + }, + } + prefService.GetDefaults().HomeDashboardID = 1 + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 2} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "browser", + WeekStart: "2", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("GetPreferencesWithDefaults with saved org and user json data should return user json data", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + JSONData: &orgPreferencesJsonData, + }, + { + OrgID: 1, + UserID: 1, + JSONData: &userPreferencesJsonData, + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, UserID: 1} + preference, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + require.Equal(t, &pref.Preference{ + Theme: "light", + JSONData: &userPreferencesJsonData, + Timezone: "UTC", + }, preference) + }) + + t.Run("GetWithDefaults with saved org and teams json data should return last team json data", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{} + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + JSONData: &orgPreferencesJsonData, + }, + { + OrgID: 1, + TeamID: 2, + JSONData: &team1PreferencesJsonData, + }, + { + OrgID: 1, + TeamID: 3, + JSONData: &team2PreferencesJsonData, + }, + } + query := &pref.GetPreferenceWithDefaultsQuery{ + OrgID: 1, Teams: []int64{2, 3}, + } + preference, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + require.Equal(t, &pref.Preference{ + Timezone: "UTC", + Theme: "light", + JSONData: &team2PreferencesJsonData, + }, preference) + }) + + t.Run("GetWithDefaults with saved org and teams home dashboard should return last team home dashboard", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + Theme: "dark", + Timezone: "UTC", + } + prefStoreFake.ExpectedListPreferences = []*pref.Preference{ + { + OrgID: 1, + HomeDashboardID: 1, + Theme: "light", + Timezone: "browser", + WeekStart: "1", + }, + { + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + Theme: "light", + Timezone: "browser", + WeekStart: "2", + }, + } + + query := &pref.GetPreferenceWithDefaultsQuery{OrgID: 1, Teams: []int64{2, 3}} + preferences, err := prefService.GetWithDefaults(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + Theme: "light", + Timezone: "browser", + WeekStart: "2", + HomeDashboardID: 4, + JSONData: &pref.PreferenceJSONData{}, + } + if diff := cmp.Diff(expected, preferences); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("SavePreferences for a user should store correct values", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1"}, + ) + require.NoError(t, err) + }) + + t.Run("SavePreferences for a user should store correct values, when preference not found", func(t *testing.T) { + prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + }, + ) + require.NoError(t, err) + prefStoreFake.ExpectedGetError = nil + }) + + t.Run("SavePreferences for a user should store correct values with nav and query history", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + ID: 1, + OrgID: 1, + UserID: 3, + TeamID: 6, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: &userPreferencesJsonData, + } + err := prefService.Save(context.Background(), + &pref.SavePreferenceCommand{ + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + Navbar: &userNavbarPreferences, + QueryHistory: &emptyQueryPreference, + }, + ) + require.NoError(t, err) + }) + + t.Run("Get for a user should store correct values", func(t *testing.T) { + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + preference, err := prefService.Get(context.Background(), &pref.GetPreferenceQuery{}) + require.NoError(t, err) + + expected := &pref.Preference{ + ID: preference.ID, + Version: preference.Version, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + Created: preference.Created, + Updated: preference.Updated, + } + if diff := cmp.Diff(expected, preference); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + + t.Run("Patch for a user should store correct values", func(t *testing.T) { + darkTheme := "dark" + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: &userPreferencesJsonData, + } + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + Theme: &darkTheme, + Navbar: &userNavbarPreferences, + QueryHistory: &queryPreference2, + }) + require.NoError(t, err) + }) + + t.Run("Patch for a user should store correct values, without navbar and query history", func(t *testing.T) { + darkTheme := "dark" + prefStoreFake.ExpectedPreference = &pref.Preference{ + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + } + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + Theme: &darkTheme, + Navbar: &userNavbarPreferences, + QueryHistory: &queryPreference2, + }) + require.NoError(t, err) + }) + + t.Run("Patch for a user should store correct values, when preference not found", func(t *testing.T) { + timezone := "browser" + weekStart := "1" + homeDashboardID := int64(5) + prefStoreFake.ExpectedGetError = pref.ErrPrefNotFound + prefStoreFake.ExpectedPreference = nil + + err := prefService.Patch(context.Background(), + &pref.PatchPreferenceCommand{ + HomeDashboardID: &homeDashboardID, + Timezone: &timezone, + WeekStart: &weekStart, + Navbar: &emptyNavbarPreferences, + QueryHistory: &emptyQueryPreference, + }) + require.NoError(t, err) + prefStoreFake.ExpectedGetError = nil + }) +} + +type FakePreferenceStore struct { + ExpectedPreference *pref.Preference + ExpectedListPreferences []*pref.Preference + ExpectedID int64 + ExpectedError error + ExpectedGetError error +} + +func newPreferenceStoreFake() *FakePreferenceStore { + return &FakePreferenceStore{} +} + +func (f *FakePreferenceStore) List(ctx context.Context, query *pref.Preference) ([]*pref.Preference, error) { + return f.ExpectedListPreferences, f.ExpectedError +} + +func (f *FakePreferenceStore) Get(ctx context.Context, query *pref.Preference) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedGetError +} + +func (f *FakePreferenceStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, error) { + return f.ExpectedID, f.ExpectedError +} + +func (f *FakePreferenceStore) Update(ctx context.Context, cmd *pref.Preference) error { + return f.ExpectedError +} diff --git a/pkg/services/preference/prefimpl/store.go b/pkg/services/preference/prefimpl/store.go new file mode 100644 index 00000000000..139b723363b --- /dev/null +++ b/pkg/services/preference/prefimpl/store.go @@ -0,0 +1,88 @@ +package prefimpl + +import ( + "context" + "strings" + + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/services/sqlstore/db" +) + +type store interface { + Get(context.Context, *pref.Preference) (*pref.Preference, error) + List(context.Context, *pref.Preference) ([]*pref.Preference, error) + Insert(context.Context, *pref.Preference) (int64, error) + Update(context.Context, *pref.Preference) error +} + +type sqlStore struct { + db db.DB +} + +func (s *sqlStore) Get(ctx context.Context, query *pref.Preference) (*pref.Preference, error) { + var prefs pref.Preference + err := s.db.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + exist, err := sess.Where("org_id=? AND user_id=? AND team_id=?", query.OrgID, query.UserID, query.TeamID).Get(&prefs) + if err != nil { + return err + } + if !exist { + return pref.ErrPrefNotFound + } + return nil + }) + if err != nil { + return nil, err + } + return &prefs, nil +} + +func (s *sqlStore) List(ctx context.Context, query *pref.Preference) ([]*pref.Preference, error) { + prefs := make([]*pref.Preference, 0) + params := make([]interface{}, 0) + filter := "" + + if len(query.Teams) > 0 { + filter = "(org_id=? AND team_id IN (?" + strings.Repeat(",?", len(query.Teams)-1) + ")) OR " + params = append(params, query.OrgID) + for _, v := range query.Teams { + params = append(params, v) + } + } + + filter += "(org_id=? AND user_id=? AND team_id=0) OR (org_id=? AND team_id=0 AND user_id=0)" + params = append(params, query.OrgID) + params = append(params, query.UserID) + params = append(params, query.OrgID) + + err := s.db.WithDbSession(ctx, func(dbSession *sqlstore.DBSession) error { + err := dbSession.Where(filter, params...). + OrderBy("user_id ASC, team_id ASC"). + Find(&prefs) + + if err != nil { + return err + } + + return nil + }) + return prefs, err +} + +func (s *sqlStore) Update(ctx context.Context, cmd *pref.Preference) error { + return s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.ID(cmd.ID).AllCols().Update(cmd) + return err + }) +} + +func (s *sqlStore) Insert(ctx context.Context, cmd *pref.Preference) (int64, error) { + var ID int64 + var err error + err = s.db.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { + ID, err = sess.Insert(cmd) + return err + }) + return ID, err +} diff --git a/pkg/services/preference/prefimpl/store_test.go b/pkg/services/preference/prefimpl/store_test.go new file mode 100644 index 00000000000..773b6dea57b --- /dev/null +++ b/pkg/services/preference/prefimpl/store_test.go @@ -0,0 +1,170 @@ +//go:build integration +// +build integration + +package prefimpl + +import ( + "context" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/grafana/grafana/pkg/models" + pref "github.com/grafana/grafana/pkg/services/preference" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestPreferencesDataAccess(t *testing.T) { + ss := sqlstore.InitTestDB(t) + prefStore := sqlStore{db: ss} + orgNavbarPreferences := pref.NavbarPreference{ + SavedItems: []pref.NavLink{{ + ID: "alerting", + Text: "Alerting", + Url: "/alerting", + }}, + } + + t.Run("Get with saved org and user home dashboard returns not found", func(t *testing.T) { + query := &pref.Preference{OrgID: 1, UserID: 1, TeamID: 2} + prefs, err := prefStore.Get(context.Background(), query) + require.EqualError(t, err, pref.ErrPrefNotFound.Error()) + require.Nil(t, prefs) + }) + + t.Run("Get with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 1, + HomeDashboardID: 4, + TeamID: 2, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, TeamID: 2} + prefs, err := prefStore.Get(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs.HomeDashboardID) + }) + + t.Run("List with saved org and user home dashboard should return user home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 1, + TeamID: 3, + HomeDashboardID: 1, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, Teams: []int64{2}} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs[0].HomeDashboardID) + }) + + t.Run("List with saved org and other user home dashboard should return org home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + OrgID: 1, + UserID: 2, + TeamID: 3, + HomeDashboardID: 1, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1, UserID: 1, Teams: []int64{3}} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(1), prefs[0].HomeDashboardID) + require.Equal(t, int64(1), prefs[1].HomeDashboardID) + }) + + t.Run("List with saved org and teams home dashboard should return last team home dashboard", func(t *testing.T) { + query := &pref.Preference{ + OrgID: 1, Teams: []int64{2, 3}, + } + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(4), prefs[0].HomeDashboardID) + require.Equal(t, int64(1), prefs[1].HomeDashboardID) + require.Equal(t, int64(1), prefs[2].HomeDashboardID) + }) + + t.Run("List with saved org and other teams home dashboard should return org home dashboard", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, HomeDashboardID: 1, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + _, err = prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, TeamID: 2, HomeDashboardID: 2, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + _, err = prefStore.Insert(context.Background(), &pref.Preference{OrgID: 1, TeamID: 3, HomeDashboardID: 3, Created: time.Now(), Updated: time.Now()}) + require.NoError(t, err) + + query := &pref.Preference{OrgID: 1} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + require.Equal(t, int64(1), prefs[0].HomeDashboardID) + }) + + t.Run("Update for a user should only modify a single value", func(t *testing.T) { + ss := sqlstore.InitTestDB(t) + prefStore := sqlStore{db: ss} + id, err := prefStore.Insert(context.Background(), &pref.Preference{ + UserID: models.SignedInUser{}.UserId, + Theme: "dark", + Timezone: "browser", + HomeDashboardID: 5, + WeekStart: "1", + JSONData: &pref.PreferenceJSONData{Navbar: orgNavbarPreferences}, + Created: time.Now(), + Updated: time.Now(), + }) + require.NoError(t, err) + + err = prefStore.Update(context.Background(), &pref.Preference{ + ID: id, + Theme: "dark", + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Created: time.Now(), + Updated: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + }) + require.NoError(t, err) + query := &pref.Preference{} + prefs, err := prefStore.List(context.Background(), query) + require.NoError(t, err) + expected := &pref.Preference{ + ID: prefs[0].ID, + Version: prefs[0].Version, + HomeDashboardID: 5, + Timezone: "browser", + WeekStart: "1", + Theme: "dark", + JSONData: prefs[0].JSONData, + Created: prefs[0].Created, + Updated: prefs[0].Updated, + } + if diff := cmp.Diff(expected, prefs[0]); diff != "" { + t.Fatalf("Result mismatch (-want +got):\n%s", diff) + } + }) + t.Run("insert preference that does not exist", func(t *testing.T) { + _, err := prefStore.Insert(context.Background(), + &pref.Preference{ + UserID: models.SignedInUser{}.UserId, + Created: time.Now(), + Updated: time.Now(), + JSONData: &pref.PreferenceJSONData{}, + }) + require.NoError(t, err) + }) +} diff --git a/pkg/services/preference/preftest/fake.go b/pkg/services/preference/preftest/fake.go new file mode 100644 index 00000000000..f52acea2047 --- /dev/null +++ b/pkg/services/preference/preftest/fake.go @@ -0,0 +1,32 @@ +package preftest + +import ( + "context" + + pref "github.com/grafana/grafana/pkg/services/preference" +) + +type FakePreferenceService struct { + ExpectedPreference *pref.Preference + ExpectedError error +} + +func NewPreferenceServiceFake() *FakePreferenceService { + return &FakePreferenceService{} +} + +func (f *FakePreferenceService) GetWithDefaults(ctx context.Context, query *pref.GetPreferenceWithDefaultsQuery) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedError +} + +func (f *FakePreferenceService) Get(ctx context.Context, query *pref.GetPreferenceQuery) (*pref.Preference, error) { + return f.ExpectedPreference, f.ExpectedError +} + +func (f *FakePreferenceService) Save(ctx context.Context, cmd *pref.SavePreferenceCommand) error { + return f.ExpectedError +} + +func (f *FakePreferenceService) GetDefaults() *pref.Preference { + return f.ExpectedPreference +}