Merge branch 'main' into remove-managed-install-feature-flag

This commit is contained in:
Hugo Oshiro 2025-02-13 17:49:29 +01:00
commit b5a04cb5ef
24 changed files with 211 additions and 657 deletions

View File

@ -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 '<grafana_url>/api/access-control/roles/basic_viewer'
-X PUT -d @/tmp/basic_viewer.json '<grafana_url>/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 '<grafana_url>/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 '<grafana_url>/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.

View File

@ -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)

View File

@ -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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/administration/correlations/correlation-configuration/#target-query).
1. Build a target query using [variables syntax](/docs/grafana/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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/<GRAFANA_VERSION>/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.

View File

@ -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');
});
});

View File

@ -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');
});
});

View File

@ -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:*",
@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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{

View File

@ -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
}

View File

@ -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))
}

View File

@ -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)

View File

@ -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))

View File

@ -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 {

View File

@ -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,

View File

@ -457,6 +457,8 @@ message ResourceSearchRequest {
bool is_deleted = 10;
int64 page = 11;
int64 permission = 12;
}
message ResourceSearchResponse {

View File

@ -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)

View File

@ -38,7 +38,7 @@ export class ServiceAccountPicker extends Component<Props, State> {
}
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,

View File

@ -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<T = object, S = object, K = string> implements
return getBackendSrv().get<Resource<T, S, K>>(`${this.url}/${name}`);
}
public watch(opts?: WatchOptions): Observable<ResourceEvent<T, S, K>> {
public watch(
params?: WatchOptions,
config?: Pick<BackendSrvRequest, 'data' | 'method'>
): Observable<ResourceEvent<T, S, K>> {
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),

View File

@ -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() }) });
}
});

View File

@ -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();

View File

@ -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');
});
});

View File

@ -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;
}

View File

@ -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
@ -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"
@ -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"
@ -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