Compare commits

...

5 Commits

Author SHA1 Message Date
Develer 8a01197f27 refactor(sql-expr): rename backendQueries to nonDashboardQueries 2026-01-14 19:53:47 +01:00
Alyssa Joyner 1d3f09d519 [InfluxDB]: Remove banner (#116141) 2026-01-13 08:32:09 -07:00
Alex Khomenko ec1ace398e Recent dashboards: Add experimental toggle (#116121)
* Add experimentRecentlyViewedDashboards toggle

* Emit dashboards_browse_list_viewed event

* Move feature toggle to parent

* merge
2026-01-13 17:22:20 +02:00
Yunwen Zheng fe5aa3e281 RecentlyViewedDashboards: UI tweaks (#116171) 2026-01-13 10:20:17 -05:00
Jo a01777eafa docs: improve RBAC and role creation documentation (#116188)
* docs: improve RBAC and role creation documentation

- Clarify that file-based RBAC provisioning is for self-managed instances only
- Distinguish between Grafana Admin (Server Admin) and Org Admin
- Remove incorrect UI instructions for custom role creation
- Add Terraform example for creating custom roles and assignments

* Apply suggestions from code review

Co-authored-by: Anna Urbiztondo <anna.urbiztondo@grafana.com>

---------

Co-authored-by: Anna Urbiztondo <anna.urbiztondo@grafana.com>
2026-01-13 15:11:15 +00:00
15 changed files with 204 additions and 121 deletions
@@ -35,10 +35,10 @@ For Grafana Cloud users, Grafana Support is not authorised to make org role chan
## Grafana server administrators
A Grafana server administrator manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
A Grafana server administrator (sometimes referred to as a **Grafana Admin**) manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
{{< admonition type="note" >}}
The server administrator role does not mean that the user is also a Grafana [organization administrator](#organization-roles).
{{< admonition type="caution" >}}
The server administrator role is distinct from the [organization administrator](#organization-roles) role.
{{< /admonition >}}
A server administrator can perform the following tasks:
@@ -50,7 +50,7 @@ A server administrator can perform the following tasks:
- Upgrade the server to Grafana Enterprise.
{{< admonition type="note" >}}
The server administrator role does not exist in Grafana Cloud.
The server administrator (Grafana Admin) role does not exist in Grafana Cloud.
{{< /admonition >}}
To assign or remove server administrator privileges, see [Server user management](../user-management/server-user-management/assign-remove-server-admin-privileges/).
@@ -53,6 +53,11 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/
rbac-terraform-provisioning:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-terraform-provisioning/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/rbac-terraform-provisioning/
rbac-grafana-provisioning:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/
@@ -145,7 +150,13 @@ Refer to the [RBAC HTTP API](ref:api-rbac-get-a-role) for more details.
## Create custom roles
This section shows you how to create a custom RBAC role using Grafana provisioning and the HTTP API.
This section shows you how to create a custom RBAC role using Grafana provisioning or the HTTP API.
Creating and editing custom roles is not currently possible in the Grafana UI. To manage custom roles, use one of the following methods:
- [Provisioning](ref:rbac-grafana-provisioning) (for self-managed instances)
- [HTTP API](ref:api-rbac-create-a-new-custom-role)
- [Terraform](ref:rbac-terraform-provisioning)
Create a custom role when basic roles and fixed roles do not meet your permissions requirements.
@@ -153,14 +164,101 @@ Create a custom role when basic roles and fixed roles do not meet your permissio
- [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy).
- Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions, actions, and scopes](ref:custom-role-actions-scopes).
- [Enable role provisioning](ref:rbac-grafana-provisioning).
- Ensure that you have permissions to create a custom role.
- By default, the Grafana Admin role has permission to create custom roles.
- A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope.
### Create custom roles using provisioning
### Create custom roles using the HTTP API
[File-based provisioning](ref:rbac-grafana-provisioning) is one method you can use to create custom roles.
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
{{< admonition type="note" >}}
When you create a custom role you can only give it the same permissions you already have. For example, if you only have `users:create` permissions, then you can't create a role that includes other permissions.
{{< /admonition >}}
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
**Example request**
```
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
}
]
}'
```
**Example response**
```
{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
"updated": "2021-05-17T22:07:31.569936+02:00",
"created": "2021-05-17T22:07:31.569935+02:00"
}
],
"updated": "2021-05-17T22:07:31.564403+02:00",
"created": "2021-05-17T22:07:31.564403+02:00"
}
```
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
### Create custom roles using Terraform
You can use the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) to manage custom roles and their assignments. This is the recommended method for Grafana Cloud users who want to manage RBAC as code. For more information, refer to [Provisioning RBAC with Terraform](ref:rbac-terraform-provisioning).
The following example creates a custom role and assigns it to a team:
```terraform
resource "grafana_role" "custom_folder_manager" {
name = "custom:folders:manager"
description = "Custom role for reading and creating folders"
uid = "custom-folders-manager"
version = 1
global = true
permissions {
action = "folders:read"
scope = "folders:*"
}
permissions {
action = "folders:create"
scope = "folders:uid:general" # Allows creating folders at the root level
}
}
resource "grafana_role_assignment" "custom_folder_manager_assignment" {
role_uid = grafana_role.custom_folder_manager.uid
teams = ["<TEAM_UID>"]
}
```
For more information, refer to the [`grafana_role`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role) and [`grafana_role_assignment`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role_assignment) documentation in the Terraform Registry.
### Create custom roles using file-based provisioning
You can use [file-based provisioning](ref:rbac-grafana-provisioning) to create custom roles for self-managed instances.
1. Open the YAML configuration file and locate the `roles` section.
@@ -251,61 +349,6 @@ roles:
state: 'absent'
```
### Create custom roles using the HTTP API
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
{{< admonition type="note" >}}
You cannot create a custom role with permissions that you do not have. For example, if you only have `users:create` permissions, then you cannot create a role that includes other permissions.
{{< /admonition >}}
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
**Example request**
```
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
}
]
}'
```
**Example response**
```
{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
"updated": "2021-05-17T22:07:31.569936+02:00",
"created": "2021-05-17T22:07:31.569935+02:00"
}
],
"updated": "2021-05-17T22:07:31.564403+02:00",
"created": "2021-05-17T22:07:31.564403+02:00"
}
```
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
## Update basic role permissions
If the default basic role definitions do not meet your requirements, you can change their permissions.
@@ -6,7 +6,6 @@ description: Learn about RBAC Grafana provisioning and view an example YAML prov
file that configures Grafana role assignments.
labels:
products:
- cloud
- enterprise
menuTitle: Provisioning RBAC with Grafana
title: Provisioning RBAC with Grafana
@@ -52,11 +51,13 @@ refs:
# Provisioning RBAC with Grafana
{{< admonition type="note" >}}
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud).
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) for self-managed instances. This feature is not available in Grafana Cloud.
{{< /admonition >}}
You can create, change or remove [Custom roles](ref:manage-rbac-roles-create-custom-roles-using-provisioning) and create or remove [basic role assignments](ref:assign-rbac-roles-assign-a-fixed-role-to-a-basic-role-using-provisioning), by adding one or more YAML configuration files in the `provisioning/access-control/` directory.
Because this method requires access to the file system where Grafana is running, it's only available for self-managed Grafana instances. To provision RBAC in Grafana Cloud, use [Terraform](ref:rbac-terraform-provisioning) or the [HTTP API](ref:api-rbac-create-and-manage-custom-roles).
Grafana performs provisioning during startup. After you make a change to the configuration file, you can reload it during runtime. You do not need to restart the Grafana server for your changes to take effect.
**Before you begin:**
+5
View File
@@ -984,6 +984,11 @@ export interface FeatureToggles {
*/
recentlyViewedDashboards?: boolean;
/**
* A/A test for recently viewed dashboards feature
* @default false
*/
experimentRecentlyViewedDashboards?: boolean;
/**
* Enable configuration of alert enrichments in Grafana Cloud.
* @default false
*/
+9
View File
@@ -1625,6 +1625,15 @@ var (
FrontendOnly: true,
Expression: "false",
},
{
Name: "experimentRecentlyViewedDashboards",
Description: "A/A test for recently viewed dashboards feature",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendSearchNavOrganise,
FrontendOnly: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertEnrichment",
Description: "Enable configuration of alert enrichments in Grafana Cloud.",
+1
View File
@@ -223,6 +223,7 @@ kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,f
kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false
restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false
recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
experimentRecentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false
alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
223 kubernetesExternalGroupMapping experimental @grafana/identity-access-team false false false
224 restoreDashboards experimental @grafana/grafana-search-navigate-organise false false false
225 recentlyViewedDashboards experimental @grafana/grafana-search-navigate-organise false false true
226 experimentRecentlyViewedDashboards experimental @grafana/grafana-search-navigate-organise false false true
227 alertEnrichment experimental @grafana/alerting-squad false false false
228 alertEnrichmentMultiStep experimental @grafana/alerting-squad false false false
229 alertEnrichmentConditional experimental @grafana/alerting-squad false false false
+15
View File
@@ -1365,6 +1365,21 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "experimentRecentlyViewedDashboards",
"resourceVersion": "1768214542023",
"creationTimestamp": "2026-01-12T10:42:22Z"
},
"spec": {
"description": "A/A test for recently viewed dashboards feature",
"stage": "experimental",
"codeowner": "@grafana/grafana-search-navigate-organise",
"frontend": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "exploreLogsAggregatedMetrics",
@@ -1,11 +1,12 @@
import { css } from '@emotion/css';
import { memo, useEffect, useMemo } from 'react';
import { memo, useEffect, useMemo, useRef } from 'react';
import { useLocation, useParams } from 'react-router-dom-v5-compat';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
import { Page } from 'app/core/components/Page/Page';
@@ -44,6 +45,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
const location = useLocation();
const search = useMemo(() => new URLSearchParams(location.search), [location.search]);
const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: folderUID });
const isRecentlyViewedEnabled = !folderUID && evaluateBooleanFlag('recentlyViewedDashboards', false);
useEffect(() => {
stateManager.initStateFromUrl(folderUID);
@@ -73,6 +75,23 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
}
}, [isSearching, searchState.result, stateManager]);
// Emit exposure event for A/A test once when page loads
const hasEmittedExposureEvent = useRef(false);
useEffect(() => {
if (!isRecentlyViewedEnabled || hasEmittedExposureEvent.current) {
return;
}
hasEmittedExposureEvent.current = true;
const isExperimentTreatment = evaluateBooleanFlag('experimentRecentlyViewedDashboards', false);
reportInteraction('dashboards_browse_list_viewed', {
experiment_dashboard_list_recently_viewed: isExperimentTreatment ? 'treatment' : 'control',
has_recently_viewed_component: isExperimentTreatment,
});
}, [isRecentlyViewedEnabled]);
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
const [saveFolder] = useUpdateFolder();
const navModel = useMemo(() => {
@@ -179,8 +198,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
>
<Page.Contents className={styles.pageContents}>
<ProvisionedFolderPreviewBanner queryParams={queryParams} />
{/* only show recently viewed dashboards when in root */}
{!folderUID && <RecentlyViewedDashboards />}
{/* only show recently viewed dashboards when in root and flag is enabled */}
{isRecentlyViewedEnabled && <RecentlyViewedDashboards />}
<div>
<FilterInput
placeholder={getSearchPlaceholder(searchState.includePanels)}
@@ -5,7 +5,6 @@ import { useAsyncRetry } from 'react-use';
import { GrafanaTheme2, store } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
@@ -28,9 +27,6 @@ export function RecentlyViewedDashboards() {
retry,
error,
} = useAsyncRetry(async () => {
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
return [];
}
return getRecentlyViewedDashboards(MAX_RECENT);
}, []);
const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0);
@@ -48,7 +44,7 @@ export function RecentlyViewedDashboards() {
setIsOpen(!isOpen);
};
if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
if (recentDashboards.length === 0) {
return null;
}
@@ -123,6 +119,7 @@ const getStyles = (theme: GrafanaTheme2) => {
color: 'transparent',
cursor: 'pointer',
},
padding: 0,
}),
content: css({
paddingTop: theme.spacing(0),
@@ -11,9 +11,9 @@ describe('isDashboardDatasource', () => {
{ refId: 'C', datasource: { uid: 'mysql-uid', type: 'mysql' } },
];
const backendQueries = queries.filter((q) => !isDashboardDatasource(q));
const nonDashboardQueries = queries.filter((q) => !isDashboardDatasource(q));
expect(backendQueries.map((q) => q.refId)).toEqual(['A', 'C']);
expect(nonDashboardQueries.map((q) => q.refId)).toEqual(['A', 'C']);
});
it('returns true when query has dashboard datasource uid', () => {
@@ -67,9 +67,9 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
try {
// Filter out Dashboard datasource queries - they are frontend-only and can't be processed by backend
const backendQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
const nonDashboardQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
if (backendQueries.length === 0) {
if (nonDashboardQueries.length === 0) {
setSchemas({ kind: 'SQLSchemaResponse', apiVersion: 'query.grafana.app/v0alpha1', sqlSchemas: {} });
setLoading(false);
return;
@@ -81,7 +81,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
const response = await getBackendSrv().post<SQLSchemasResponse>(
`/apis/query.grafana.app/v0alpha1/namespaces/${namespace}/sqlschemas/name`,
{
queries: backendQueries,
queries: nonDashboardQueries,
from: currentTimeRange.from.toISOString(),
to: currentTimeRange.to.toISOString(),
}
@@ -36,9 +36,4 @@ describe('ConfigEditor', () => {
expect(screen.getByTestId('url-auth-section')).toBeInTheDocument();
expect(screen.getByTestId('db-connection-section')).toBeInTheDocument();
});
it('shows the informational alert', () => {
render(<ConfigEditor {...defaultProps} />);
expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument();
});
});
@@ -2,13 +2,12 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Alert, Box, Stack, TextLink, Text, useStyles2 } from '@grafana/ui';
import { Box, Stack, Text, useStyles2 } from '@grafana/ui';
import { DatabaseConnectionSection } from './DatabaseConnectionSection';
import { LeftSideBar } from './LeftSideBar';
import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection';
import { CONTAINER_MIN_WIDTH } from './constants';
import { trackInfluxDBConfigV2FeedbackButtonClicked } from './tracking';
import { Props } from './types';
export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Props) => {
@@ -22,22 +21,6 @@ export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Prop
</div>
<Box width="60%" flex="1 1 auto" minWidth={CONTAINER_MIN_WIDTH}>
<Stack direction="column">
<Alert
severity="info"
title="You are viewing a new design for the InfluxDB configuration settings."
className={styles.alertHeight}
>
<>
<TextLink
href="https://docs.google.com/forms/d/e/1FAIpQLSdi-zyX3c51vh937UKhNYYxhljUnFi6dQSlZv50mES9NrK-ig/viewform"
external
onClick={trackInfluxDBConfigV2FeedbackButtonClicked}
>
Share your thoughts
</TextLink>{' '}
to help us make it even better.
</>
</Alert>
<Text variant="bodySmall" color="secondary">
Fields marked with * are required
</Text>
@@ -1,3 +1,5 @@
import { truncate } from 'lodash';
import { reportInteraction } from '@grafana/runtime';
import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
import { LocationInfo } from 'app/features/search/service/types';
@@ -25,6 +27,7 @@ export function DashListItem({
onStarChange,
}: Props) {
const css = useStyles2(getStyles);
const shortTitle = truncate(dashboard.name, { length: 40, omission: '…' });
const onCardLinkClick = () => {
reportInteraction('grafana_recently_viewed_dashboards_click_card', {
@@ -54,27 +57,35 @@ export function DashListItem({
</div>
) : (
<Card className={css.dashlistCard} noMargin>
<Stack justifyContent="space-between" alignItems="center">
<Link href={url} onClick={onCardLinkClick}>
{dashboard.name}
</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</Stack>
{showFolderNames && locationInfo && (
<Stack alignItems="center" direction="row" gap={0}>
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
<Stack direction="column" justifyContent="space-between" height="100%">
<Stack justifyContent="space-between" alignItems="start">
<Link
className={css.dashlistCardLink}
href={url}
aria-label={dashboard.name}
title={dashboard.name}
onClick={onCardLinkClick}
>
{shortTitle}
</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</Stack>
)}
{showFolderNames && locationInfo && (
<Stack alignItems="start" direction="row" gap={0.5} height="25%">
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
</Stack>
)}
</Stack>
</Card>
)}
</>
@@ -32,6 +32,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
textDecoration: 'underline',
},
height: '100%',
paddingTop: theme.spacing(1.5),
'&:hover': {
backgroundImage: gradient,
@@ -41,5 +42,8 @@ export const getStyles = (theme: GrafanaTheme2) => {
dashlistCardIcon: css({
marginRight: theme.spacing(0.5),
}),
dashlistCardLink: css({
paddingTop: theme.spacing(0.5),
}),
};
};