mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'main' into remove-managed-install-feature-flag
This commit is contained in:
commit
b5a04cb5ef
@ -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.
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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))
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -457,6 +457,8 @@ message ResourceSearchRequest {
|
||||
bool is_deleted = 10;
|
||||
|
||||
int64 page = 11;
|
||||
|
||||
int64 permission = 12;
|
||||
}
|
||||
|
||||
message ResourceSearchResponse {
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
|
@ -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() }) });
|
||||
}
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
62
yarn.lock
62
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
|
||||
|
||||
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user