Navigation: Introduce a preferences table to store Navbar preferences (#44914)
* First attempt at creating new navbar_preferences table in db * Apply to every nav item instead of just home * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * Chore: introduce initTestDB options for features * fix unit tests * Add another unit test and some logic for detecting if a preference already exists * tidy up * Only override IsFeatureToggleEnabled if it's defined * Extract setNavPreferences out into it's own function, initialise features correctly * Make the linter happy * Use new structure * user essentials mob! 🔱 * user essentials mob! 🔱 * Split NavbarPreferences from Preferences * user essentials mob! 🔱 * user essentials mob! 🔱 * Fix lint error * Start adding tests * Change internal db structure to be a generic json object * GetJsonData -> GetPreferencesJsonData * Stop using simplejson + add some more unit tests * Update pkg/api/preferences.go Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com> * Updates following review comments * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * Change patch to upsert, add a unit test * remove commented out code * introduce patch user/org preferences methods * Return Navbar preferences in the get call * Fix integration test by instantiating JsonData * Address review comments * Rename HideFromNavbar -> Hide * add swagger:model comment * Add patch to the preferences documentation * Add openapi annotations * Add a short description * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * user essentials mob! 🔱 * Update unit tests * remove unneeded url * remove outdated comment * Update integration tests * update generated swagger Co-authored-by: Alexandra Vargas <alexa1866@gmail.com> Co-authored-by: Hugo Häggmark <hugo.haggmark@gmail.com> Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
co-authored by
Marcus Efraimsson
Alexandra Vargas
Hugo Häggmark
parent
60af3af92c
commit
586272e5f0
@@ -167,6 +167,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
|
||||
userRoute.Get("/preferences", routing.Wrap(hs.GetUserPreferences))
|
||||
userRoute.Put("/preferences", routing.Wrap(hs.UpdateUserPreferences))
|
||||
userRoute.Patch("/preferences", routing.Wrap(hs.PatchUserPreferences))
|
||||
|
||||
userRoute.Get("/auth-tokens", routing.Wrap(hs.GetUserAuthTokens))
|
||||
userRoute.Post("/revoke-auth-token", routing.Wrap(hs.RevokeUserAuthToken))
|
||||
@@ -229,6 +230,7 @@ func (hs *HTTPServer) registerRoutes() {
|
||||
// prefs
|
||||
orgRoute.Get("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesRead)), routing.Wrap(hs.GetOrgPreferences))
|
||||
orgRoute.Put("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite)), routing.Wrap(hs.UpdateOrgPreferences))
|
||||
orgRoute.Patch("/preferences", authorize(reqOrgAdmin, ac.EvalPermission(ActionOrgsPreferencesWrite)), routing.Wrap(hs.PatchOrgPreferences))
|
||||
})
|
||||
|
||||
// current org without requirement of user to be org admin
|
||||
|
||||
@@ -20,3 +20,14 @@ package definitions
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route PATCH /org/preferences org_preferences patchOrgPreferences
|
||||
//
|
||||
// Patch Current Org Prefs.
|
||||
//
|
||||
// Responses:
|
||||
// 200: addOrgUser
|
||||
// 400: badRequestError
|
||||
// 401: unauthorisedError
|
||||
// 403: forbiddenError
|
||||
// 500: internalServerError
|
||||
|
||||
@@ -23,6 +23,16 @@ import "github.com/grafana/grafana/pkg/api/dtos"
|
||||
// 401: unauthorisedError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:route PATCH /user/preferences user_preferences patchUserPreferences
|
||||
//
|
||||
// Patch user preferences.
|
||||
//
|
||||
// Responses:
|
||||
// 200: okResponse
|
||||
// 400: badRequestError
|
||||
// 401: unauthorisedError
|
||||
// 500: internalServerError
|
||||
|
||||
// swagger:parameters updateUserPreferences updateOrgPreferences updateTeamPreferences
|
||||
type UpdateUserPreferencesParam struct {
|
||||
// in:body
|
||||
@@ -35,3 +45,10 @@ type GetPreferencesResponse struct {
|
||||
// in:body
|
||||
Body dtos.Prefs `json:"body"`
|
||||
}
|
||||
|
||||
// swagger:parameters patchUserPreferences patchOrgPreferences patchTeamPreferences
|
||||
type PatchUserPreferencesParam struct {
|
||||
// in:body
|
||||
// required:true
|
||||
Body dtos.PatchPrefsCmd `json:"body"`
|
||||
}
|
||||
|
||||
@@ -37,7 +37,8 @@ const (
|
||||
// are negative to ensure that the default items are placed above
|
||||
// any items with default weight.
|
||||
|
||||
WeightHome = (iota - 20) * 100
|
||||
WeightSavedItems = (iota - 20) * 100
|
||||
WeightHome
|
||||
WeightCreate
|
||||
WeightDashboard
|
||||
WeightExplore
|
||||
|
||||
+23
-6
@@ -1,10 +1,13 @@
|
||||
package dtos
|
||||
|
||||
import "github.com/grafana/grafana/pkg/models"
|
||||
|
||||
type Prefs struct {
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Theme string `json:"theme"`
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Navbar models.NavbarPreference `json:"navbar,omitempty"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
@@ -15,6 +18,20 @@ type UpdatePrefsCmd struct {
|
||||
// Default:0
|
||||
HomeDashboardID int64 `json:"homeDashboardId"`
|
||||
// Enum: utc,browser
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Timezone string `json:"timezone"`
|
||||
WeekStart string `json:"weekStart"`
|
||||
Navbar *models.NavbarPreference `json:"navbar,omitempty"`
|
||||
}
|
||||
|
||||
// swagger:model
|
||||
type PatchPrefsCmd struct {
|
||||
// Enum: light,dark
|
||||
Theme *string `json:"theme,omitempty"`
|
||||
// The numerical :id of a favorited dashboard
|
||||
// Default:0
|
||||
HomeDashboardID *int64 `json:"homeDashboardId,omitempty"`
|
||||
// Enum: utc,browser
|
||||
Timezone *string `json:"timezone,omitempty"`
|
||||
WeekStart *string `json:"weekStart,omitempty"`
|
||||
Navbar *models.NavbarPreference `json:"navbar,omitempty"`
|
||||
}
|
||||
|
||||
@@ -159,6 +159,20 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
navTree := []*dtos.NavLink{}
|
||||
|
||||
if hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||
savedItemsLinks, err := hs.buildSavedItemsNavLinks(c)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
Text: "Saved Items",
|
||||
Id: "saved-items",
|
||||
Icon: "heart",
|
||||
SortWeight: dtos.WeightSavedItems,
|
||||
Section: dtos.NavSectionCore,
|
||||
Children: savedItemsLinks,
|
||||
})
|
||||
|
||||
navTree = append(navTree, &dtos.NavLink{
|
||||
Text: "Home",
|
||||
Id: "home",
|
||||
@@ -388,6 +402,30 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
|
||||
return navTree, nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) buildSavedItemsNavLinks(c *models.ReqContext) ([]*dtos.NavLink, error) {
|
||||
savedItemsChildNavs := []*dtos.NavLink{}
|
||||
|
||||
// query preferences table for any saved items
|
||||
prefsQuery := models.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
|
||||
if err := hs.SQLStore.GetPreferencesWithDefaults(c.Req.Context(), &prefsQuery); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
savedItems := prefsQuery.Result.JsonData.Navbar.SavedItems
|
||||
|
||||
if len(savedItems) > 0 {
|
||||
for _, savedItem := range savedItems {
|
||||
savedItemsChildNavs = append(savedItemsChildNavs, &dtos.NavLink{
|
||||
Id: savedItem.Id,
|
||||
Text: savedItem.Text,
|
||||
Url: savedItem.Url,
|
||||
Target: savedItem.Target,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return savedItemsChildNavs, nil
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) buildDashboardNavLinks(c *models.ReqContext, hasEditPerm bool) []*dtos.NavLink {
|
||||
dashboardChildNavs := []*dtos.NavLink{}
|
||||
if !hs.Features.IsEnabled(featuremgmt.FlagNewNavigation) {
|
||||
|
||||
@@ -51,6 +51,10 @@ func (hs *HTTPServer) getPreferencesFor(ctx context.Context, orgID, userID, team
|
||||
WeekStart: prefsQuery.Result.WeekStart,
|
||||
}
|
||||
|
||||
if prefsQuery.Result.JsonData != nil {
|
||||
dto.Navbar = prefsQuery.Result.JsonData.Navbar
|
||||
}
|
||||
|
||||
return response.JSON(200, &dto)
|
||||
}
|
||||
|
||||
@@ -84,6 +88,37 @@ func (hs *HTTPServer) updatePreferencesFor(ctx context.Context, orgID, userID, t
|
||||
return response.Success("Preferences updated")
|
||||
}
|
||||
|
||||
// PATCH /api/user/preferences
|
||||
func (hs *HTTPServer) PatchUserPreferences(c *models.ReqContext) response.Response {
|
||||
dtoCmd := dtos.PatchPrefsCmd{}
|
||||
if err := web.Bind(c.Req, &dtoCmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return hs.patchPreferencesFor(c.Req.Context(), c.OrgId, c.UserId, 0, &dtoCmd)
|
||||
}
|
||||
|
||||
func (hs *HTTPServer) patchPreferencesFor(ctx context.Context, orgID, userID, teamId int64, dtoCmd *dtos.PatchPrefsCmd) response.Response {
|
||||
if dtoCmd.Theme != nil && *dtoCmd.Theme != lightTheme && *dtoCmd.Theme != darkTheme && *dtoCmd.Theme != defaultTheme {
|
||||
return response.Error(400, "Invalid theme", nil)
|
||||
}
|
||||
patchCmd := models.PatchPreferencesCommand{
|
||||
UserId: userID,
|
||||
OrgId: orgID,
|
||||
TeamId: teamId,
|
||||
Theme: dtoCmd.Theme,
|
||||
Timezone: dtoCmd.Timezone,
|
||||
WeekStart: dtoCmd.WeekStart,
|
||||
HomeDashboardId: dtoCmd.HomeDashboardID,
|
||||
Navbar: dtoCmd.Navbar,
|
||||
}
|
||||
|
||||
if err := hs.SQLStore.PatchPreferences(ctx, &patchCmd); err != nil {
|
||||
return response.Error(500, "Failed to save preferences", err)
|
||||
}
|
||||
|
||||
return response.Success("Preferences updated")
|
||||
}
|
||||
|
||||
// GET /api/org/preferences
|
||||
func (hs *HTTPServer) GetOrgPreferences(c *models.ReqContext) response.Response {
|
||||
return hs.getPreferencesFor(c.Req.Context(), c.OrgId, 0, 0)
|
||||
@@ -97,3 +132,12 @@ func (hs *HTTPServer) UpdateOrgPreferences(c *models.ReqContext) response.Respon
|
||||
}
|
||||
return hs.updatePreferencesFor(c.Req.Context(), c.OrgId, 0, 0, &dtoCmd)
|
||||
}
|
||||
|
||||
// PATCH /api/org/preferences
|
||||
func (hs *HTTPServer) PatchOrgPreferences(c *models.ReqContext) response.Response {
|
||||
dtoCmd := dtos.PatchPrefsCmd{}
|
||||
if err := web.Bind(c.Req, &dtoCmd); err != nil {
|
||||
return response.Error(http.StatusBadRequest, "bad request data", err)
|
||||
}
|
||||
return hs.patchPreferencesFor(c.Req.Context(), c.OrgId, 0, 0, &dtoCmd)
|
||||
}
|
||||
|
||||
@@ -11,10 +11,16 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
getOrgPreferencesURL = "/api/org/preferences/"
|
||||
putOrgPreferencesURL = "/api/org/preferences/"
|
||||
getOrgPreferencesURL = "/api/org/preferences/"
|
||||
putOrgPreferencesURL = "/api/org/preferences/"
|
||||
patchOrgPreferencesUrl = "/api/org/preferences/"
|
||||
patchUserPreferencesUrl = "/api/user/preferences/"
|
||||
|
||||
testUpdateOrgPreferencesCmd = `{ "theme": "light", "homeDashboardId": 1 }`
|
||||
testUpdateOrgPreferencesCmd = `{ "theme": "light", "homeDashboardId": 1 }`
|
||||
testPatchOrgPreferencesCmd = `{"navbar":{"savedItems":[{"id":"snapshots","text":"Snapshots","icon":"camera","url":"/dashboard/snapshots"}]}}`
|
||||
testPatchOrgPreferencesCmdBad = `this is not json`
|
||||
testPatchUserPreferencesCmd = `{"navbar":{"savedItems":[{"id":"snapshots","text":"Snapshots","icon":"camera","url":"/dashboard/snapshots"}]}}`
|
||||
testPatchUserPreferencesCmdBad = `this is not json`
|
||||
)
|
||||
|
||||
func TestAPIEndpoint_GetCurrentOrgPreferences_LegacyAccessControl(t *testing.T) {
|
||||
@@ -109,3 +115,43 @@ func TestAPIEndpoint_PutCurrentOrgPreferences_AccessControl(t *testing.T) {
|
||||
assert.Equal(t, http.StatusForbidden, response.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIEndpoint_PatchUserPreferences(t *testing.T) {
|
||||
sc := setupHTTPServer(t, true, false)
|
||||
|
||||
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
setInitCtxSignedInOrgAdmin(sc.initCtx)
|
||||
input := strings.NewReader(testPatchUserPreferencesCmd)
|
||||
t.Run("Returns 200 on success", func(t *testing.T) {
|
||||
response := callAPI(sc.server, http.MethodPatch, patchUserPreferencesUrl, input, t)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
})
|
||||
|
||||
input = strings.NewReader(testPatchUserPreferencesCmdBad)
|
||||
t.Run("Returns 400 with bad data", func(t *testing.T) {
|
||||
response := callAPI(sc.server, http.MethodPut, patchUserPreferencesUrl, input, t)
|
||||
assert.Equal(t, http.StatusBadRequest, response.Code)
|
||||
})
|
||||
}
|
||||
|
||||
func TestAPIEndpoint_PatchOrgPreferences(t *testing.T) {
|
||||
sc := setupHTTPServer(t, true, false)
|
||||
|
||||
_, err := sc.db.CreateOrgWithMember("TestOrg", testUserID)
|
||||
require.NoError(t, err)
|
||||
|
||||
setInitCtxSignedInOrgAdmin(sc.initCtx)
|
||||
input := strings.NewReader(testPatchOrgPreferencesCmd)
|
||||
t.Run("Returns 200 on success", func(t *testing.T) {
|
||||
response := callAPI(sc.server, http.MethodPatch, patchOrgPreferencesUrl, input, t)
|
||||
assert.Equal(t, http.StatusOK, response.Code)
|
||||
})
|
||||
|
||||
input = strings.NewReader(testPatchOrgPreferencesCmdBad)
|
||||
t.Run("Returns 400 with bad data", func(t *testing.T) {
|
||||
response := callAPI(sc.server, http.MethodPut, patchOrgPreferencesUrl, input, t)
|
||||
assert.Equal(t, http.StatusBadRequest, response.Code)
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user