From 5a74a1a0f6b795e943865f9ac2a473611578b855 Mon Sep 17 00:00:00 2001 From: Todd Treece <360020+toddtreece@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:19:22 -0500 Subject: [PATCH 01/18] Metrics: Use correct gatherer in graphite bridge (#100624) --- pkg/infra/metrics/service.go | 6 ++++-- pkg/infra/metrics/settings.go | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/infra/metrics/service.go b/pkg/infra/metrics/service.go index f704ab9248b..99c1e048d4f 100644 --- a/pkg/infra/metrics/service.go +++ b/pkg/infra/metrics/service.go @@ -26,12 +26,13 @@ func (lw *logWrapper) Println(v ...any) { lw.logger.Info("graphite metric bridge", v...) } -func ProvideService(cfg *setting.Cfg, reg prometheus.Registerer) (*InternalMetricsService, error) { +func ProvideService(cfg *setting.Cfg, reg prometheus.Registerer, gatherer prometheus.Gatherer) (*InternalMetricsService, error) { initMetricVars(reg) initFrontendMetrics(reg) s := &InternalMetricsService{ - Cfg: cfg, + Cfg: cfg, + gatherer: gatherer, } return s, s.readSettings() } @@ -41,6 +42,7 @@ type InternalMetricsService struct { intervalSeconds int64 graphiteCfg *graphitebridge.Config + gatherer prometheus.Gatherer } func (im *InternalMetricsService) Run(ctx context.Context) error { diff --git a/pkg/infra/metrics/settings.go b/pkg/infra/metrics/settings.go index 54715db249e..587956158f2 100644 --- a/pkg/infra/metrics/settings.go +++ b/pkg/infra/metrics/settings.go @@ -5,8 +5,6 @@ import ( "strings" "time" - "github.com/prometheus/client_golang/prometheus" - "github.com/grafana/grafana/pkg/infra/metrics/graphitebridge" ) @@ -40,7 +38,7 @@ func (im *InternalMetricsService) parseGraphiteSettings() error { URL: address, Prefix: graphiteSection.Key("prefix").MustString("prod.grafana.%(instance_name)s"), CountersAsDelta: true, - Gatherer: prometheus.DefaultGatherer, + Gatherer: im.gatherer, Interval: time.Duration(im.intervalSeconds) * time.Second, Timeout: 10 * time.Second, Logger: &logWrapper{logger: metricsLogger}, From 1018aec6bcd0be36f9de4efda325fe189f28ccd3 Mon Sep 17 00:00:00 2001 From: Oscar Kilhed Date: Thu, 13 Feb 2025 16:31:11 +0100 Subject: [PATCH 02/18] Dashboards: Fix repeats not being added on refresh when using searchLayout (#100621) Fix repeats not being added --- .../dashboard-scene/scene/PanelSearchLayout.tsx | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx index 780fab43b99..2026f777a6a 100644 --- a/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx +++ b/public/app/features/dashboard-scene/scene/PanelSearchLayout.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/css'; import classNames from 'classnames'; -import { useEffect } from 'react'; +import { useEffect, useState } from 'react'; import { GrafanaTheme2 } from '@grafana/data'; import { SceneGridRow, VizPanel, sceneGraph } from '@grafana/scenes'; @@ -12,6 +12,7 @@ import { forceActivateFullSceneObjectTree } from '../utils/utils'; import { DashboardScene } from './DashboardScene'; import { DashboardGridItem } from './layout-default/DashboardGridItem'; import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutManager'; +import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export interface Props { dashboard: DashboardScene; @@ -25,6 +26,7 @@ export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: const { body } = dashboard.state; const filteredPanels: VizPanel[] = []; const styles = useStyles2(getStyles); + const [_, setRepeatsUpdated] = useState(''); const bodyGrid = body instanceof DefaultGridLayoutManager ? body.state.grid : null; @@ -34,11 +36,11 @@ export function PanelSearchLayout({ dashboard, panelSearch = '', panelsPerRow }: for (const gridItem of bodyGrid.state.children) { if (gridItem instanceof DashboardGridItem) { - filterPanels(gridItem, dashboard, panelSearch, filteredPanels); + filterPanels(gridItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); } else if (gridItem instanceof SceneGridRow) { for (const rowItem of gridItem.state.children) { if (rowItem instanceof DashboardGridItem) { - filterPanels(rowItem, dashboard, panelSearch, filteredPanels); + filterPanels(rowItem, dashboard, panelSearch, filteredPanels, setRepeatsUpdated); } } } @@ -98,7 +100,8 @@ function filterPanels( gridItem: DashboardGridItem, dashboard: DashboardScene, searchString: string, - filteredPanels: VizPanel[] + filteredPanels: VizPanel[], + setRepeatsUpdated: (updated: string) => void ) { const interpolatedSearchString = sceneGraph.interpolate(dashboard, searchString).toLowerCase(); @@ -107,6 +110,12 @@ function filterPanels( const panel = gridItem.state.body; const interpolatedTitle = panel.interpolate(panel.state.title, undefined, 'text').toLowerCase(); if (interpolatedTitle.includes(interpolatedSearchString)) { + gridItem.subscribeToEvent(DashboardRepeatsProcessedEvent, (event) => { + const source = event.payload.source; + if (source instanceof DashboardGridItem) { + setRepeatsUpdated(event.payload.source.state.key ?? ''); + } + }); gridItem.activate(); } } From 30939fd0e937f67b07f527af3d7e19da936dd43a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Irene=20Rodr=C3=ADguez?= Date: Thu, 13 Feb 2025 16:44:20 +0100 Subject: [PATCH 03/18] Update relrefs (#100626) --- .../explore/correlations-editor-in-explore.md | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/docs/sources/explore/correlations-editor-in-explore.md b/docs/sources/explore/correlations-editor-in-explore.md index 50053226158..dd0a0ce1936 100644 --- a/docs/sources/explore/correlations-editor-in-explore.md +++ b/docs/sources/explore/correlations-editor-in-explore.md @@ -3,6 +3,7 @@ labels: products: - enterprise - oss + - cloud title: Correlations Editor in Explore weight: 20 --- @@ -13,22 +14,22 @@ weight: 20 The Explore editor is available in 10.1 and later versions. In the editor, transformations is available in Grafana 10.3 and later versions. {{% /admonition %}} -Correlations allow users to build a link between any two data sources. For more information about correlations in general, please see the [correlations]({{< relref "../administration/correlations" >}}) topic in the administration page. +Correlations allow users to build a link between any two data sources. For more information about correlations in general, please see the [correlations](/docs/grafana//administration/correlations/) topic in the administration page. ## Create a correlation 1. In Grafana, navigate to the Explore page. -1. Select a data source that you would like to be [the source data source]({{< relref "../administration/correlations/correlation-configuration#source-data-source-and-result-field" >}}) for a new correlation. -1. Run a query producing data in [a supported visualization]({{< relref "../administration/correlations#correlations" >}}). -1. Click **+ Add** in the top toolbar and select **Add correlation** (you can also select **Correlations Editor** from the [Command Palette]({{< relref "../search#command-palette" >}})). +1. Select a data source that you would like to be [the source data source](/docs/grafana//administration/correlations/correlation-configuration/#source-data-source-and-result-field) for a new correlation. +1. Run a query producing data in [a supported visualization](/docs/grafana//administration/correlations/#correlations). +1. Click **+ Add** in the top toolbar and select **Add correlation** (you can also select **Correlations Editor** from the [Command Palette](/docs/grafana//search/#command-palette)). 1. Explore is now in Correlations Editor mode indicated by a blue border and top bar. You can exit Correlations Editor by clicking **Exit** in the top bar. 1. You can now create the following new correlations for the visualization with links that are attached to the data that you can use to build a new query: - Logs: links are displayed next to field values inside log details for each log row - Table: every table cell is a link 1. Click on a link to add a new correlation. - Links are associated with a field that is used as a [result field of a correlation]({{< relref "../administration/correlations/correlation-configuration" >}}). -1. In the split view that opens, use the right pane to set up [the target query source of the correlation]({{< relref "../administration/correlations/correlation-configuration#target-query" >}}). -1. Build a target query using [variables syntax]({{< relref "../dashboards/variables/variable-syntax" >}}) with variables from the list provided at the top of the pane. The list contains sample values from the selected data row. + Links are associated with a field that is used as a [result field of a correlation](/docs/grafana//administration/correlations/correlation-configuration/). +1. In the split view that opens, use the right pane to set up [the target query source of the correlation](/docs/grafana//administration/correlations/correlation-configuration/#target-query). +1. Build a target query using [variables syntax](/docs/grafana//dashboards/variables/variable-syntax/) with variables from the list provided at the top of the pane. The list contains sample values from the selected data row. 1. Provide a label and description (optional). A label will be used as the name of the link inside the visualization and can contain variables. 1. Provide transformations (optional; see below for details). @@ -37,7 +38,7 @@ Correlations allow users to build a link between any two data sources. For more ## Transformations -Transformations allow you to extract values that exist in a field with other data. For example, using a transformation, you can extract one portion of a log line to use in a correlation. For more details on transformations in correlations, see [Correlations]({{< relref "../administration/correlations/correlation-configuration/#correlation-transformations" >}}). +Transformations allow you to extract values that exist in a field with other data. For example, using a transformation, you can extract one portion of a log line to use in a correlation. For more details on transformations in correlations, see [Correlations](/docs/grafana//explore/correlations-editor-in-explore/#transformations). After clicking one of the generated links in the editor mode, you can add transformations by clicking **Add transformation** in the Transformations dropdown menu. @@ -47,7 +48,7 @@ You can use a transformation in your correlation with the following steps: Select the portion of the field that you want to use for the transformation. For example, a log line. Once selected, the value of this field will be used to assist you in building the transformation. 1. Select the type of the transformation. - See [correlations]({{< relref "../administration/correlations/correlation-configuration/#correlation-transformations" >}}) for the options and relevant settings. + See [correlations](/docs/grafana//explore/correlations-editor-in-explore/#transformations) for the options and relevant settings. 1. Based on your selection, you might see one or more variables populate, or you might need to provide more specifications in options that are displayed. 1. Select **Add transformation to correlation** to add the specified variables to the list of available variables. @@ -57,7 +58,7 @@ For regular expressions in this dialog box, the `mapValue` referred to in other ## Correlations examples -The following examples show how to create correlations using the Correlations Editor in Explore. If you'd like to follow these examples, make sure to set up a [test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +The following examples show how to create correlations using the Correlations Editor in Explore. If you'd like to follow these examples, make sure to set up a [test data source](/docs/grafana//datasources/testdata/#testdata-data-source). ### Create a text to graph correlation @@ -65,7 +66,7 @@ This example shows how to create a correlation using Correlations Editor in Expl Correlations allow you to use results of one query to run a new query in any data source. In this example, you will run a query that renders tabular data. The data will be used to run a different query that yields a graph result. -To follow this example, make sure you have set up [a test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +To follow this example, make sure you have set up [a test data source](/docs/grafana//datasources/testdata/#testdata-data-source). 1. In Grafana, navigate to **Explore**. 1. Select the **test data source** from the dropdown menu at the top left of the page. @@ -100,7 +101,7 @@ You can apply the same steps to any data source. Correlations allow you to creat In this example, you will create a correlation to demonstrate how to use transformations to extract values from the log line and another field. -To follow this example, make sure you have set up [a test data source]({{< relref "../datasources/testdata#testdata-data-source" >}}). +To follow this example, make sure you have set up [a test data source](/docs/grafana//datasources/testdata/#testdata-data-source). 1. In Grafana, navigate to **Explore**. 1. Select the **test data source** from the dropdown menu at the top left of the page. From aeb57f671bfacf3cf30eddc24e1b9f489c17a6b8 Mon Sep 17 00:00:00 2001 From: Hugo Kiyodi Oshiro Date: Thu, 13 Feb 2025 16:53:03 +0100 Subject: [PATCH 04/18] Docs: Improve instructions to change basic roles (#100586) --- .../plan-rbac-rollout-strategy/index.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md b/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md index 09c2fc4c3d4..e04c2fcecf2 100644 --- a/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md +++ b/docs/sources/administration/roles-and-permissions/access-control/plan-rbac-rollout-strategy/index.md @@ -369,9 +369,11 @@ Here are two ways to achieve this: # Update the role curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' -H 'Content-Type: application/json' \ - -X PUT-d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' + -X PUT -d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' ``` + The token that is used in this request is the [service account token](ref:service-accounts). + - Or use the `role > from` list and `permission > state` option of your provisioning file: ```yaml @@ -394,6 +396,20 @@ Here are two ways to achieve this: state: 'present' ``` + If your goal is to remove an access to an app you should remove it from the role and update it. For example: + + ```bash + # Fetch the role, modify it to remove permissions to kentik-connect-app and increment role version + curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' \ + -X GET '/api/access-control/roles/basic_viewer' | \ + jq 'del(.created)| del(.updated) | del(.permissions[].created) | del(.permissions[].updated) | .version += 1' | \ + jq 'del(.permissions[] | select (.action == "plugins.app:access" and .scope == "plugins:id:kentik-connect-app"))' + + # Update the role + curl -H 'Authorization: Bearer glsa_kcVxDhZtu5ISOZIEt' -H 'Content-Type: application/json' \ + -X PUT -d @/tmp/basic_viewer.json '/api/access-control/roles/basic_viewer' + ``` + ### Manage user permissions through teams In the scenario where you want users to grant access by the team they belong to, we recommend to set users role to `No Basic Role` and let the team assignment assign the role instead. From 0dab3848267f0aa29c2314f3388df0af3af306c0 Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Thu, 13 Feb 2025 18:55:36 +0300 Subject: [PATCH 05/18] K8s/Frontend: Update watch support (#100631) use watch from gitsync --- public/app/features/apiserver/client.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/public/app/features/apiserver/client.ts b/public/app/features/apiserver/client.ts index 90a5004b8b0..f631aca175a 100644 --- a/public/app/features/apiserver/client.ts +++ b/public/app/features/apiserver/client.ts @@ -1,6 +1,6 @@ import { Observable, from, retry, catchError, filter, map, mergeMap } from 'rxjs'; -import { config, getBackendSrv } from '@grafana/runtime'; +import { BackendSrvRequest, config, getBackendSrv } from '@grafana/runtime'; import { contextSrv } from 'app/core/core'; import { getAPINamespace } from '../../api/utils'; @@ -40,18 +40,26 @@ export class ScopedResourceClient implements return getBackendSrv().get>(`${this.url}/${name}`); } - public watch(opts?: WatchOptions): Observable> { + public watch( + params?: WatchOptions, + config?: Pick + ): Observable> { const decoder = new TextDecoder(); - const params = { - ...opts, + const { name, ...rest } = params ?? {}; // name needs to be added to fieldSelector + const requestParams = { + ...rest, watch: true, - labelSelector: this.parseListOptionsSelector(opts?.labelSelector), - fieldSelector: this.parseListOptionsSelector(opts?.fieldSelector), + labelSelector: this.parseListOptionsSelector(params?.labelSelector), + fieldSelector: this.parseListOptionsSelector(params?.fieldSelector), }; + if (name) { + requestParams.fieldSelector = `metadata.name=${name}`; + } return getBackendSrv() .chunked({ - url: params.name ? `${this.url}/${params.name}` : this.url, - params, + url: this.url, + params: requestParams, + ...config, }) .pipe( filter((response) => response.ok && response.data instanceof Uint8Array), From d719e6c621211116868735ab3df87a94dd0eecf8 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 13 Feb 2025 17:02:51 +0100 Subject: [PATCH 06/18] ServiceAccounts: Fix search in SA picker (#100634) --- public/app/core/components/Select/ServiceAccountPicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/Select/ServiceAccountPicker.tsx b/public/app/core/components/Select/ServiceAccountPicker.tsx index 28c8e3441d2..b1c0e476891 100644 --- a/public/app/core/components/Select/ServiceAccountPicker.tsx +++ b/public/app/core/components/Select/ServiceAccountPicker.tsx @@ -38,7 +38,7 @@ export class ServiceAccountPicker extends Component { } return getBackendSrv() - .get(`/api/serviceaccounts/search`) + .get(`/api/serviceaccounts/search?query=${query}&perpage=100`) .then((result: ServiceAccountsState) => { return result.serviceAccounts.map((sa) => ({ id: sa.id, From 90eb499b781ca94ed39390f93515aa543c25b08d Mon Sep 17 00:00:00 2001 From: Gabriel MABILLE Date: Thu, 13 Feb 2025 17:17:14 +0100 Subject: [PATCH 07/18] PublicDashboards: Fetch dashboard as Grafana (#100344) --- pkg/apimachinery/identity/context.go | 2 + .../publicdashboards/service/query.go | 104 +--- .../publicdashboards/service/query_test.go | 457 +----------------- .../publicdashboards/service/service.go | 7 +- 4 files changed, 25 insertions(+), 545 deletions(-) diff --git a/pkg/apimachinery/identity/context.go b/pkg/apimachinery/identity/context.go index 627cace5d61..81a7f81de6c 100644 --- a/pkg/apimachinery/identity/context.go +++ b/pkg/apimachinery/identity/context.go @@ -75,12 +75,14 @@ func getWildcardPermissions(actions ...string) map[string][]string { // serviceIdentityPermissions is a list of wildcard permissions for provided actions. // We should add every action required "internally" here. var serviceIdentityPermissions = getWildcardPermissions( + "annotations:read", "folders:read", "folders:write", "folders:create", "dashboards:read", "dashboards:write", "dashboards:create", + "datasources:query", "datasources:read", "alert.provisioning:write", "alert.provisioning.secrets:read", diff --git a/pkg/services/publicdashboards/service/query.go b/pkg/services/publicdashboards/service/query.go index 3d8731e895d..446f74ab3b6 100644 --- a/pkg/services/publicdashboards/service/query.go +++ b/pkg/services/publicdashboards/service/query.go @@ -8,16 +8,13 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/expr" - "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" - "github.com/grafana/grafana/pkg/services/datasources" - "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/publicdashboards/models" "github.com/grafana/grafana/pkg/services/publicdashboards/validation" - "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/tsdb/grafanads" ) @@ -37,8 +34,8 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to unmarshal dashboard annotations: %w", err) } - anonymousUser := buildAnonymousUser(ctx, dash, pd.features) - + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the annotations. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dash.OrgID) uniqueEvents := make(map[int64]models.AnnotationEvent, 0) for _, anno := range annoDto.Annotations.List { // skip annotations that are not enabled or are not a grafana datasource @@ -51,7 +48,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT OrgID: dash.OrgID, DashboardID: dash.ID, DashboardUID: dash.UID, - SignedInUser: anonymousUser, + SignedInUser: svcIdent, } if anno.Target != nil { @@ -63,7 +60,7 @@ func (pd *PublicDashboardServiceImpl) FindAnnotations(ctx context.Context, reqDT } } - annotationItems, err := pd.AnnotationsRepo.Find(ctx, annoQuery) + annotationItems, err := pd.AnnotationsRepo.Find(svcCtx, annoQuery) if err != nil { return nil, models.ErrInternalServerError.Errorf("FindAnnotations: failed to find annotations: %w", err) } @@ -139,8 +136,9 @@ func (pd *PublicDashboardServiceImpl) GetQueryDataResponse(ctx context.Context, return nil, models.ErrPanelQueriesNotFound.Errorf("GetQueryDataResponse: failed to extract queries from panel") } - anonymousUser := buildAnonymousUser(ctx, dashboard, pd.features) - res, err := pd.QueryDataService.QueryData(ctx, anonymousUser, skipDSCache, metricReq) + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the datasource. + svcCtx, svcIdent := identity.WithServiceIdentity(ctx, dashboard.OrgID) + res, err := pd.QueryDataService.QueryData(svcCtx, svcIdent, skipDSCache, metricReq) reqDatasources := metricReq.GetUniqueDatasourceTypes() if err != nil { @@ -180,92 +178,6 @@ func (pd *PublicDashboardServiceImpl) buildMetricRequest(dashboard *dashboards.D }, nil } -// buildAnonymousUser creates a user with permissions to read from all datasources used in the dashboard -func buildAnonymousUser(ctx context.Context, dashboard *dashboards.Dashboard, features featuremgmt.FeatureToggles) *user.SignedInUser { - datasourceUids := getUniqueDashboardDatasourceUids(dashboard.Data) - - // Create a user with blank permissions - anonymousUser := &user.SignedInUser{OrgID: dashboard.OrgID, Permissions: make(map[int64]map[string][]string)} - - // Scopes needed for Annotation queries - annotationScopes := []string{accesscontrol.ScopeAnnotationsTypeDashboard} - // Need to access all dashboards since tags annotations span across all dashboards - dashboardScopes := []string{dashboards.ScopeDashboardsProvider.GetResourceAllScope()} - - // Scopes needed for datasource queries - queryScopes := make([]string, 0) - readScopes := make([]string, 0) - for _, uid := range datasourceUids { - scope := datasources.ScopeProvider.GetResourceScopeUID(uid) - queryScopes = append(queryScopes, scope) - readScopes = append(readScopes, scope) - } - - // Apply all scopes to the actions we need the user to be able to perform - permissions := make(map[string][]string) - permissions[datasources.ActionQuery] = queryScopes - permissions[datasources.ActionRead] = readScopes - permissions[dashboards.ActionDashboardsRead] = dashboardScopes - permissions[accesscontrol.ActionAnnotationsRead] = annotationScopes - - if features.IsEnabled(ctx, featuremgmt.FlagAnnotationPermissionUpdate) { - permissions[accesscontrol.ActionAnnotationsRead] = dashboardScopes - } - - anonymousUser.Permissions[dashboard.OrgID] = permissions - - return anonymousUser -} - -func getUniqueDashboardDatasourceUids(dashboard *simplejson.Json) []string { - var datasourceUids []string - exists := map[string]bool{} - - // collapsed rows contain panels in a nested structure, so we need to flatten them before calculate unique uids - flattenedPanels := getFlattenedPanels(dashboard) - - for _, panelObj := range flattenedPanels { - panel := simplejson.NewFromAny(panelObj) - uid := getDataSourceUidFromJson(panel) - - // if uid is for a mixed datasource, get the datasource uids from the targets - if uid == "-- Mixed --" { - for _, targetObj := range panel.Get("targets").MustArray() { - target := simplejson.NewFromAny(targetObj) - datasourceUid := getDataSourceUidFromJson(target) - if _, ok := exists[datasourceUid]; !ok { - datasourceUids = append(datasourceUids, datasourceUid) - exists[datasourceUid] = true - } - } - } else { - if _, ok := exists[uid]; !ok { - datasourceUids = append(datasourceUids, uid) - exists[uid] = true - } - } - } - - return datasourceUids -} - -func getFlattenedPanels(dashboard *simplejson.Json) []any { - var flatPanels []any - for _, panelObj := range dashboard.Get("panels").MustArray() { - panel := simplejson.NewFromAny(panelObj) - // if the panel is a row and it is collapsed, get the queries from the panels inside the row - // if it is not collapsed, the row does not have any panels - if panel.Get("type").MustString() == "row" { - if panel.Get("collapsed").MustBool() { - flatPanels = append(flatPanels, panel.Get("panels").MustArray()...) - } - } else { - flatPanels = append(flatPanels, panelObj) - } - } - return flatPanels -} - func groupQueriesByPanelId(dashboard *simplejson.Json) map[int64][]*simplejson.Json { result := make(map[int64][]*simplejson.Json) diff --git a/pkg/services/publicdashboards/service/query_test.go b/pkg/services/publicdashboards/service/query_test.go index f6aec672eac..0db1dfd08b0 100644 --- a/pkg/services/publicdashboards/service/query_test.go +++ b/pkg/services/publicdashboards/service/query_test.go @@ -11,8 +11,8 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend/gtime" "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" - "github.com/grafana/grafana/pkg/infra/db" dashboard2 "github.com/grafana/grafana/pkg/kinds/dashboard" "github.com/grafana/grafana/pkg/services/annotations" "github.com/grafana/grafana/pkg/services/dashboards" @@ -110,161 +110,6 @@ const ( "schemaVersion": 35 }` - dashboardWithMixedDatasource = ` -{ - "panels": [ - { - "datasource": { - "type": "datasource", - "uid": "-- Mixed --" - }, - "id": 1, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - }, - { - "datasource": "6SOeCRrVk", - "exemplar": true, - "expr": "test{id=\"f0dd9b69-ad04-4342-8e79-ced8c245683b\", name=\"test\"}", - "hide": false, - "interval": "", - "legendFormat": "", - "refId": "B" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 3, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - - dashboardWithDuplicateDatasources = ` -{ - "panels": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "id": 1, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "abc123" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 2, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "id": 3, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "_yxMP8Ynk" - }, - "exemplar": true, - "expr": "go_goroutines{job=\"$job\"}", - "interval": "", - "legendFormat": "", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "schemaVersion": 35 -}` - oldStyleDashboard = ` { "panels": [ @@ -460,218 +305,6 @@ const ( ], "schemaVersion": 35 }` - - dashboardWithCollapsedRows = ` -{ -"panels": [ - { - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 12, - "title": "Row title", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 8, - "w": 12, - "x": 0, - "y": 1 - }, - "id": 11, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "qCbTUC37k" - }, - "editorMode": "builder", - "expr": "access_evaluation_duration_bucket", - "legendFormat": "__auto", - "range": true, - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - }, - { - "collapsed": true, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 9 - }, - "id": 10, - "panels": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisCenteredZero": false, - "axisColorMode": "text", - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 0, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "auto", - "spanNulls": false, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green" - }, - { - "color": "red", - "value": 80 - } - ] - } - }, - "overrides": [] - }, - "gridPos": { - "h": 9, - "w": 12, - "x": 0, - "y": 10 - }, - "id": 8, - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom", - "showLegend": true - }, - "tooltip": { - "mode": "single", - "sort": "none" - } - }, - "pluginVersion": "9.4.0-pre", - "targets": [ - { - "datasource": { - "type": "influxdb", - "uid": "P49A45DF074423DFB" - }, - "query": "// v.bucket, v.timeRangeStart, and v.timeRange stop are all variables supported by the flux plugin and influxdb\nfrom(bucket: v.bucket)\n |> range(start: v.timeRangeStart, stop: v.timeRangeStop)\n |> filter(fn: (r) => r[\"_value\"] >= 10 and r[\"_value\"] <= 20)", - "refId": "A" - } - ], - "title": "Panel Title", - "type": "timeseries" - } - ], - "title": "Row title 1", - "type": "row" - } - ] -}` ) func TestGetQueryDataResponse(t *testing.T) { @@ -731,8 +364,7 @@ func TestGetQueryDataResponse(t *testing.T) { func TestFindAnnotations(t *testing.T) { color := "red" name := "annoName" - features := featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - t.Run("will build anonymous user with correct permissions to get annotations", func(t *testing.T) { + t.Run("service identity has correct permissions to get annotations dashboards and query datasources", func(t *testing.T) { fakeStore := &FakePublicDashboardStore{} fakeStore.On("FindByAccessToken", mock.Anything, mock.AnythingOfType("string")). Return(&PublicDashboard{Uid: "uid1", IsEnabled: true}, nil) @@ -746,11 +378,14 @@ func TestFindAnnotations(t *testing.T) { } dash := dashboards.NewDashboard("testDashboard") - items, _ := service.FindAnnotations(context.Background(), reqDTO, "abc123") - anonUser := buildAnonymousUser(context.Background(), dash, features) - - assert.Equal(t, "dashboards:*", anonUser.Permissions[0]["dashboards:read"][0]) + items, err := service.FindAnnotations(context.Background(), reqDTO, "abc123") + require.NoError(t, err) assert.Len(t, items, 0) + + _, svcIdent := identity.WithServiceIdentity(context.Background(), dash.OrgID) + require.Equal(t, "*", svcIdent.GetPermissions()["datasources:query"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["dashboards:read"][0]) + require.Equal(t, "*", svcIdent.GetPermissions()["annotations:read"][0]) }) t.Run("Test events from tag queries overwrite built-in annotation queries and duplicate events are not returned", func(t *testing.T) { @@ -1121,47 +756,6 @@ func TestGetMetricRequest(t *testing.T) { }) } -func TestGetUniqueDashboardDatasourceUids(t *testing.T) { - t.Run("can get unique datasource ids from dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithDuplicateDatasources)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "_yxMP8Ynk", uids[1]) - }) - - t.Run("can get unique datasource ids from dashboard with a mixed datasource", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithMixedDatasource)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 3) - require.Equal(t, "abc123", uids[0]) - require.Equal(t, "6SOeCRrVk", uids[1]) - require.Equal(t, "_yxMP8Ynk", uids[2]) - }) - - t.Run("can get no datasource uids from empty dashboard", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(`{"panels": {}}`)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 0) - }) - - t.Run("can get unique datasource ids from dashboard with rows", func(t *testing.T) { - json, err := simplejson.NewJson([]byte(dashboardWithCollapsedRows)) - require.NoError(t, err) - - uids := getUniqueDashboardDatasourceUids(json) - require.Len(t, uids, 2) - require.Equal(t, "qCbTUC37k", uids[0]) - require.Equal(t, "P49A45DF074423DFB", uids[1]) - }) -} - func TestBuildMetricRequest(t *testing.T) { fakeDashboardService := &dashboards.FakeDashboardService{} service, sqlStore, cfg := newPublicDashboardServiceImpl(t, nil, nil, nil, fakeDashboardService, nil) @@ -1318,39 +912,6 @@ func TestBuildMetricRequest(t *testing.T) { }) } -func TestBuildAnonymousUser(t *testing.T) { - sqlStore, cfg := db.InitTestDBWithCfg(t) - dashboardStore, err := dashboardsDB.ProvideDashboardStore(sqlStore, cfg, featuremgmt.WithFeatures(), tagimpl.ProvideService(sqlStore)) - require.NoError(t, err) - dashboard := insertTestDashboard(t, dashboardStore, "testDashie", 1, 0, "", true, []map[string]interface{}{}, nil) - features := featuremgmt.WithFeatures() - - t.Run("will add datasource read and query permissions to user for each datasource in dashboard", func(t *testing.T) { - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:query"][0]) - require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:query"][1]) - require.Equal(t, "datasources:uid:ds1", user.Permissions[user.OrgID]["datasources:read"][0]) - require.Equal(t, "datasources:uid:ds3", user.Permissions[user.OrgID]["datasources:read"][1]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations", func(t *testing.T) { - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "annotations:type:dashboard", user.Permissions[user.OrgID]["annotations:read"][0]) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) - }) - t.Run("will add dashboard and annotation permissions needed for getting annotations when FlagAnnotationPermissionUpdate is enabled", func(t *testing.T) { - features = featuremgmt.WithFeatures(featuremgmt.FlagAnnotationPermissionUpdate) - user := buildAnonymousUser(context.Background(), dashboard, features) - - require.Equal(t, dashboard.OrgID, user.OrgID) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["annotations:read"][0]) - require.Equal(t, "dashboards:*", user.Permissions[user.OrgID]["dashboards:read"][0]) - }) -} - func TestGroupQueriesByPanelId(t *testing.T) { t.Run("can extract queries from dashboard with panel datasource string that has no datasource on panel targets", func(t *testing.T) { json, err := simplejson.NewJson([]byte(oldStyleDashboard)) diff --git a/pkg/services/publicdashboards/service/service.go b/pkg/services/publicdashboards/service/service.go index d3239e2857d..9223981518c 100644 --- a/pkg/services/publicdashboards/service/service.go +++ b/pkg/services/publicdashboards/service/service.go @@ -13,6 +13,7 @@ import ( "go.opentelemetry.io/otel" "github.com/grafana/grafana/pkg/api/dtos" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/services/accesscontrol" @@ -136,7 +137,11 @@ func (pd *PublicDashboardServiceImpl) Find(ctx context.Context, uid string) (*Pu func (pd *PublicDashboardServiceImpl) FindDashboard(ctx context.Context, orgId int64, dashboardUid string) (*dashboards.Dashboard, error) { ctx, span := tracer.Start(ctx, "publicdashboards.FindDashboard") defer span.End() - dash, err := pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + + // We don't have a signed in user for public dashboards. We are using Grafana's Identity to query the dashboard. + dash, err := identity.WithServiceIdentityFn(ctx, orgId, func(ctx context.Context) (*dashboards.Dashboard, error) { + return pd.dashboardService.GetDashboard(ctx, &dashboards.GetDashboardQuery{UID: dashboardUid, OrgID: orgId}) + }) if err != nil { var dashboardErr dashboards.DashboardErr if ok := errors.As(err, &dashboardErr); ok { From 6e4c1a57c19c12d633fa1d72f19af30aa43db66d Mon Sep 17 00:00:00 2001 From: Johnny Kartheiser <140559259+JohnnyK-Grafana@users.noreply.github.com> Date: Thu, 13 Feb 2025 10:29:04 -0600 Subject: [PATCH 08/18] docs: capitalization issues (#100562) fixing two capitalization issues for product names. --- .../configure-notifications/manage-contact-points/_index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md index 160d76360dc..215ed1926dc 100644 --- a/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md +++ b/docs/sources/alerting/configure-notifications/manage-contact-points/_index.md @@ -140,13 +140,13 @@ Each contact point integration has its own configuration options and setup proce - [Discord](ref:discord) - [Email](ref:email) - [Google Chat](ref:gchat) -- [Grafana Oncall](ref:oncall) +- [Grafana OnCall](ref:oncall) - Kafka REST Proxy - Line - [Microsoft Teams](ref:teams) - [MQTT](ref:mqtt) - [Opsgenie](ref:opsgenie) -- [Pagerduty](ref:pagerduty) +- [PagerDuty](ref:pagerduty) - Pushover - Sensu Go - [Slack](ref:slack) From 155492c8a5858330aba5f8a6a5168ebd5ec4cc94 Mon Sep 17 00:00:00 2001 From: Will Assis <35489495+gassiss@users.noreply.github.com> Date: Thu, 13 Feb 2025 13:35:53 -0300 Subject: [PATCH 09/18] search: handle "permission" query param in search (#100607) handle "permission" query param in search --- .../dashboard/legacysearcher/search_client.go | 19 +++++-------------- .../dashboards/service/dashboard_service.go | 4 ++++ pkg/storage/unified/resource/resource.pb.go | 16 +++++++++++++--- pkg/storage/unified/resource/resource.proto | 2 ++ pkg/storage/unified/search/bleve.go | 8 +++++++- 5 files changed, 31 insertions(+), 18 deletions(-) diff --git a/pkg/registry/apis/dashboard/legacysearcher/search_client.go b/pkg/registry/apis/dashboard/legacysearcher/search_client.go index bd2e4a896a8..c0e18288ea5 100644 --- a/pkg/registry/apis/dashboard/legacysearcher/search_client.go +++ b/pkg/registry/apis/dashboard/legacysearcher/search_client.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/apis/dashboard" folderv0alpha1 "github.com/grafana/grafana/pkg/apis/folder/v0alpha1" "github.com/grafana/grafana/pkg/services/dashboards" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/search" "github.com/grafana/grafana/pkg/services/sqlstore/searchstore" "github.com/grafana/grafana/pkg/storage/unified/resource" @@ -40,9 +41,6 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour req.Query = strings.ReplaceAll(req.Query, "*", "") } - // TODO add missing support for the following query params: - // - folderIds (won't support, must use folderUIDs) - // - permission query := &dashboards.FindPersistedDashboardsQuery{ Title: req.Query, Limit: req.Limit, @@ -51,6 +49,10 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour IsDeleted: req.IsDeleted, } + if req.Permission == int64(dashboardaccess.PERMISSION_EDIT) { + query.Permission = dashboardaccess.PERMISSION_EDIT + } + var queryType string if req.Options.Key.Resource == dashboard.DASHBOARD_RESOURCE { queryType = searchstore.TypeDashboard @@ -123,22 +125,11 @@ func (c *DashboardSearchClient) Search(ctx context.Context, req *resource.Resour } } - // TODO need to test this - // emptyResponse, err := a.dashService.GetSharedDashboardUIDsQuery(ctx, query) - - // if err != nil { - // return nil, err - // } else if emptyResponse { - // return nil, nil - // } - res, err := c.dashboardStore.FindDashboards(ctx, query) if err != nil { return nil, err } - // TODO sort if query.Sort == "" see sortedHits in services/search/service.go - searchFields := resource.StandardSearchFields() list := &resource.ResourceSearchResponse{ Results: &resource.ResourceTable{ diff --git a/pkg/services/dashboards/service/dashboard_service.go b/pkg/services/dashboards/service/dashboard_service.go index 3964922fcda..ed8a8fa1d19 100644 --- a/pkg/services/dashboards/service/dashboard_service.go +++ b/pkg/services/dashboards/service/dashboard_service.go @@ -1755,6 +1755,10 @@ func (dr *DashboardServiceImpl) searchDashboardsThroughK8sRaw(ctx context.Contex request.IsDeleted = query.IsDeleted } + if query.Permission > 0 { + request.Permission = int64(query.Permission) + } + if query.Limit < 1 { query.Limit = 1000 } diff --git a/pkg/storage/unified/resource/resource.pb.go b/pkg/storage/unified/resource/resource.pb.go index ee8801f361b..dc6593d385f 100644 --- a/pkg/storage/unified/resource/resource.pb.go +++ b/pkg/storage/unified/resource/resource.pb.go @@ -2016,6 +2016,7 @@ type ResourceSearchRequest struct { Explain bool `protobuf:"varint,9,opt,name=explain,proto3" json:"explain,omitempty"` IsDeleted bool `protobuf:"varint,10,opt,name=is_deleted,json=isDeleted,proto3" json:"is_deleted,omitempty"` Page int64 `protobuf:"varint,11,opt,name=page,proto3" json:"page,omitempty"` + Permission int64 `protobuf:"varint,12,opt,name=permission,proto3" json:"permission,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -2127,6 +2128,13 @@ func (x *ResourceSearchRequest) GetPage() int64 { return 0 } +func (x *ResourceSearchRequest) GetPermission() int64 { + if x != nil { + return x.Permission + } + return 0 +} + type ResourceSearchResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // Error details @@ -4238,8 +4246,8 @@ var file_resource_proto_rawDesc = string([]byte{ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x67, 0x72, 0x6f, 0x75, 0x70, 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xee, - 0x04, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, + 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x8e, + 0x05, 0x0a, 0x15, 0x52, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2f, 0x0a, 0x07, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x15, 0x2e, 0x72, 0x65, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x73, @@ -4265,7 +4273,9 @@ var file_resource_proto_rawDesc = string([]byte{ 0x6c, 0x61, 0x69, 0x6e, 0x12, 0x1d, 0x0a, 0x0a, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x09, 0x69, 0x73, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, - 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, + 0x03, 0x52, 0x04, 0x70, 0x61, 0x67, 0x65, 0x12, 0x1e, 0x0a, 0x0a, 0x70, 0x65, 0x72, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x70, 0x65, 0x72, + 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x30, 0x0a, 0x04, 0x53, 0x6f, 0x72, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x65, 0x73, 0x63, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x04, 0x64, 0x65, 0x73, 0x63, 0x1a, 0x33, 0x0a, 0x05, 0x46, 0x61, 0x63, diff --git a/pkg/storage/unified/resource/resource.proto b/pkg/storage/unified/resource/resource.proto index f16f194d360..c11ed29f259 100644 --- a/pkg/storage/unified/resource/resource.proto +++ b/pkg/storage/unified/resource/resource.proto @@ -457,6 +457,8 @@ message ResourceSearchRequest { bool is_deleted = 10; int64 page = 11; + + int64 permission = 12; } message ResourceSearchResponse { diff --git a/pkg/storage/unified/search/bleve.go b/pkg/storage/unified/search/bleve.go index bf9748cf5bc..2b76bb97529 100644 --- a/pkg/storage/unified/search/bleve.go +++ b/pkg/storage/unified/search/bleve.go @@ -18,6 +18,7 @@ import ( "github.com/blevesearch/bleve/v2/search/query" bleveSearch "github.com/blevesearch/bleve/v2/search/searcher" index "github.com/blevesearch/bleve_index_api" + "github.com/grafana/grafana/pkg/services/dashboards/dashboardaccess" "github.com/grafana/grafana/pkg/services/featuremgmt" "go.opentelemetry.io/otel/trace" "k8s.io/apimachinery/pkg/selection" @@ -611,11 +612,16 @@ func (b *bleveIndex) toBleveSearchRequest(ctx context.Context, req *resource.Res if !ok { return nil, resource.AsErrorResult(fmt.Errorf("missing auth info")) } + verb := utils.VerbList + if req.Permission == int64(dashboardaccess.PERMISSION_EDIT) { + verb = utils.VerbPatch + } + checker, err := access.Compile(ctx, auth, authlib.ListRequest{ Namespace: b.key.Namespace, Group: b.key.Group, Resource: b.key.Resource, - Verb: utils.VerbList, + Verb: verb, }) if err != nil { return nil, resource.AsErrorResult(err) From 2bdeb727cfa56859e94e0474d7b19b923956eaee Mon Sep 17 00:00:00 2001 From: Josh Hunt Date: Thu, 13 Feb 2025 16:36:16 +0000 Subject: [PATCH 10/18] Chore: Bump react-router to v5.3.4 (#100500) --- package.json | 4 ++-- packages/grafana-ui/package.json | 2 +- yarn.lock | 40 +++++++++++--------------------- 3 files changed, 16 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index 4d638dc7b9b..150751ba6e4 100644 --- a/package.json +++ b/package.json @@ -380,8 +380,8 @@ "react-redux": "9.2.0", "react-resizable": "3.0.5", "react-responsive-carousel": "^3.2.23", - "react-router": "5.3.3", - "react-router-dom": "5.3.3", + "react-router": "5.3.4", + "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", "react-select": "5.10.0", "react-split-pane": "0.1.92", diff --git a/packages/grafana-ui/package.json b/packages/grafana-ui/package.json index cee844a1b5d..eff1830e872 100644 --- a/packages/grafana-ui/package.json +++ b/packages/grafana-ui/package.json @@ -97,7 +97,7 @@ "react-i18next": "^15.0.0", "react-inlinesvg": "4.1.5", "react-loading-skeleton": "3.5.0", - "react-router-dom": "5.3.3", + "react-router-dom": "5.3.4", "react-router-dom-v5-compat": "^6.26.1", "react-select": "5.10.0", "react-table": "7.8.0", diff --git a/yarn.lock b/yarn.lock index f818433fbda..f1092de32f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4124,7 +4124,7 @@ __metadata: react-i18next: "npm:^15.0.0" react-inlinesvg: "npm:4.1.5" react-loading-skeleton: "npm:3.5.0" - react-router-dom: "npm:5.3.3" + react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" react-select: "npm:5.10.0" react-select-event: "npm:^5.1.0" @@ -18399,8 +18399,8 @@ __metadata: react-refresh: "npm:0.14.0" react-resizable: "npm:3.0.5" react-responsive-carousel: "npm:^3.2.23" - react-router: "npm:5.3.3" - react-router-dom: "npm:5.3.3" + react-router: "npm:5.3.4" + react-router-dom: "npm:5.3.4" react-router-dom-v5-compat: "npm:^6.26.1" react-select: "npm:5.10.0" react-select-event: "npm:5.5.1" @@ -22463,19 +22463,6 @@ __metadata: languageName: node linkType: hard -"mini-create-react-context@npm:^0.4.0": - version: 0.4.1 - resolution: "mini-create-react-context@npm:0.4.1" - dependencies: - "@babel/runtime": "npm:^7.12.1" - tiny-warning: "npm:^1.0.3" - peerDependencies: - prop-types: ^15.0.0 - react: ^0.14.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10/c816c785b7dccd67fdfa6a5edc673363b11845b6abca8a9d9f3ffa74520266d979b56f5db0dfc62ed912a90553c15be28c816311fc9c7856ab66a81d461d50e6 - languageName: node - linkType: hard - "mini-css-extract-plugin@npm:2.9.2": version: 2.9.2 resolution: "mini-css-extract-plugin@npm:2.9.2" @@ -26768,20 +26755,20 @@ __metadata: languageName: node linkType: hard -"react-router-dom@npm:5.3.3": - version: 5.3.3 - resolution: "react-router-dom@npm:5.3.3" +"react-router-dom@npm:5.3.4": + version: 5.3.4 + resolution: "react-router-dom@npm:5.3.4" dependencies: "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" loose-envify: "npm:^1.3.1" prop-types: "npm:^15.6.2" - react-router: "npm:5.3.3" + react-router: "npm:5.3.4" tiny-invariant: "npm:^1.0.2" tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/49552596f1a4c753b99324a5f4345b3ee91fbb780aa65851a7113f053044ef96c083d2ded12937e593b23a0fcdf58b9e49780df6bf6e27d9eeb348b3c85ae611 + checksum: 10/5e0696ae2d86f466ff700944758a227e1dcd79b48797d567776506e4e3b4a08b81336155feb86a33be9f38c17c4d3d94212b5c60c8ee9a086022e4fd3961db29 languageName: node linkType: hard @@ -26798,15 +26785,14 @@ __metadata: languageName: node linkType: hard -"react-router@npm:5.3.3": - version: 5.3.3 - resolution: "react-router@npm:5.3.3" +"react-router@npm:5.3.4": + version: 5.3.4 + resolution: "react-router@npm:5.3.4" dependencies: "@babel/runtime": "npm:^7.12.13" history: "npm:^4.9.0" hoist-non-react-statics: "npm:^3.1.0" loose-envify: "npm:^1.3.1" - mini-create-react-context: "npm:^0.4.0" path-to-regexp: "npm:^1.7.0" prop-types: "npm:^15.6.2" react-is: "npm:^16.6.0" @@ -26814,7 +26800,7 @@ __metadata: tiny-warning: "npm:^1.0.0" peerDependencies: react: ">=15" - checksum: 10/4631eed91020c73950804c7c7454e74b2eb495f803c5ca60c8b5572ca72cc06e336f3b08d9ee3fa730128a52c4d9e16d1aa7e8b7f85560629117e16d99a01cef + checksum: 10/99d54a99af6bc6d7cad2e5ea7eee9485b62a8b8e16a1182b18daa7fad7dafa5e526850eaeebff629848b297ae055a9cb5b4aba8760e81af8b903efc049d48f5c languageName: node linkType: hard @@ -30309,7 +30295,7 @@ __metadata: languageName: node linkType: hard -"tiny-warning@npm:^1.0.0, tiny-warning@npm:^1.0.3": +"tiny-warning@npm:^1.0.0": version: 1.0.3 resolution: "tiny-warning@npm:1.0.3" checksum: 10/da62c4acac565902f0624b123eed6dd3509bc9a8d30c06e017104bedcf5d35810da8ff72864400ad19c5c7806fc0a8323c68baf3e326af7cb7d969f846100d71 From b58b5b5768fc34a81d8e2bf4d23150ae3f287f8a Mon Sep 17 00:00:00 2001 From: Georges Chaudy Date: Thu, 13 Feb 2025 17:39:33 +0100 Subject: [PATCH 11/18] grpc: improve grpc logger (#100606) use proper grpc logging --- .../grpcserver/interceptors/logging.go | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/pkg/services/grpcserver/interceptors/logging.go b/pkg/services/grpcserver/interceptors/logging.go index 2a3997a7024..db4f017d1e4 100644 --- a/pkg/services/grpcserver/interceptors/logging.go +++ b/pkg/services/grpcserver/interceptors/logging.go @@ -2,27 +2,34 @@ package interceptors import ( "context" + "fmt" "github.com/grafana/grafana/pkg/infra/log" + "github.com/grpc-ecosystem/go-grpc-middleware/v2/interceptors/logging" "google.golang.org/grpc" ) -func LoggingUnaryInterceptor(logger log.Logger, enabled bool) grpc.UnaryServerInterceptor { - return func( - ctx context.Context, - req any, - info *grpc.UnaryServerInfo, - handler grpc.UnaryHandler, - ) (resp any, err error) { - resp, err = handler(ctx, req) - if enabled { - ctxLogger := logger.FromContext(ctx) - if err != nil { - ctxLogger.Error("gRPC call", "method", info.FullMethod, "req", req, "err", err) - } else { - ctxLogger.Info("gRPC call", "method", info.FullMethod, "req", req, "resp", resp) - } +func InterceptorLogger(l log.Logger, enabled bool) logging.Logger { + return logging.LoggerFunc(func(ctx context.Context, lvl logging.Level, msg string, fields ...any) { + if !enabled { + return } - return resp, err - } + l := l.FromContext(ctx) + switch lvl { + case logging.LevelDebug: + l.Debug(msg, fields...) + case logging.LevelInfo: + l.Info(msg, fields...) + case logging.LevelWarn: + l.Warn(msg, fields...) + case logging.LevelError: + l.Error(msg, fields...) + default: + panic(fmt.Sprintf("unknown level %v", lvl)) + } + }) +} + +func LoggingUnaryInterceptor(logger log.Logger, enabled bool) grpc.UnaryServerInterceptor { + return logging.UnaryServerInterceptor(InterceptorLogger(logger, enabled)) } From 5315b4fd2df445584bec3748d49c1e7227ad0e47 Mon Sep 17 00:00:00 2001 From: Bogdan Matei Date: Thu, 13 Feb 2025 18:41:09 +0200 Subject: [PATCH 12/18] Dashboard: Fix repeats behavior for inspect, solo panel and repeated and empty panels (#100605) --- e2e/old-arch/various-suite/solo-route.spec.ts | 4 +-- e2e/various-suite/solo-route.spec.ts | 4 +-- package.json | 4 +-- .../scene/DashboardSceneUrlSync.ts | 32 ++++++++++++++++--- .../DefaultGridLayoutManager.tsx | 8 +++++ .../dashboard-scene/utils/clone.test.ts | 2 ++ .../features/dashboard-scene/utils/utils.ts | 13 ++++++-- yarn.lock | 22 ++++++------- 8 files changed, 66 insertions(+), 23 deletions(-) diff --git a/e2e/old-arch/various-suite/solo-route.spec.ts b/e2e/old-arch/various-suite/solo-route.spec.ts index 9717baf41bd..415257ead7c 100644 --- a/e2e/old-arch/various-suite/solo-route.spec.ts +++ b/e2e/old-arch/various-suite/solo-route.spec.ts @@ -25,7 +25,7 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-16-clone-0/grid-item-2/panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server=A').should('exist'); @@ -38,7 +38,7 @@ describe('Solo Route', () => { 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server = A, pod = Rob').should('exist'); + e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); }); diff --git a/e2e/various-suite/solo-route.spec.ts b/e2e/various-suite/solo-route.spec.ts index 9717baf41bd..415257ead7c 100644 --- a/e2e/various-suite/solo-route.spec.ts +++ b/e2e/various-suite/solo-route.spec.ts @@ -25,7 +25,7 @@ describe('Solo Route', () => { it('Can view solo repeated panel in scenes', () => { // open Panel Tests - Graph NG e2e.pages.SoloPanel.visit( - 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-16-clone-0/grid-item-2/panel-2-clone-0&__feature.dashboardSceneSolo=true' + 'templating-repeating-panels/templating-repeating-panels?orgId=1&from=1699934989607&to=1699956589607&panelId=panel-2-clone-0&__feature.dashboardSceneSolo=true' ); e2e.components.Panels.Panel.title('server=A').should('exist'); @@ -38,7 +38,7 @@ describe('Solo Route', () => { 'Repeating-rows-uid/repeating-rows?orgId=1&var-server=A&var-server=B&var-server=D&var-pod=1&var-pod=2&var-pod=3&panelId=panel-16-clone-1/grid-item-2/panel-2-clone-1&__feature.dashboardSceneSolo=true' ); - e2e.components.Panels.Panel.title('server = A, pod = Rob').should('exist'); + e2e.components.Panels.Panel.title('server = B, pod = Rob').should('exist'); cy.contains('uplot-main-div').should('not.exist'); }); }); diff --git a/package.json b/package.json index 150751ba6e4..7e889ddedde 100644 --- a/package.json +++ b/package.json @@ -275,8 +275,8 @@ "@grafana/prometheus": "workspace:*", "@grafana/runtime": "workspace:*", "@grafana/saga-icons": "workspace:*", - "@grafana/scenes": "6.0.1", - "@grafana/scenes-react": "6.0.1", + "@grafana/scenes": "6.0.2", + "@grafana/scenes-react": "6.0.2", "@grafana/schema": "workspace:*", "@grafana/sql": "workspace:*", "@grafana/ui": "workspace:*", diff --git a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts index 3493be625f9..cde4c8df0e0 100644 --- a/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts +++ b/public/app/features/dashboard-scene/scene/DashboardSceneUrlSync.ts @@ -22,7 +22,8 @@ import { DefaultGridLayoutManager } from './layout-default/DefaultGridLayoutMana import { DashboardRepeatsProcessedEvent } from './types/DashboardRepeatsProcessedEvent'; export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { - private _eventSub?: Unsubscribable; + private _viewEventSub?: Unsubscribable; + private _inspectEventSub?: Unsubscribable; constructor(private _scene: DashboardScene) {} @@ -78,6 +79,14 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { if (typeof values.inspect === 'string') { let panel = findVizPanelByKey(this._scene, values.inspect); if (!panel) { + // If we are trying to view a repeat clone that can't be found it might be that the repeats have not been processed yet + // Here we check if the key contains the clone key so we force the repeat processing + // It doesn't matter if the element or the ancestors are clones or not, just that the key contains the clone key + if (containsCloneKey(values.inspect)) { + this._handleInspectRepeatClone(values.inspect); + return; + } + appEvents.emit(AppEvents.alertError, ['Panel not found']); locationService.partial({ inspect: null }); return; @@ -177,12 +186,27 @@ export class DashboardSceneUrlSync implements SceneObjectUrlSyncHandler { } } + private _handleInspectRepeatClone(inspect: string) { + if (!this._inspectEventSub) { + this._inspectEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { + const panel = findVizPanelByKey(this._scene, inspect); + if (panel) { + this._inspectEventSub?.unsubscribe(); + this._scene.setState({ + inspectPanelKey: inspect, + overlay: new PanelInspectDrawer({ panelRef: panel.getRef() }), + }); + } + }); + } + } + private _handleViewRepeatClone(viewPanel: string) { - if (!this._eventSub) { - this._eventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { + if (!this._viewEventSub) { + this._viewEventSub = this._scene.subscribeToEvent(DashboardRepeatsProcessedEvent, () => { const panel = findVizPanelByKey(this._scene, viewPanel); if (panel) { - this._eventSub?.unsubscribe(); + this._viewEventSub?.unsubscribe(); this._scene.setState({ viewPanelScene: new ViewPanelScene({ panelRef: panel.getRef() }) }); } }); diff --git a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx index 97b268757d9..bad747f55de 100644 --- a/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx +++ b/public/app/features/dashboard-scene/scene/layout-default/DefaultGridLayoutManager.tsx @@ -252,6 +252,14 @@ export class DefaultGridLayoutManager } public activateRepeaters() { + if (!this.isActive) { + this.activate(); + } + + if (!this.state.grid.isActive) { + this.state.grid.activate(); + } + this.state.grid.forEachChild((child) => { if (child instanceof DashboardGridItem && !child.isActive) { child.activate(); diff --git a/public/app/features/dashboard-scene/utils/clone.test.ts b/public/app/features/dashboard-scene/utils/clone.test.ts index 58dcef6fb38..97b0e377e6a 100644 --- a/public/app/features/dashboard-scene/utils/clone.test.ts +++ b/public/app/features/dashboard-scene/utils/clone.test.ts @@ -30,6 +30,8 @@ describe('clone', () => { expect(getOriginalKey('panel-clone-1')).toBe('panel'); expect(getOriginalKey('row-clone-1/panel-clone-2')).toBe('panel'); expect(getOriginalKey('tab-clone-0/row-clone-1/panel-clone-2')).toBe('panel'); + expect(getOriginalKey('panel-2-clone-3')).toBe('panel-2'); + expect(getOriginalKey('panel-2')).toBe('panel-2'); }); }); diff --git a/public/app/features/dashboard-scene/utils/utils.ts b/public/app/features/dashboard-scene/utils/utils.ts index 27ecaac2fe2..5e52a1d517f 100644 --- a/public/app/features/dashboard-scene/utils/utils.ts +++ b/public/app/features/dashboard-scene/utils/utils.ts @@ -21,7 +21,7 @@ import { panelMenuBehavior } from '../scene/PanelMenuBehavior'; import { DashboardGridItem } from '../scene/layout-default/DashboardGridItem'; import { DashboardLayoutManager, isDashboardLayoutManager } from '../scene/types/DashboardLayoutManager'; -import { getLastKeyFromClone, getOriginalKey } from './clone'; +import { getOriginalKey, isClonedKey } from './clone'; export const NEW_PANEL_HEIGHT = 8; export const NEW_PANEL_WIDTH = 12; @@ -64,7 +64,16 @@ function findVizPanelInternal(scene: SceneObject, key: string | undefined): VizP const panel = sceneGraph.findObject(scene, (obj) => { const objKey = obj.state.key!; - if (objKey === key || getLastKeyFromClone(objKey) === getLastKeyFromClone(key) || getOriginalKey(objKey) === key) { + if (objKey === key) { + return true; + } + + // It might be possible to have the keys changed in the meantime from `panel-2` to `panel-2-clone-0` + // We need to check this as well + const originalObjectKey = !isClonedKey(objKey) ? getOriginalKey(objKey) : objKey; + const originalKey = !isClonedKey(key) ? getOriginalKey(key) : key; + + if (originalObjectKey === originalKey) { return true; } diff --git a/yarn.lock b/yarn.lock index f1092de32f7..49162fcfe61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3814,11 +3814,11 @@ __metadata: languageName: unknown linkType: soft -"@grafana/scenes-react@npm:6.0.1": - version: 6.0.1 - resolution: "@grafana/scenes-react@npm:6.0.1" +"@grafana/scenes-react@npm:6.0.2": + version: 6.0.2 + resolution: "@grafana/scenes-react@npm:6.0.2" dependencies: - "@grafana/scenes": "npm:6.0.1" + "@grafana/scenes": "npm:6.0.2" lru-cache: "npm:^10.2.2" react-use: "npm:^17.4.0" peerDependencies: @@ -3830,13 +3830,13 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/e4ad83cc628f17232fe9c8d74f641c65e2e289c177ce88a6990d00f6bea4e1a091115e7b98200de7bcff14ace0fe20eb816141fe533fee7d2ad5f7f665404d2c + checksum: 10/9744e01f2ff912229e43cedfa41d626ccdfd034f5b9718b57c593bc90edadade960f76baf1d8ad19eed03709c17c62397df1871b89acc635172aa14f6a20e096 languageName: node linkType: hard -"@grafana/scenes@npm:6.0.1": - version: 6.0.1 - resolution: "@grafana/scenes@npm:6.0.1" +"@grafana/scenes@npm:6.0.2": + version: 6.0.2 + resolution: "@grafana/scenes@npm:6.0.2" dependencies: "@floating-ui/react": "npm:^0.26.16" "@leeoniya/ufuzzy": "npm:^1.0.16" @@ -3854,7 +3854,7 @@ __metadata: react: ^18.0.0 react-dom: ^18.0.0 react-router-dom: ^6.28.0 - checksum: 10/6862e57358ba2e63f139e7f3bb977b19945f67eb070aa2c85c073a55dc460d3ccfeecfee22aea92c660a7632ac997e6cd945f9466b64103436a221979e6e8fcb + checksum: 10/2584f296db6299ef0a09d51f5c267ebcf7e44bd17b4d6516e38d3220f8f1d7aebc63c5fc6523979c4ac4d3f555416ca573e85e03bd36eb33a11941a5b3497149 languageName: node linkType: hard @@ -18151,8 +18151,8 @@ __metadata: "@grafana/prometheus": "workspace:*" "@grafana/runtime": "workspace:*" "@grafana/saga-icons": "workspace:*" - "@grafana/scenes": "npm:6.0.1" - "@grafana/scenes-react": "npm:6.0.1" + "@grafana/scenes": "npm:6.0.2" + "@grafana/scenes-react": "npm:6.0.2" "@grafana/schema": "workspace:*" "@grafana/sql": "workspace:*" "@grafana/tsconfig": "npm:^2.0.0" From 19777ba3e99bb40e7db1b5c9f92021d85deefac8 Mon Sep 17 00:00:00 2001 From: linoman <2051016+linoman@users.noreply.github.com> Date: Thu, 13 Feb 2025 17:49:21 +0100 Subject: [PATCH 13/18] Skip flaky test that's breaking the CI pipelines (#100640) --- pkg/tests/alertmanager/alertmanager_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tests/alertmanager/alertmanager_test.go b/pkg/tests/alertmanager/alertmanager_test.go index 0f127ea2ab6..269e53075e2 100644 --- a/pkg/tests/alertmanager/alertmanager_test.go +++ b/pkg/tests/alertmanager/alertmanager_test.go @@ -13,6 +13,7 @@ func TestAlertmanagerIntegration_ExtraDedupStage(t *testing.T) { } t.Run("assert no flapping alerts when stopOnExtraDedup is enabled", func(t *testing.T) { + t.Skip("skipping flaky test") s, err := NewAlertmanagerScenario() require.NoError(t, err) defer s.Close() From eeadb7e771f18fa62dc6b5d0cb757666cde43aeb Mon Sep 17 00:00:00 2001 From: xavi <114113189+volcanonoodle@users.noreply.github.com> Date: Thu, 13 Feb 2025 18:02:54 +0100 Subject: [PATCH 14/18] IAM: Log error when malformed json arrays are found in SSO configs (#99896) --- pkg/login/social/connectors/azuread_oauth.go | 20 +++++++++--- pkg/login/social/connectors/common.go | 25 ++++++++++++++- pkg/login/social/connectors/generic_oauth.go | 31 ++++++++++++++++--- pkg/login/social/connectors/github_oauth.go | 30 ++++++++++++++---- pkg/login/social/connectors/gitlab_oauth.go | 2 +- pkg/login/social/connectors/google_oauth.go | 2 +- .../social/connectors/grafana_com_oauth.go | 20 +++++++++--- pkg/login/social/connectors/okta_oauth.go | 2 +- pkg/login/social/socialimpl/service.go | 4 +-- pkg/util/strings.go | 17 +++++++--- public/app/features/auth-config/utils/data.ts | 5 ++- 11 files changed, 128 insertions(+), 30 deletions(-) diff --git a/pkg/login/social/connectors/azuread_oauth.go b/pkg/login/social/connectors/azuread_oauth.go index 0f4a72ed5a7..8ae1f3380ab 100644 --- a/pkg/login/social/connectors/azuread_oauth.go +++ b/pkg/login/social/connectors/azuread_oauth.go @@ -88,10 +88,17 @@ type keySetJWKS struct { } func NewAzureADProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles, cache remotecache.CacheStorage) *SocialAzureAD { + s := newSocialBase(social.AzureADProviderName, orgRoleMapper, info, features, cfg) + + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.AzureADProviderName, "error", err) + } + provider := &SocialAzureAD{ - SocialBase: newSocialBase(social.AzureADProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, cache: cache, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, forceUseGraphAPI: MustBool(info.Extra[forceUseGraphAPIKey], ExtraAzureADSettingKeys[forceUseGraphAPIKey].DefaultValue.(bool)), } @@ -236,7 +243,7 @@ func (s *SocialAzureAD) managedIdentityCallback(ctx context.Context) (string, er } func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.AzureADProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } @@ -250,7 +257,12 @@ func (s *SocialAzureAD) Reload(ctx context.Context, settings ssoModels.SSOSettin appendUniqueScope(s.Config, social.OfflineAccessScope) } - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.AzureADProviderName, "error", err) + } + + s.allowedOrganizations = allowedOrganizations s.forceUseGraphAPI = MustBool(newInfo.Extra[forceUseGraphAPIKey], false) return nil diff --git a/pkg/login/social/connectors/common.go b/pkg/login/social/connectors/common.go index 255b96742e8..15fc590f359 100644 --- a/pkg/login/social/connectors/common.go +++ b/pkg/login/social/connectors/common.go @@ -2,6 +2,7 @@ package connectors import ( "context" + "errors" "fmt" "io" "net/http" @@ -13,6 +14,7 @@ import ( "github.com/mitchellh/mapstructure" "golang.org/x/oauth2" + "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -165,9 +167,25 @@ func MustBool(value any, defaultValue bool) bool { return result } +// CreateOAuthInfoFromKeyValuesWithLogging creates an OAuthInfo struct from a map[string]any using mapstructure +// it puts all extra key values into OAuthInfo's Extra map. +// It logs as errors any parsing errors that are not critical +func CreateOAuthInfoFromKeyValuesWithLogging(l log.Logger, provider string, settingsKV map[string]any) (*social.OAuthInfo, error) { + parsingWarns := []error{} + info, err := createOAuthInfoFromKeyValues(settingsKV, &parsingWarns) + if len(parsingWarns) > 0 { + l.Error("Invalid auth configuration setting", "error", errors.Join(parsingWarns...), "provider", provider) + } + return info, err +} + // CreateOAuthInfoFromKeyValues creates an OAuthInfo struct from a map[string]any using mapstructure // it puts all extra key values into OAuthInfo's Extra map func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.OAuthInfo, error) { + return createOAuthInfoFromKeyValues(settingsKV, nil) +} + +func createOAuthInfoFromKeyValues(settingsKV map[string]any, parsingWarns *[]error) (*social.OAuthInfo, error) { emptyStrToSliceDecodeHook := func(from reflect.Type, to reflect.Type, data any) (any, error) { if from.Kind() == reflect.String && to.Kind() == reflect.Slice { strData, ok := data.(string) @@ -178,7 +196,12 @@ func CreateOAuthInfoFromKeyValues(settingsKV map[string]any) (*social.OAuthInfo, if strData == "" { return []string{}, nil } - return util.SplitString(strData), nil + + splitStr, err := util.SplitStringWithError(strData) + if err != nil && parsingWarns != nil { + *parsingWarns = append(*parsingWarns, err) + } + return splitStr, nil } return data, nil } diff --git a/pkg/login/social/connectors/generic_oauth.go b/pkg/login/social/connectors/generic_oauth.go index eb4a32f8381..15989c0df93 100644 --- a/pkg/login/social/connectors/generic_oauth.go +++ b/pkg/login/social/connectors/generic_oauth.go @@ -53,6 +53,18 @@ type SocialGenericOAuth struct { } func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGenericOAuth { + s := newSocialBase(social.GenericOAuthProviderName, orgRoleMapper, info, features, cfg) + + teamIds, err := util.SplitStringWithError(info.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + provider := &SocialGenericOAuth{ SocialBase: newSocialBase(social.GenericOAuthProviderName, orgRoleMapper, info, features, cfg), teamsUrl: info.TeamsUrl, @@ -63,8 +75,8 @@ func NewGenericOAuthProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMa loginAttributePath: info.Extra[loginAttributePathKey], idTokenAttributeName: info.Extra[idTokenAttributeNameKey], teamIdsAttributePath: info.TeamIdsAttributePath, - teamIds: util.SplitString(info.Extra[teamIdsKey]), - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + teamIds: teamIds, + allowedOrganizations: allowedOrganizations, } if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { @@ -118,7 +130,7 @@ func validateTeamsUrlWhenNotEmpty(info *social.OAuthInfo, requester identity.Req } func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GenericOAuthProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } @@ -128,6 +140,15 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS s.updateInfo(ctx, social.GenericOAuthProviderName, newInfo) + teamIds, err := util.SplitStringWithError(newInfo.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GenericOAuthProviderName, "error", err) + } + s.teamsUrl = newInfo.TeamsUrl s.emailAttributeName = newInfo.EmailAttributeName s.emailAttributePath = newInfo.EmailAttributePath @@ -136,8 +157,8 @@ func (s *SocialGenericOAuth) Reload(ctx context.Context, settings ssoModels.SSOS s.loginAttributePath = newInfo.Extra[loginAttributePathKey] s.idTokenAttributeName = newInfo.Extra[idTokenAttributeNameKey] s.teamIdsAttributePath = newInfo.TeamIdsAttributePath - s.teamIds = util.SplitString(newInfo.Extra[teamIdsKey]) - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.teamIds = teamIds + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/github_oauth.go b/pkg/login/social/connectors/github_oauth.go index 124b642f822..f5f0b43b3f3 100644 --- a/pkg/login/social/connectors/github_oauth.go +++ b/pkg/login/social/connectors/github_oauth.go @@ -62,13 +62,23 @@ var ( ) func NewGitHubProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGithub { - teamIdsSplitted := util.SplitString(info.Extra[teamIdsKey]) + s := newSocialBase(social.GitHubProviderName, orgRoleMapper, info, features, cfg) + + teamIdsSplitted, err := util.SplitStringWithError(info.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GitHubProviderName, "error", err) + } teamIds := mustInts(teamIdsSplitted) + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GitHubProviderName, "error", err) + } + provider := &SocialGithub{ - SocialBase: newSocialBase(social.GitHubProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, teamIds: teamIds, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, } if len(teamIdsSplitted) != len(teamIds) { @@ -117,14 +127,22 @@ func teamIdsNumbersValidator(info *social.OAuthInfo, requester identity.Requeste } func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GitHubProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } - teamIdsSplitted := util.SplitString(newInfo.Extra[teamIdsKey]) + teamIdsSplitted, err := util.SplitStringWithError(newInfo.Extra[teamIdsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", teamIdsKey, "provider", social.GitHubProviderName, "error", err) + } teamIds := mustInts(teamIdsSplitted) + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GitHubProviderName, "error", err) + } + if len(teamIdsSplitted) != len(teamIds) { s.log.Warn("Failed to parse team ids. Team ids must be a list of numbers.", "teamIds", teamIdsSplitted) } @@ -135,7 +153,7 @@ func (s *SocialGithub) Reload(ctx context.Context, settings ssoModels.SSOSetting s.updateInfo(ctx, social.GitHubProviderName, newInfo) s.teamIds = teamIds - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/gitlab_oauth.go b/pkg/login/social/connectors/gitlab_oauth.go index d51544dd7c7..7497c24d619 100644 --- a/pkg/login/social/connectors/gitlab_oauth.go +++ b/pkg/login/social/connectors/gitlab_oauth.go @@ -87,7 +87,7 @@ func (s *SocialGitlab) Validate(ctx context.Context, newSettings ssoModels.SSOSe } func (s *SocialGitlab) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GitlabProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/connectors/google_oauth.go b/pkg/login/social/connectors/google_oauth.go index 3044548948b..113b4c0ef5a 100644 --- a/pkg/login/social/connectors/google_oauth.go +++ b/pkg/login/social/connectors/google_oauth.go @@ -87,7 +87,7 @@ func (s *SocialGoogle) Validate(ctx context.Context, newSettings ssoModels.SSOSe } func (s *SocialGoogle) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GoogleProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/connectors/grafana_com_oauth.go b/pkg/login/social/connectors/grafana_com_oauth.go index 3f016b1f01c..84ea01632d1 100644 --- a/pkg/login/social/connectors/grafana_com_oauth.go +++ b/pkg/login/social/connectors/grafana_com_oauth.go @@ -39,15 +39,22 @@ type OrgRecord struct { } func NewGrafanaComProvider(info *social.OAuthInfo, cfg *setting.Cfg, orgRoleMapper *OrgRoleMapper, ssoSettings ssosettings.Service, features featuremgmt.FeatureToggles) *SocialGrafanaCom { + s := newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg) + // Override necessary settings info.AuthUrl = cfg.GrafanaComURL + "/oauth2/authorize" info.TokenUrl = cfg.GrafanaComURL + "/api/oauth2/token" info.AuthStyle = "inheader" + allowedOrganizations, err := util.SplitStringWithError(info.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GrafanaComProviderName, "error", err) + } + provider := &SocialGrafanaCom{ - SocialBase: newSocialBase(social.GrafanaComProviderName, orgRoleMapper, info, features, cfg), + SocialBase: s, url: cfg.GrafanaComURL, - allowedOrganizations: util.SplitString(info.Extra[allowedOrganizationsKey]), + allowedOrganizations: allowedOrganizations, } if features.IsEnabledGlobally(featuremgmt.FlagSsoSettingsApi) { @@ -80,11 +87,16 @@ func (s *SocialGrafanaCom) Validate(ctx context.Context, newSettings ssoModels.S } func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.GrafanaComProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } + allowedOrganizations, err := util.SplitStringWithError(newInfo.Extra[allowedOrganizationsKey]) + if err != nil { + s.log.Error("Invalid auth configuration setting", "config", allowedOrganizationsKey, "provider", social.GrafanaComProviderName, "error", err) + } + // Override necessary settings newInfo.AuthUrl = s.cfg.GrafanaComURL + "/oauth2/authorize" newInfo.TokenUrl = s.cfg.GrafanaComURL + "/api/oauth2/token" @@ -96,7 +108,7 @@ func (s *SocialGrafanaCom) Reload(ctx context.Context, settings ssoModels.SSOSet s.updateInfo(ctx, social.GrafanaComProviderName, newInfo) s.url = s.cfg.GrafanaComURL - s.allowedOrganizations = util.SplitString(newInfo.Extra[allowedOrganizationsKey]) + s.allowedOrganizations = allowedOrganizations return nil } diff --git a/pkg/login/social/connectors/okta_oauth.go b/pkg/login/social/connectors/okta_oauth.go index b126c2acd1d..ffa3a32a350 100644 --- a/pkg/login/social/connectors/okta_oauth.go +++ b/pkg/login/social/connectors/okta_oauth.go @@ -84,7 +84,7 @@ func (s *SocialOkta) Validate(ctx context.Context, newSettings ssoModels.SSOSett } func (s *SocialOkta) Reload(ctx context.Context, settings ssoModels.SSOSettings) error { - newInfo, err := CreateOAuthInfoFromKeyValues(settings.Settings) + newInfo, err := CreateOAuthInfoFromKeyValuesWithLogging(s.log, social.OktaProviderName, settings.Settings) if err != nil { return ssosettings.ErrInvalidSettings.Errorf("SSO settings map cannot be converted to OAuthInfo: %v", err) } diff --git a/pkg/login/social/socialimpl/service.go b/pkg/login/social/socialimpl/service.go index ddfb9bf7016..65a9a5573cc 100644 --- a/pkg/login/social/socialimpl/service.go +++ b/pkg/login/social/socialimpl/service.go @@ -65,7 +65,7 @@ func ProvideService(cfg *setting.Cfg, continue } - info, err := connectors.CreateOAuthInfoFromKeyValues(ssoSetting.Settings) + info, err := connectors.CreateOAuthInfoFromKeyValuesWithLogging(ss.log, ssoSetting.Provider, ssoSetting.Settings) if err != nil { ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", ssoSetting.Provider) continue @@ -85,7 +85,7 @@ func ProvideService(cfg *setting.Cfg, settingsKVs := convertIniSectionToMap(sec) - info, err := connectors.CreateOAuthInfoFromKeyValues(settingsKVs) + info, err := connectors.CreateOAuthInfoFromKeyValuesWithLogging(ss.log, name, settingsKVs) if err != nil { ss.log.Error("Failed to create OAuthInfo for provider", "error", err, "provider", name) continue diff --git a/pkg/util/strings.go b/pkg/util/strings.go index f3a2d35540f..b3bbed21cf2 100644 --- a/pkg/util/strings.go +++ b/pkg/util/strings.go @@ -33,9 +33,18 @@ func stringsFallback(vals ...string) string { // SplitString splits a string and returns a list of strings. It supports JSON list syntax and strings separated by commas or spaces. // It supports quoted strings with spaces, e.g. "foo bar", "baz". +// It will return an empty list if it fails to parse the string. func SplitString(str string) []string { + result, _ := SplitStringWithError(str) + return result +} + +// SplitStringWithError splits a string and returns a list of strings. It supports JSON list syntax and strings separated by commas or spaces. +// It supports quoted strings with spaces, e.g. "foo bar", "baz". +// It returns an error if it cannot parse the string. +func SplitStringWithError(str string) ([]string, error) { if len(str) == 0 { - return []string{} + return []string{}, nil } // JSON list syntax support @@ -43,9 +52,9 @@ func SplitString(str string) []string { var res []string err := json.Unmarshal([]byte(str), &res) if err != nil { - return []string{} + return []string{}, fmt.Errorf("incorrect format: %s", str) } - return res + return res, nil } matches := stringListItemMatcher.FindAllString(str, -1) @@ -55,7 +64,7 @@ func SplitString(str string) []string { result[i] = strings.Trim(match, "\"") } - return result + return result, nil } // GetAgeString returns a string representing certain time from years to minutes. diff --git a/public/app/features/auth-config/utils/data.ts b/public/app/features/auth-config/utils/data.ts index c0beae9ec27..00a3dd84453 100644 --- a/public/app/features/auth-config/utils/data.ts +++ b/public/app/features/auth-config/utils/data.ts @@ -56,7 +56,10 @@ const strToValue = (val: string | string[]): SelectableValue[] => { } // Stored as JSON Array if (val.startsWith('[') && val.endsWith(']')) { - return JSON.parse(val).map((v: string) => ({ label: v, value: v })); + // Fallback to parsing it like a non-json string if it is not valid json, instead of crashing. + try { + return JSON.parse(val).map((v: string) => ({ label: v, value: v })); + } catch {} } return val.split(/[\s,]/).map((s) => ({ label: s, value: s })); From 02118cc6aad41160743d5490bedcc3eacb366aed Mon Sep 17 00:00:00 2001 From: Ashley Harrison Date: Thu, 13 Feb 2025 17:44:13 +0000 Subject: [PATCH 15/18] Chore: Automerge i18n PRs (#99555) * add enable automerge step and update CODEOWNERS * add approver steps * move automerge step to pr approver token * get vault secrets * update workflow permissions * remove local --- .github/CODEOWNERS | 5 ++ .github/workflows/i18n-crowdin-download.yml | 57 +++++++++++++++++---- 2 files changed, 52 insertions(+), 10 deletions(-) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a349655800b..f10a2d3b391 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -381,6 +381,11 @@ /crowdin.yml @grafana/grafana-frontend-platform /public/locales/ @grafana/grafana-frontend-platform +/public/locales/de-DE @grafanabot +/public/locales/es-ES @grafanabot +/public/locales/fr-FR @grafanabot +/public/locales/pt-BR @grafanabot +/public/locales/zh-Hans @grafanabot /public/app/core/internationalization/ @grafana/grafana-frontend-platform /e2e/ @grafana/grafana-frontend-platform /e2e/cloud-plugins-suite/ @grafana/partner-datasources diff --git a/.github/workflows/i18n-crowdin-download.yml b/.github/workflows/i18n-crowdin-download.yml index cba14abe43b..e0c3c50b9bb 100644 --- a/.github/workflows/i18n-crowdin-download.yml +++ b/.github/workflows/i18n-crowdin-download.yml @@ -3,7 +3,7 @@ name: Crowdin Download Action on: workflow_dispatch: schedule: - - cron: "0 * * * *" + - cron: "0 0 * * *" jobs: download-sources-from-crowdin: @@ -12,6 +12,7 @@ jobs: permissions: contents: write # needed to commit changes into the PR pull-requests: write # needed to update PR description, labels, etc + id-token: write # needed to get vault secrets steps: - name: Generate token @@ -41,17 +42,11 @@ jobs: pull_request_body: | :robot: Automatic download of translations from Crowdin. - Steps for merging: - 1. A quick sanity check of the changes and approve. Things to look out for: - - No changes in the English file. The source of truth is in the main branch, NOT in Crowdin. - - Translations maybe be removed if the English phrase was removed, but there should not be many of these - - Anything else that looks 'funky'. Ask if you're not sure. - 2. Approve & (Auto-)merge. :tada: + This runs once per day and will merge automatically if all the required checks pass. - If there's a conflict, close the pull request and **delete the branch**. A GH action will recreate the pull request. - Remember, the longer this pull request is open, the more likely it is that it'll get conflicts. + If there's a conflict, close the pull request and **delete the branch**. + You can then either wait for the schedule to trigger a new PR, or rerun the action manually. pull_request_labels: 'area/frontend, area/internationalization, no-changelog, no-backport' - pull_request_reviewers: 'grafana-frontend-platform' pull_request_base_branch_name: 'main' base_url: 'https://grafana.api.crowdin.com' config: 'crowdin.yml' @@ -119,3 +114,45 @@ jobs: with: pr: ${{ steps.crowdin-download.outputs.pull_request_number }} token: ${{ steps.generate_token.outputs.token }} + + - name: Get vault secrets + id: vault-secrets + uses: grafana/shared-workflows/actions/get-vault-secrets@main + with: + # Secrets placed in ci/repo/grafana/grafana/grafana-pr-approver + repo_secrets: | + GRAFANA_PR_APPROVER_APP_ID=grafana-pr-approver:app-id + GRAFANA_PR_APPROVER_APP_PEM=grafana-pr-approver:private-key + + - name: Generate approver token + if: steps.crowdin-download.outputs.pull_request_url + id: generate_approver_token + uses: tibdex/github-app-token@b62528385c34dbc9f38e5f4225ac829252d1ea92 + with: + app_id: ${{ env.GRAFANA_PR_APPROVER_APP_ID }} + private_key: ${{ env.GRAFANA_PR_APPROVER_APP_PEM }} + + - name: Approve and automerge PR + if: steps.crowdin-download.outputs.pull_request_url + shell: bash + # Only approve if: + # - the PR does not modify files other than json files under the public/locales/ directory + # - the PR does not modify the en-US locale + run: | + filesChanged=$(gh pr diff --name-only ${{ steps.crowdin-download.outputs.pull_request_url }}) + + if [[ $(echo $filesChanged | grep -v 'public/locales/[a-zA-Z\-]*/grafana.json' | wc -l) -ne 0 ]]; then + echo "Non-i18n changes detected, not approving" + exit 1 + fi + + if [[ $(echo $filesChanged | grep "public/locales/en-US" | wc -l) -ne 0 ]]; then + echo "public/locales/en-US changes detected, not approving" + exit 1 + fi + + echo "Approving and enabling automerge" + gh pr review ${{ steps.crowdin-download.outputs.pull_request_url }} --approve + gh pr merge --auto --squash ${{ steps.crowdin-download.outputs.pull_request_url }} + env: + GITHUB_TOKEN: ${{ steps.generate_approver_token.outputs.token }} From 5aeaa18ac2d4c8866b774b8b9bd175d216c0e065 Mon Sep 17 00:00:00 2001 From: Adela Almasan <88068998+adela-almasan@users.noreply.github.com> Date: Thu, 13 Feb 2025 11:46:29 -0600 Subject: [PATCH 16/18] Canvas: One click links and actions (#99616) Co-authored-by: Leon Sorokin --- .../VizTooltip/VizTooltipFooter.tsx | 95 +++++++---- public/app/features/actions/utils.ts | 1 + public/app/features/canvas/element.ts | 3 +- public/app/features/canvas/elements/cloud.tsx | 3 +- .../features/canvas/elements/droneFront.tsx | 3 +- .../features/canvas/elements/droneSide.tsx | 3 +- .../app/features/canvas/elements/droneTop.tsx | 3 +- .../app/features/canvas/elements/ellipse.tsx | 3 +- public/app/features/canvas/elements/icon.tsx | 3 +- .../features/canvas/elements/metricValue.tsx | 9 +- .../canvas/elements/parallelogram.tsx | 3 +- .../features/canvas/elements/rectangle.tsx | 3 +- .../canvas/elements/server/server.tsx | 3 +- public/app/features/canvas/elements/text.tsx | 3 +- .../app/features/canvas/elements/triangle.tsx | 3 +- .../features/canvas/elements/windTurbine.tsx | 3 +- .../app/features/canvas/runtime/element.tsx | 148 ++++++++++++------ public/app/features/canvas/runtime/scene.tsx | 2 + .../app/plugins/panel/canvas/CanvasPanel.tsx | 5 + .../panel/canvas/components/CanvasTooltip.tsx | 1 - .../canvas/editor/element/elementEditor.tsx | 35 +---- .../plugins/panel/canvas/migrations.test.ts | 14 +- public/app/plugins/panel/canvas/migrations.ts | 23 ++- public/app/plugins/panel/canvas/module.tsx | 1 - public/locales/en-US/grafana.json | 1 + public/locales/pseudo-LOCALE/grafana.json | 1 + 26 files changed, 218 insertions(+), 157 deletions(-) diff --git a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx index c3c4a34eb64..5b79c42711f 100644 --- a/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx +++ b/packages/grafana-ui/src/components/VizTooltip/VizTooltipFooter.tsx @@ -1,11 +1,13 @@ import { css } from '@emotion/css'; +import { useMemo } from 'react'; -import { ActionModel, Field, GrafanaTheme2, LinkModel } from '@grafana/data'; +import { ActionModel, Field, GrafanaTheme2, LinkModel, ThemeSpacingTokens } from '@grafana/data'; import { Button, DataLinkButton, Icon, Stack } from '..'; import { useStyles2 } from '../../themes'; import { Trans } from '../../utils/i18n'; import { ActionButton } from '../Actions/ActionButton'; +import { ResponsiveProp } from '../Layout/utils/responsiveness'; interface VizTooltipFooterProps { dataLinks: Array>; @@ -15,50 +17,75 @@ interface VizTooltipFooterProps { export const ADD_ANNOTATION_ID = 'add-annotation-button'; -const renderDataLinks = (dataLinks: LinkModel[], styles: ReturnType) => { - const oneClickLink = dataLinks.find((link) => link.oneClick === true); +type RenderOneClickTrans = (title: string) => React.ReactNode; +type RenderItem = ( + item: T, + idx: number, + styles: ReturnType +) => React.ReactNode; + +function makeRenderLinksOrActions( + renderOneClickTrans: RenderOneClickTrans, + renderItem: RenderItem, + itemGap?: ResponsiveProp +) { + const renderLinksOrActions = (items: T[], styles: ReturnType) => { + if (items.length === 0) { + return; + } + + const oneClickItem = items.find((item) => item.oneClick === true); + + if (oneClickItem != null) { + return ( +
+ + + + {renderOneClickTrans(oneClickItem.title)} + + +
+ ); + } - if (oneClickLink != null) { return ( - - - - - Click to open {{ linkTitle: oneClickLink.title }} - - - +
+ + {items.map((item, i) => renderItem(item, i, styles))} + +
); - } + }; - return ( - - {dataLinks.map((link, i) => ( - - ))} - - ); -}; + return renderLinksOrActions; +} -const renderActions = (actions: ActionModel[]) => { - return ( - - {actions.map((action, i) => ( - - ))} - - ); -}; +const renderDataLinks = makeRenderLinksOrActions( + (title) => ( + Click to open {{ linkTitle: title }} + ), + (item, i, styles) => ( + + ), + 0.5 +); + +const renderActions = makeRenderLinksOrActions( + (title) => Click to {{ actionTitle: title }}, + (item, i, styles) => +); export const VizTooltipFooter = ({ dataLinks, actions = [], annotate }: VizTooltipFooterProps) => { const styles = useStyles2(getStyles); - const hasOneClickLink = dataLinks.some((link) => link.oneClick === true); + const hasOneClickLink = useMemo(() => dataLinks.some((link) => link.oneClick === true), [dataLinks]); + const hasOneClickAction = useMemo(() => actions.some((action) => action.oneClick === true), [actions]); return (
- {dataLinks.length > 0 &&
{renderDataLinks(dataLinks, styles)}
} - {!hasOneClickLink && actions.length > 0 &&
{renderActions(actions)}
} - {!hasOneClickLink && annotate != null && ( + {!hasOneClickAction && renderDataLinks(dataLinks, styles)} + {!hasOneClickLink && renderActions(actions, styles)} + {!hasOneClickLink && !hasOneClickAction && annotate != null && (