- Access mode controls how requests to the data source will be handled.
-
- Server
- {' '}
- should be the preferred way if nothing else is stated.
-
-
Server access mode (Default):
-
- All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to
- the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs
- to be accessible from the grafana backend/server if you select this access mode.
-
-
Browser access mode:
-
- All requests will be made from the browser directly to the data source and may be subject to Cross-Origin Resource
- Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access mode.
-
+ Access mode controls how requests to the data source will be handled.
+
+ Server
+ {' '}
+ should be the preferred way if nothing else is stated.
+
+
Server access mode (Default):
+
+ All requests will be made from the browser to Grafana backend/server which in turn will forward the requests to
+ the data source and by that circumvent possible Cross-Origin Resource Sharing (CORS) requirements. The URL needs
+ to be accessible from the grafana backend/server if you select this access mode.
+
+
Browser access mode:
+
+ All requests will be made from the browser directly to the data source and may be subject to Cross-Origin
+ Resource Sharing (CORS) requirements. The URL needs to be accessible from the browser if you select this access
+ mode.
+
+
+ );
+};
+
+const getAccessStyles = (theme: GrafanaTheme2) => ({
+ infoBox: css({
+ marginTop: theme.spacing(3),
+ }),
+});
const LABEL_WIDTH = 26;
diff --git a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
index 791adfa5391..377318481c3 100644
--- a/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
+++ b/packages/grafana-ui/src/themes/GlobalStyles/GlobalStyles.tsx
@@ -5,8 +5,10 @@ import { useTheme2 } from '../ThemeContext';
import { getAgularPanelStyles } from './angularPanelStyles';
import { getCardStyles } from './card';
+import { getCodeStyles } from './code';
import { getElementStyles } from './elements';
import { getExtraStyles } from './extra';
+import { getFontStyles } from './fonts';
import { getFormElementStyles } from './forms';
import { getLegacySelectStyles } from './legacySelect';
import { getMarkdownStyles } from './markdownStyles';
@@ -22,8 +24,10 @@ export function GlobalStyles() {
return (
0 {
- adminNodeLinks = append(adminNodeLinks, generalNode)
- }
-
- if len(pluginsNode.Children) > 0 {
- adminNodeLinks = append(adminNodeLinks, pluginsNode)
- }
-
- if len(usersNode.Children) > 0 {
- adminNodeLinks = append(adminNodeLinks, usersNode)
- }
-
- authenticationNode := root.FindById("authentication")
- if authenticationNode != nil {
- authenticationNode.IsSection = true
- adminNodeLinks = append(adminNodeLinks, authenticationNode)
- }
-
- costManagementNode := root.FindById("plugin-page-grafana-costmanagementui-app")
-
- if costManagementNode != nil {
- adminNodeLinks = append(adminNodeLinks, costManagementNode)
- }
-
- costManagementMetricsNode := root.FindByURL("/a/grafana-costmanagementui-app/metrics")
- adaptiveMetricsNode := root.FindById("plugin-page-grafana-adaptive-metrics-app")
-
- if costManagementMetricsNode != nil && adaptiveMetricsNode != nil {
- costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, adaptiveMetricsNode)
- }
-
- attributionsNode := root.FindById("plugin-page-grafana-attributions-app")
-
- if costManagementMetricsNode != nil && attributionsNode != nil {
- costManagementMetricsNode.Children = append(costManagementMetricsNode.Children, attributionsNode)
- }
-
- costManagementLogsNode := root.FindByURL("/a/grafana-costmanagementui-app/logs")
- logVolumeExplorerNode := root.FindById("plugin-page-grafana-logvolumeexplorer-app")
-
- if costManagementLogsNode != nil && logVolumeExplorerNode != nil {
- costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerNode)
- }
-
- if len(adminNodeLinks) > 0 {
- orgAdminNode.Children = adminNodeLinks
- } else {
- root.RemoveSection(orgAdminNode)
+ costManagementLogsNode := FindByURL(costManagementApp.Children, "/a/grafana-costmanagementui-app/logs")
+ if costManagementLogsNode != nil {
+ if logVolumeExplorerApp != nil {
+ costManagementLogsNode.Children = append(costManagementLogsNode.Children, logVolumeExplorerApp)
+ }
+ }
+ adminNodeLinks = append(adminNodeLinks, costManagementApp)
}
+ orgAdminNode.Children = adminNodeLinks
}
}
diff --git a/pkg/services/navtree/navtreeimpl/admin.go b/pkg/services/navtree/navtreeimpl/admin.go
index 5dad84f681a..a81542fbb18 100644
--- a/pkg/services/navtree/navtreeimpl/admin.go
+++ b/pkg/services/navtree/navtreeimpl/admin.go
@@ -21,60 +21,9 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
orgsAccessEvaluator := ac.EvalPermission(ac.ActionOrgsRead)
authConfigUIAvailable := s.license.FeatureEnabled(social.SAMLProviderName) || s.cfg.LDAPAuthEnabled
- // FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why
- // while we don't have a permissions for listing plugins the legacy check has to stay as a default
- if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "Plugins",
- Id: "plugins",
- SubTitle: "Extend the Grafana experience with plugins",
- Icon: "plug",
- Url: s.cfg.AppSubURL + "/plugins",
- })
- }
-
- if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
- })
- }
-
- if hasAccess(ac.TeamsAccessEvaluator) {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "Teams",
- Id: "teams",
- SubTitle: "Groups of users that have common dashboard and permission needs",
- Icon: "users-alt",
- Url: s.cfg.AppSubURL + "/org/teams",
- })
- }
-
- if enableServiceAccount(s, c) {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "Service accounts",
- Id: "serviceaccounts",
- SubTitle: "Use service accounts to run automated workloads in Grafana",
- Icon: "gf-service-account",
- Url: s.cfg.AppSubURL + "/org/serviceaccounts",
- })
- }
-
- disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID())
- if err != nil {
- return nil, err
- }
- if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "API keys",
- Id: "apikeys",
- SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
- Icon: "key-skeleton-alt",
- Url: s.cfg.AppSubURL + "/org/apikeys",
- })
- }
-
+ generalNodeLinks := []*navtree.NavLink{}
if hasAccess(ac.OrgPreferencesAccessEvaluator) {
- configNodes = append(configNodes, &navtree.NavLink{
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Default preferences",
Id: "org-settings",
SubTitle: "Manage preferences across an organization",
@@ -82,32 +31,18 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Url: s.cfg.AppSubURL + "/org",
})
}
-
- if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) ||
- (hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) {
- configNodes = append(configNodes, &navtree.NavLink{
- Text: "Authentication",
- Id: "authentication",
- SubTitle: "Manage your auth settings and configure single sign-on",
- Icon: "signin",
- Url: s.cfg.AppSubURL + "/admin/authentication",
- })
- }
-
if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) {
- configNodes = append(configNodes, &navtree.NavLink{
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Settings", SubTitle: "View the settings defined in your Grafana config", Id: "server-settings", Url: s.cfg.AppSubURL + "/admin/settings", Icon: "sliders-v-alt",
})
}
-
if hasGlobalAccess(orgsAccessEvaluator) {
- configNodes = append(configNodes, &navtree.NavLink{
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Organizations", SubTitle: "Isolated instances of Grafana running on the same server", Id: "global-orgs", Url: s.cfg.AppSubURL + "/admin/orgs", Icon: "building",
})
}
-
if s.features.IsEnabled(ctx, featuremgmt.FlagFeatureToggleAdminPage) && hasAccess(ac.EvalPermission(ac.ActionFeatureManagementRead)) {
- configNodes = append(configNodes, &navtree.NavLink{
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
Text: "Feature Toggles",
SubTitle: "View and edit feature toggles",
Id: "feature-toggles",
@@ -115,9 +50,51 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
Icon: "toggle-on",
})
}
+ if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) {
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
+ Text: "Storage",
+ Id: "storage",
+ SubTitle: "Manage file storage",
+ Icon: "cube",
+ Url: s.cfg.AppSubURL + "/admin/storage",
+ })
+ }
+ if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) {
+ generalNodeLinks = append(generalNodeLinks, &navtree.NavLink{
+ Text: "Migrate to Grafana Cloud",
+ Id: "migrate-to-cloud",
+ SubTitle: "Copy configuration from your self-managed installation to a cloud stack",
+ Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud",
+ })
+ }
+ generalNode := &navtree.NavLink{
+ Text: "General",
+ SubTitle: "Manage default preferences and settings across Grafana",
+ Id: navtree.NavIDCfgGeneral,
+ Url: "/admin/general",
+ Icon: "shield",
+ Children: generalNodeLinks,
+ }
+
+ if len(generalNode.Children) > 0 {
+ configNodes = append(configNodes, generalNode)
+ }
+
+ pluginsNodeLinks := []*navtree.NavLink{}
+ // FIXME: If plugin admin is disabled or externally managed, server admins still need to access the page, this is why
+ // while we don't have a permissions for listing plugins the legacy check has to stay as a default
+ if pluginaccesscontrol.ReqCanAdminPlugins(s.cfg)(c) || hasAccess(pluginaccesscontrol.AdminAccessEvaluator) {
+ pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
+ Text: "Plugins",
+ Id: "plugins",
+ SubTitle: "Extend the Grafana experience with plugins",
+ Icon: "plug",
+ Url: s.cfg.AppSubURL + "/plugins",
+ })
+ }
if s.features.IsEnabled(ctx, featuremgmt.FlagCorrelations) && hasAccess(correlations.ConfigurationPageAccess) {
- configNodes = append(configNodes, &navtree.NavLink{
+ pluginsNodeLinks = append(pluginsNodeLinks, &navtree.NavLink{
Text: "Correlations",
Icon: "gf-glue",
SubTitle: "Add and configure correlations",
@@ -126,25 +103,80 @@ func (s *ServiceImpl) getAdminNode(c *contextmodel.ReqContext) (*navtree.NavLink
})
}
- if hasAccess(ac.EvalPermission(ac.ActionSettingsRead, ac.ScopeSettingsAll)) && s.features.IsEnabled(ctx, featuremgmt.FlagStorage) {
- storage := &navtree.NavLink{
- Text: "Storage",
- Id: "storage",
- SubTitle: "Manage file storage",
- Icon: "cube",
- Url: s.cfg.AppSubURL + "/admin/storage",
- }
- configNodes = append(configNodes, storage)
+ pluginsNode := &navtree.NavLink{
+ Text: "Plugins and data",
+ SubTitle: "Install plugins and define the relationships between data",
+ Id: navtree.NavIDCfgPlugins,
+ Url: "/admin/plugins",
+ Icon: "shield",
+ Children: pluginsNodeLinks,
}
- if s.features.IsEnabled(ctx, featuremgmt.FlagOnPremToCloudMigrations) && c.SignedInUser.HasRole(org.RoleAdmin) {
- migrateToCloud := &navtree.NavLink{
- Text: "Migrate to Grafana Cloud",
- Id: "migrate-to-cloud",
- SubTitle: "Copy configuration from your self-managed installation to a cloud stack",
- Url: s.cfg.AppSubURL + "/admin/migrate-to-cloud",
- }
- configNodes = append(configNodes, migrateToCloud)
+ if len(pluginsNode.Children) > 0 {
+ configNodes = append(configNodes, pluginsNode)
+ }
+
+ accessNodeLinks := []*navtree.NavLink{}
+ if hasAccess(ac.EvalAny(ac.EvalPermission(ac.ActionOrgUsersRead), ac.EvalPermission(ac.ActionUsersRead, ac.ScopeGlobalUsersAll))) {
+ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
+ Text: "Users", SubTitle: "Manage users in Grafana", Id: "global-users", Url: s.cfg.AppSubURL + "/admin/users", Icon: "user",
+ })
+ }
+ if hasAccess(ac.TeamsAccessEvaluator) {
+ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
+ Text: "Teams",
+ Id: "teams",
+ SubTitle: "Groups of users that have common dashboard and permission needs",
+ Icon: "users-alt",
+ Url: s.cfg.AppSubURL + "/org/teams",
+ })
+ }
+ if enableServiceAccount(s, c) {
+ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
+ Text: "Service accounts",
+ Id: "serviceaccounts",
+ SubTitle: "Use service accounts to run automated workloads in Grafana",
+ Icon: "gf-service-account",
+ Url: s.cfg.AppSubURL + "/org/serviceaccounts",
+ })
+ }
+ disabled, err := s.apiKeyService.IsDisabled(ctx, c.SignedInUser.GetOrgID())
+ if err != nil {
+ return nil, err
+ }
+ if hasAccess(ac.ApiKeyAccessEvaluator) && !disabled {
+ accessNodeLinks = append(accessNodeLinks, &navtree.NavLink{
+ Text: "API keys",
+ Id: "apikeys",
+ SubTitle: "Manage and create API keys that are used to interact with Grafana HTTP APIs",
+ Icon: "key-skeleton-alt",
+ Url: s.cfg.AppSubURL + "/org/apikeys",
+ })
+ }
+
+ usersNode := &navtree.NavLink{
+ Text: "Users and access",
+ SubTitle: "Configure access for individual users, teams, and service accounts",
+ Id: navtree.NavIDCfgAccess,
+ Url: "/admin/access",
+ Icon: "shield",
+ Children: accessNodeLinks,
+ }
+
+ if len(usersNode.Children) > 0 {
+ configNodes = append(configNodes, usersNode)
+ }
+
+ if authConfigUIAvailable && hasAccess(ssoutils.EvalAuthenticationSettings(s.cfg)) ||
+ (hasAccess(ssoutils.OauthSettingsEvaluator(s.cfg)) && s.features.IsEnabled(ctx, featuremgmt.FlagSsoSettingsApi)) {
+ configNodes = append(configNodes, &navtree.NavLink{
+ Text: "Authentication",
+ Id: "authentication",
+ SubTitle: "Manage your auth settings and configure single sign-on",
+ Icon: "signin",
+ IsSection: true,
+ Url: s.cfg.AppSubURL + "/admin/authentication",
+ })
}
configNode := &navtree.NavLink{
diff --git a/pkg/services/navtree/navtreeimpl/applinks.go b/pkg/services/navtree/navtreeimpl/applinks.go
index 5030d033380..06ab60eb520 100644
--- a/pkg/services/navtree/navtreeimpl/applinks.go
+++ b/pkg/services/navtree/navtreeimpl/applinks.go
@@ -295,7 +295,7 @@ func (s *ServiceImpl) readNavigationSettings() {
"grafana-incident-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 2, Text: "Incidents"},
"grafana-ml-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 3, Text: "Machine Learning"},
"grafana-slo-app": {SectionID: navtree.NavIDAlertsAndIncidents, SortWeight: 4},
- "grafana-cloud-link-app": {SectionID: navtree.NavIDCfg},
+ "grafana-cloud-link-app": {SectionID: navtree.NavIDCfgPlugins, SortWeight: 3},
"grafana-costmanagementui-app": {SectionID: navtree.NavIDCfg, Text: "Cost management"},
"grafana-adaptive-metrics-app": {SectionID: navtree.NavIDCfg, Text: "Adaptive Metrics"},
"grafana-attributions-app": {SectionID: navtree.NavIDCfg, Text: "Attributions"},
@@ -307,7 +307,7 @@ func (s *ServiceImpl) readNavigationSettings() {
}
s.navigationAppPathConfig = map[string]NavigationAppConfig{
- "/a/grafana-auth-app": {SectionID: navtree.NavIDCfg, SortWeight: 7},
+ "/a/grafana-auth-app": {SectionID: navtree.NavIDCfgAccess, SortWeight: 2},
}
appSections := s.cfg.Raw.Section("navigation.app_sections")
diff --git a/pkg/services/navtree/navtreeimpl/navtree.go b/pkg/services/navtree/navtreeimpl/navtree.go
index 62d0a07e8c4..4526465de90 100644
--- a/pkg/services/navtree/navtreeimpl/navtree.go
+++ b/pkg/services/navtree/navtreeimpl/navtree.go
@@ -151,7 +151,7 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, prefs *pref.Prefere
orgAdminNode, err := s.getAdminNode(c)
- if orgAdminNode != nil {
+ if orgAdminNode != nil && len(orgAdminNode.Children) > 0 {
treeRoot.AddSection(orgAdminNode)
} else if err != nil {
return nil, err
diff --git a/pkg/services/store/auth.go b/pkg/services/store/auth.go
deleted file mode 100644
index af6fff1551b..00000000000
--- a/pkg/services/store/auth.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package store
-
-import (
- "fmt"
-
- "github.com/grafana/grafana/pkg/services/user"
-)
-
-// Really just spitballing here :) this should hook into a system that can give better display info
-func GetUserIDString(user *user.SignedInUser) string {
- // TODO: should we check IsDisabled?
- // TODO: could we use the NamespacedID.ID() as prefix instead of manually
- // setting "anon", "key", etc.?
- // TODO: the default unauthenticated user is not anonymous and would be
- // returned as `sys:0:` here. We may want to do something special in that
- // case
- if user == nil {
- return ""
- }
- if user.IsAnonymous {
- return "anon"
- }
- if user.ApiKeyID > 0 {
- return fmt.Sprintf("key:%d", user.UserID)
- }
- if user.IsRealUser() {
- return fmt.Sprintf("user:%d:%s", user.UserID, user.Login)
- }
- return fmt.Sprintf("sys:%d:%s", user.UserID, user.Login)
-}
diff --git a/pkg/services/store/entity/sqlstash/utils.go b/pkg/services/store/entity/sqlstash/utils.go
index 84768b3f06d..ade92f40374 100644
--- a/pkg/services/store/entity/sqlstash/utils.go
+++ b/pkg/services/store/entity/sqlstash/utils.go
@@ -9,7 +9,6 @@ import (
"text/template"
"github.com/grafana/grafana/pkg/infra/appcontext"
- "github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity/db"
"github.com/grafana/grafana/pkg/services/store/entity/sqlstash/sqltemplate"
)
@@ -33,7 +32,7 @@ func getCurrentUser(ctx context.Context) (string, error) {
return "", fmt.Errorf("%w: %w", ErrUserNotFoundInContext, err)
}
- return store.GetUserIDString(user), nil
+ return user.GetUID().String(), nil
}
// ptrOr returns the first non-nil pointer in the list or a new non-nil pointer.
diff --git a/pkg/services/store/entity/tests/server_integration_test.go b/pkg/services/store/entity/tests/server_integration_test.go
index 44eec13b401..6afe43b3173 100644
--- a/pkg/services/store/entity/tests/server_integration_test.go
+++ b/pkg/services/store/entity/tests/server_integration_test.go
@@ -11,7 +11,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/infra/appcontext"
- "github.com/grafana/grafana/pkg/services/store"
"github.com/grafana/grafana/pkg/services/store/entity"
)
@@ -122,7 +121,7 @@ func TestIntegrationEntityServer(t *testing.T) {
testCtx := createTestContext(t)
ctx := appcontext.WithUser(testCtx.ctx, testCtx.user)
- fakeUser := store.GetUserIDString(testCtx.user)
+ fakeUser := testCtx.user.GetUID().String()
firstVersion := int64(0)
group := "test.grafana.app"
resource := "jsonobjs"
diff --git a/public/app/features/browse-dashboards/components/CreateNewButton.tsx b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
index c4d20755a72..97af7819b7b 100644
--- a/public/app/features/browse-dashboards/components/CreateNewButton.tsx
+++ b/public/app/features/browse-dashboards/components/CreateNewButton.tsx
@@ -1,7 +1,7 @@
import React, { useState } from 'react';
import { useLocation } from 'react-router-dom';
-import { reportInteraction } from '@grafana/runtime';
+import { config, reportInteraction } from '@grafana/runtime';
import { Button, Drawer, Dropdown, Icon, Menu, MenuItem } from '@grafana/ui';
import {
getNewDashboardPhrase,
@@ -50,11 +50,11 @@ export default function CreateNewButton({ parentFolder, canCreateDashboard, canC
label={getNewDashboardPhrase()}
onClick={() =>
reportInteraction('grafana_menu_item_clicked', {
- url: addFolderUidToUrl('/dashboard/new', parentFolder?.uid),
+ url: buildUrl('/dashboard/new', parentFolder?.uid),
from: location.pathname,
})
}
- url={addFolderUidToUrl('/dashboard/new', parentFolder?.uid)}
+ url={buildUrl('/dashboard/new', parentFolder?.uid)}
/>
)}
{canCreateFolder &&