mirror of
https://github.com/grafana/grafana.git
synced 2024-11-26 02:40:26 -06:00
d4e013fd44
The ordering of links in the navigation bar is currently based the order of the slice containing the navigation tree. Since Grafana supports adding more links to the navigation bar with `RunIndexDataHooks` which runs at the very end of the function this means that any link registered through a hook will be placed last in the slice and be displayed last in the menu. With this PR the ordering can be specified with a weight which allows for placing links created by extensions in a more intuitive place where applicable. Stable sorting is used to ensure that the current FIFO ordering is preserved when either no weight is set or two items shares the same weight.
396 lines
13 KiB
Go
396 lines
13 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
m "github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
)
|
|
|
|
const (
|
|
// Themes
|
|
lightName = "light"
|
|
darkName = "dark"
|
|
)
|
|
|
|
func (hs *HTTPServer) setIndexViewData(c *m.ReqContext) (*dtos.IndexViewData, error) {
|
|
settings, err := hs.getFrontendSettingsMap(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
prefsQuery := m.GetPreferencesWithDefaultsQuery{User: c.SignedInUser}
|
|
if err := bus.Dispatch(&prefsQuery); err != nil {
|
|
return nil, err
|
|
}
|
|
prefs := prefsQuery.Result
|
|
|
|
// Read locale from acccept-language
|
|
acceptLang := c.Req.Header.Get("Accept-Language")
|
|
locale := "en-US"
|
|
|
|
if len(acceptLang) > 0 {
|
|
parts := strings.Split(acceptLang, ",")
|
|
locale = parts[0]
|
|
}
|
|
|
|
appURL := setting.AppUrl
|
|
appSubURL := setting.AppSubUrl
|
|
|
|
// special case when doing localhost call from phantomjs
|
|
if c.IsRenderCall {
|
|
appURL = fmt.Sprintf("%s://localhost:%s", setting.Protocol, setting.HttpPort)
|
|
appSubURL = ""
|
|
settings["appSubUrl"] = ""
|
|
}
|
|
|
|
hasEditPermissionInFoldersQuery := m.HasEditPermissionInFoldersQuery{SignedInUser: c.SignedInUser}
|
|
if err := bus.Dispatch(&hasEditPermissionInFoldersQuery); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var data = dtos.IndexViewData{
|
|
User: &dtos.CurrentUser{
|
|
Id: c.UserId,
|
|
IsSignedIn: c.IsSignedIn,
|
|
Login: c.Login,
|
|
Email: c.Email,
|
|
Name: c.Name,
|
|
OrgCount: c.OrgCount,
|
|
OrgId: c.OrgId,
|
|
OrgName: c.OrgName,
|
|
OrgRole: c.OrgRole,
|
|
GravatarUrl: dtos.GetGravatarUrl(c.Email),
|
|
IsGrafanaAdmin: c.IsGrafanaAdmin,
|
|
LightTheme: prefs.Theme == lightName,
|
|
Timezone: prefs.Timezone,
|
|
Locale: locale,
|
|
HelpFlags1: c.HelpFlags1,
|
|
HasEditPermissionInFolders: hasEditPermissionInFoldersQuery.Result,
|
|
},
|
|
Settings: settings,
|
|
Theme: prefs.Theme,
|
|
AppUrl: appURL,
|
|
AppSubUrl: appSubURL,
|
|
GoogleAnalyticsId: setting.GoogleAnalyticsId,
|
|
GoogleTagManagerId: setting.GoogleTagManagerId,
|
|
BuildVersion: setting.BuildVersion,
|
|
BuildCommit: setting.BuildCommit,
|
|
NewGrafanaVersion: plugins.GrafanaLatestVersion,
|
|
NewGrafanaVersionExists: plugins.GrafanaHasUpdate,
|
|
AppName: setting.ApplicationName,
|
|
AppNameBodyClass: getAppNameBodyClass(hs.License.HasValidLicense()),
|
|
}
|
|
|
|
if setting.DisableGravatar {
|
|
data.User.GravatarUrl = setting.AppSubUrl + "/public/img/user_profile.png"
|
|
}
|
|
|
|
if len(data.User.Name) == 0 {
|
|
data.User.Name = data.User.Login
|
|
}
|
|
|
|
themeURLParam := c.Query("theme")
|
|
if themeURLParam == lightName {
|
|
data.User.LightTheme = true
|
|
data.Theme = lightName
|
|
} else if themeURLParam == darkName {
|
|
data.User.LightTheme = false
|
|
data.Theme = darkName
|
|
}
|
|
|
|
if hasEditPermissionInFoldersQuery.Result {
|
|
children := []*dtos.NavLink{
|
|
{Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"},
|
|
}
|
|
|
|
if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR {
|
|
children = append(children, &dtos.NavLink{Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"})
|
|
}
|
|
|
|
children = append(children, &dtos.NavLink{Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"})
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Create",
|
|
Id: "create",
|
|
Icon: "fa fa-fw fa-plus",
|
|
Url: setting.AppSubUrl + "/dashboard/new",
|
|
Children: children,
|
|
SortWeight: dtos.WeightCreate,
|
|
})
|
|
}
|
|
|
|
dashboardChildNavs := []*dtos.NavLink{
|
|
{Text: "Home", Id: "home", Url: setting.AppSubUrl + "/", Icon: "gicon gicon-home", HideFromTabs: true},
|
|
{Text: "Divider", Divider: true, Id: "divider", HideFromTabs: true},
|
|
{Text: "Manage", Id: "manage-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "gicon gicon-manage"},
|
|
{Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "gicon gicon-playlists"},
|
|
{Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "gicon gicon-snapshots"},
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Dashboards",
|
|
Id: "dashboards",
|
|
SubTitle: "Manage dashboards & folders",
|
|
Icon: "gicon gicon-dashboard",
|
|
Url: setting.AppSubUrl + "/",
|
|
SortWeight: dtos.WeightDashboard,
|
|
Children: dashboardChildNavs,
|
|
})
|
|
|
|
if setting.ExploreEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR || setting.ViewersCanEdit) {
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Explore",
|
|
Id: "explore",
|
|
SubTitle: "Explore your data",
|
|
Icon: "gicon gicon-explore",
|
|
SortWeight: dtos.WeightExplore,
|
|
Url: setting.AppSubUrl + "/explore",
|
|
})
|
|
}
|
|
|
|
if c.IsSignedIn {
|
|
// Only set login if it's different from the name
|
|
var login string
|
|
if c.SignedInUser.Login != c.SignedInUser.NameOrFallback() {
|
|
login = c.SignedInUser.Login
|
|
}
|
|
profileNode := &dtos.NavLink{
|
|
Text: c.SignedInUser.NameOrFallback(),
|
|
SubTitle: login,
|
|
Id: "profile",
|
|
Img: data.User.GravatarUrl,
|
|
Url: setting.AppSubUrl + "/profile",
|
|
HideFromMenu: true,
|
|
SortWeight: dtos.WeightProfile,
|
|
Children: []*dtos.NavLink{
|
|
{Text: "Preferences", Id: "profile-settings", Url: setting.AppSubUrl + "/profile", Icon: "gicon gicon-preferences"},
|
|
{Text: "Change Password", Id: "change-password", Url: setting.AppSubUrl + "/profile/password", Icon: "fa fa-fw fa-lock", HideFromMenu: true},
|
|
},
|
|
}
|
|
|
|
if !setting.DisableSignoutMenu {
|
|
// add sign out first
|
|
profileNode.Children = append(profileNode.Children, &dtos.NavLink{
|
|
Text: "Sign out", Id: "sign-out", Url: setting.AppSubUrl + "/logout", Icon: "fa fa-fw fa-sign-out", Target: "_self",
|
|
})
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, profileNode)
|
|
}
|
|
|
|
if setting.AlertingEnabled && (c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR) {
|
|
alertChildNavs := []*dtos.NavLink{
|
|
{Text: "Alert Rules", Id: "alert-list", Url: setting.AppSubUrl + "/alerting/list", Icon: "gicon gicon-alert-rules"},
|
|
{Text: "Notification channels", Id: "channels", Url: setting.AppSubUrl + "/alerting/notifications", Icon: "gicon gicon-alert-notification-channel"},
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Alerting",
|
|
SubTitle: "Alert rules & notifications",
|
|
Id: "alerting",
|
|
Icon: "gicon gicon-alert",
|
|
Url: setting.AppSubUrl + "/alerting/list",
|
|
Children: alertChildNavs,
|
|
SortWeight: dtos.WeightAlerting,
|
|
})
|
|
}
|
|
|
|
enabledPlugins, err := plugins.GetEnabledPlugins(c.OrgId)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, plugin := range enabledPlugins.Apps {
|
|
if plugin.Pinned {
|
|
appLink := &dtos.NavLink{
|
|
Text: plugin.Name,
|
|
Id: "plugin-page-" + plugin.Id,
|
|
Url: plugin.DefaultNavUrl,
|
|
Img: plugin.Info.Logos.Small,
|
|
SortWeight: dtos.WeightPlugin,
|
|
}
|
|
|
|
for _, include := range plugin.Includes {
|
|
if !c.HasUserRole(include.Role) {
|
|
continue
|
|
}
|
|
|
|
if include.Type == "page" && include.AddToNav {
|
|
link := &dtos.NavLink{
|
|
Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/page/" + include.Slug,
|
|
Text: include.Name,
|
|
}
|
|
appLink.Children = append(appLink.Children, link)
|
|
}
|
|
|
|
if include.Type == "dashboard" && include.AddToNav {
|
|
link := &dtos.NavLink{
|
|
Url: setting.AppSubUrl + "/dashboard/db/" + include.Slug,
|
|
Text: include.Name,
|
|
}
|
|
appLink.Children = append(appLink.Children, link)
|
|
}
|
|
}
|
|
|
|
if len(appLink.Children) > 0 && c.OrgRole == m.ROLE_ADMIN {
|
|
appLink.Children = append(appLink.Children, &dtos.NavLink{Divider: true})
|
|
appLink.Children = append(appLink.Children, &dtos.NavLink{Text: "Plugin Config", Icon: "gicon gicon-cog", Url: setting.AppSubUrl + "/plugins/" + plugin.Id + "/"})
|
|
}
|
|
|
|
if len(appLink.Children) > 0 {
|
|
data.NavTree = append(data.NavTree, appLink)
|
|
}
|
|
}
|
|
}
|
|
|
|
configNodes := []*dtos.NavLink{}
|
|
|
|
if c.OrgRole == m.ROLE_ADMIN {
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "Data Sources",
|
|
Icon: "gicon gicon-datasources",
|
|
Description: "Add and configure data sources",
|
|
Id: "datasources",
|
|
Url: setting.AppSubUrl + "/datasources",
|
|
})
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "Users",
|
|
Id: "users",
|
|
Description: "Manage org members",
|
|
Icon: "gicon gicon-user",
|
|
Url: setting.AppSubUrl + "/org/users",
|
|
})
|
|
}
|
|
|
|
if c.OrgRole == m.ROLE_ADMIN || hs.Cfg.EditorsCanAdmin {
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "Teams",
|
|
Id: "teams",
|
|
Description: "Manage org groups",
|
|
Icon: "gicon gicon-team",
|
|
Url: setting.AppSubUrl + "/org/teams",
|
|
})
|
|
}
|
|
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "Plugins",
|
|
Id: "plugins",
|
|
Description: "View and configure plugins",
|
|
Icon: "gicon gicon-plugins",
|
|
Url: setting.AppSubUrl + "/plugins",
|
|
})
|
|
|
|
if c.OrgRole == m.ROLE_ADMIN {
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "Preferences",
|
|
Id: "org-settings",
|
|
Description: "Organization preferences",
|
|
Icon: "gicon gicon-preferences",
|
|
Url: setting.AppSubUrl + "/org",
|
|
})
|
|
configNodes = append(configNodes, &dtos.NavLink{
|
|
Text: "API Keys",
|
|
Id: "apikeys",
|
|
Description: "Create & manage API keys",
|
|
Icon: "gicon gicon-apikeys",
|
|
Url: setting.AppSubUrl + "/org/apikeys",
|
|
})
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Id: "cfg",
|
|
Text: "Configuration",
|
|
SubTitle: "Organization: " + c.OrgName,
|
|
Icon: "gicon gicon-cog",
|
|
Url: configNodes[0].Url,
|
|
SortWeight: dtos.WeightConfig,
|
|
Children: configNodes,
|
|
})
|
|
|
|
if c.IsGrafanaAdmin {
|
|
adminNavLinks := []*dtos.NavLink{
|
|
{Text: "Users", Id: "global-users", Url: setting.AppSubUrl + "/admin/users", Icon: "gicon gicon-user"},
|
|
{Text: "Orgs", Id: "global-orgs", Url: setting.AppSubUrl + "/admin/orgs", Icon: "gicon gicon-org"},
|
|
{Text: "Settings", Id: "server-settings", Url: setting.AppSubUrl + "/admin/settings", Icon: "gicon gicon-preferences"},
|
|
{Text: "Stats", Id: "server-stats", Url: setting.AppSubUrl + "/admin/stats", Icon: "fa fa-fw fa-bar-chart"},
|
|
}
|
|
|
|
if setting.LDAPEnabled {
|
|
adminNavLinks = append(adminNavLinks, &dtos.NavLink{
|
|
Text: "LDAP", Id: "ldap", Url: setting.AppSubUrl + "/admin/ldap", Icon: "fa fa-fw fa-address-book-o",
|
|
})
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Server Admin",
|
|
SubTitle: "Manage all users & orgs",
|
|
HideFromTabs: true,
|
|
Id: "admin",
|
|
Icon: "gicon gicon-shield",
|
|
Url: setting.AppSubUrl + "/admin/users",
|
|
SortWeight: dtos.WeightAdmin,
|
|
Children: adminNavLinks,
|
|
})
|
|
}
|
|
|
|
data.NavTree = append(data.NavTree, &dtos.NavLink{
|
|
Text: "Help",
|
|
SubTitle: fmt.Sprintf(`%s v%s (%s)`, setting.ApplicationName, setting.BuildVersion, setting.BuildCommit),
|
|
Id: "help",
|
|
Url: "#",
|
|
Icon: "gicon gicon-question",
|
|
HideFromMenu: true,
|
|
SortWeight: dtos.WeightHelp,
|
|
Children: []*dtos.NavLink{
|
|
{Text: "Keyboard shortcuts", Url: "/shortcuts", Icon: "fa fa-fw fa-keyboard-o", Target: "_self"},
|
|
{Text: "Community site", Url: "http://community.grafana.com", Icon: "fa fa-fw fa-comment", Target: "_blank"},
|
|
{Text: "Documentation", Url: "http://docs.grafana.org", Icon: "fa fa-fw fa-file", Target: "_blank"},
|
|
},
|
|
})
|
|
|
|
hs.HooksService.RunIndexDataHooks(&data)
|
|
|
|
sort.SliceStable(data.NavTree, func(i, j int) bool {
|
|
return data.NavTree[i].SortWeight < data.NavTree[j].SortWeight
|
|
})
|
|
return &data, nil
|
|
}
|
|
|
|
func (hs *HTTPServer) Index(c *m.ReqContext) {
|
|
data, err := hs.setIndexViewData(c)
|
|
if err != nil {
|
|
c.Handle(500, "Failed to get settings", err)
|
|
return
|
|
}
|
|
c.HTML(200, "index", data)
|
|
}
|
|
|
|
func (hs *HTTPServer) NotFoundHandler(c *m.ReqContext) {
|
|
if c.IsApiRequest() {
|
|
c.JsonApiErr(404, "Not found", nil)
|
|
return
|
|
}
|
|
|
|
data, err := hs.setIndexViewData(c)
|
|
if err != nil {
|
|
c.Handle(500, "Failed to get settings", err)
|
|
return
|
|
}
|
|
|
|
c.HTML(404, "index", data)
|
|
}
|
|
|
|
func getAppNameBodyClass(validLicense bool) string {
|
|
if validLicense {
|
|
return "app-enterprise"
|
|
}
|
|
|
|
return "app-grafana"
|
|
}
|