Compare commits

..

5 Commits

Author SHA1 Message Date
Will Browne c3c5229986 undo go.mod change 2026-01-13 16:22:49 +00:00
Will Browne 7a2775b1a7 make update-workspace 2026-01-13 16:06:59 +00:00
Will Browne d25a8f5e72 fix lint issues 2026-01-13 15:59:06 +00:00
Will Browne 48d032e5aa undo stale comment 2026-01-13 15:58:25 +00:00
Will Browne ae06690681 move loading strategy into pkg/plugins 2026-01-13 15:46:34 +00:00
37 changed files with 334 additions and 453 deletions
+1
View File
@@ -30,6 +30,7 @@ require (
require (
cel.dev/expr v0.25.1 // indirect
github.com/Machiel/slugify v1.0.1 // indirect
github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/NYTimes/gziphandler v1.1.1 // indirect
github.com/ProtonMail/go-crypto v1.3.0 // indirect
github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+2
View File
@@ -9,6 +9,8 @@ github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2
github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ=
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/NYTimes/gziphandler v1.1.1 h1:ZUDjpQae29j0ryrS0u/B8HZfJBtBQHjqw2rQ2cqUQ3I=
github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMomdKFjzJNB0c=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+4 -17
View File
@@ -4,7 +4,6 @@ import (
"context"
"time"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
@@ -12,25 +11,16 @@ const (
defaultLocalTTL = 1 * time.Hour
)
// PluginAssetsCalculator is an interface for calculating plugin asset information.
// LocalProvider requires this to calculate loading strategy.
type PluginAssetsCalculator interface {
LoadingStrategy(ctx context.Context, p pluginstore.Plugin) plugins.LoadingStrategy
}
// LocalProvider retrieves plugin metadata for locally installed plugins.
// It uses the plugin store to access plugins that have already been loaded.
type LocalProvider struct {
store pluginstore.Store
pluginAssets PluginAssetsCalculator
store pluginstore.Store
}
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
// pluginAssets is required for calculating loading strategy.
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
func NewLocalProvider(pluginStore pluginstore.Store) *LocalProvider {
return &LocalProvider{
store: pluginStore,
pluginAssets: pluginAssets,
store: pluginStore,
}
}
@@ -41,10 +31,7 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (
return nil, ErrMetaNotFound
}
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
moduleHash := plugin.ModuleHash
spec := pluginStorePluginToMeta(plugin, loadingStrategy, moduleHash)
spec := pluginStorePluginToMeta(plugin, plugin.LoadingStrategy, plugin.ModuleHash)
return &Result{
Meta: spec,
TTL: defaultLocalTTL,
@@ -35,10 +35,10 @@ For Grafana Cloud users, Grafana Support is not authorised to make org role chan
## Grafana server administrators
A Grafana server administrator (sometimes referred to as a **Grafana Admin**) manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
A Grafana server administrator manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
{{< admonition type="caution" >}}
The server administrator role is distinct from the [organization administrator](#organization-roles) role.
{{< admonition type="note" >}}
The server administrator role does not mean that the user is also a Grafana [organization administrator](#organization-roles).
{{< /admonition >}}
A server administrator can perform the following tasks:
@@ -50,7 +50,7 @@ A server administrator can perform the following tasks:
- Upgrade the server to Grafana Enterprise.
{{< admonition type="note" >}}
The server administrator (Grafana Admin) role does not exist in Grafana Cloud.
The server administrator role does not exist in Grafana Cloud.
{{< /admonition >}}
To assign or remove server administrator privileges, see [Server user management](../user-management/server-user-management/assign-remove-server-admin-privileges/).
@@ -53,11 +53,6 @@ refs:
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/
rbac-terraform-provisioning:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-terraform-provisioning/
- pattern: /docs/grafana-cloud/
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/rbac-terraform-provisioning/
rbac-grafana-provisioning:
- pattern: /docs/grafana/
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/
@@ -150,13 +145,7 @@ Refer to the [RBAC HTTP API](ref:api-rbac-get-a-role) for more details.
## Create custom roles
This section shows you how to create a custom RBAC role using Grafana provisioning or the HTTP API.
Creating and editing custom roles is not currently possible in the Grafana UI. To manage custom roles, use one of the following methods:
- [Provisioning](ref:rbac-grafana-provisioning) (for self-managed instances)
- [HTTP API](ref:api-rbac-create-a-new-custom-role)
- [Terraform](ref:rbac-terraform-provisioning)
This section shows you how to create a custom RBAC role using Grafana provisioning and the HTTP API.
Create a custom role when basic roles and fixed roles do not meet your permissions requirements.
@@ -164,101 +153,14 @@ Create a custom role when basic roles and fixed roles do not meet your permissio
- [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy).
- Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions, actions, and scopes](ref:custom-role-actions-scopes).
- [Enable role provisioning](ref:rbac-grafana-provisioning).
- Ensure that you have permissions to create a custom role.
- By default, the Grafana Admin role has permission to create custom roles.
- A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope.
### Create custom roles using the HTTP API
### Create custom roles using provisioning
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
{{< admonition type="note" >}}
When you create a custom role you can only give it the same permissions you already have. For example, if you only have `users:create` permissions, then you can't create a role that includes other permissions.
{{< /admonition >}}
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
**Example request**
```
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
}
]
}'
```
**Example response**
```
{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
"updated": "2021-05-17T22:07:31.569936+02:00",
"created": "2021-05-17T22:07:31.569935+02:00"
}
],
"updated": "2021-05-17T22:07:31.564403+02:00",
"created": "2021-05-17T22:07:31.564403+02:00"
}
```
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
### Create custom roles using Terraform
You can use the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) to manage custom roles and their assignments. This is the recommended method for Grafana Cloud users who want to manage RBAC as code. For more information, refer to [Provisioning RBAC with Terraform](ref:rbac-terraform-provisioning).
The following example creates a custom role and assigns it to a team:
```terraform
resource "grafana_role" "custom_folder_manager" {
name = "custom:folders:manager"
description = "Custom role for reading and creating folders"
uid = "custom-folders-manager"
version = 1
global = true
permissions {
action = "folders:read"
scope = "folders:*"
}
permissions {
action = "folders:create"
scope = "folders:uid:general" # Allows creating folders at the root level
}
}
resource "grafana_role_assignment" "custom_folder_manager_assignment" {
role_uid = grafana_role.custom_folder_manager.uid
teams = ["<TEAM_UID>"]
}
```
For more information, refer to the [`grafana_role`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role) and [`grafana_role_assignment`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role_assignment) documentation in the Terraform Registry.
### Create custom roles using file-based provisioning
You can use [file-based provisioning](ref:rbac-grafana-provisioning) to create custom roles for self-managed instances.
[File-based provisioning](ref:rbac-grafana-provisioning) is one method you can use to create custom roles.
1. Open the YAML configuration file and locate the `roles` section.
@@ -349,6 +251,61 @@ roles:
state: 'absent'
```
### Create custom roles using the HTTP API
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
{{< admonition type="note" >}}
You cannot create a custom role with permissions that you do not have. For example, if you only have `users:create` permissions, then you cannot create a role that includes other permissions.
{{< /admonition >}}
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
**Example request**
```
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
--header 'Content-Type: application/json' \
--data-raw '{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
}
]
}'
```
**Example response**
```
{
"version": 1,
"uid": "jZrmlLCkGksdka",
"name": "custom:users:admin",
"displayName": "custom users admin",
"description": "My custom role which gives users permissions to create users",
"global": true,
"permissions": [
{
"action": "users:create"
"updated": "2021-05-17T22:07:31.569936+02:00",
"created": "2021-05-17T22:07:31.569935+02:00"
}
],
"updated": "2021-05-17T22:07:31.564403+02:00",
"created": "2021-05-17T22:07:31.564403+02:00"
}
```
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
## Update basic role permissions
If the default basic role definitions do not meet your requirements, you can change their permissions.
@@ -6,6 +6,7 @@ description: Learn about RBAC Grafana provisioning and view an example YAML prov
file that configures Grafana role assignments.
labels:
products:
- cloud
- enterprise
menuTitle: Provisioning RBAC with Grafana
title: Provisioning RBAC with Grafana
@@ -51,13 +52,11 @@ refs:
# Provisioning RBAC with Grafana
{{< admonition type="note" >}}
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) for self-managed instances. This feature is not available in Grafana Cloud.
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud).
{{< /admonition >}}
You can create, change or remove [Custom roles](ref:manage-rbac-roles-create-custom-roles-using-provisioning) and create or remove [basic role assignments](ref:assign-rbac-roles-assign-a-fixed-role-to-a-basic-role-using-provisioning), by adding one or more YAML configuration files in the `provisioning/access-control/` directory.
Because this method requires access to the file system where Grafana is running, it's only available for self-managed Grafana instances. To provision RBAC in Grafana Cloud, use [Terraform](ref:rbac-terraform-provisioning) or the [HTTP API](ref:api-rbac-create-and-manage-custom-roles).
Grafana performs provisioning during startup. After you make a change to the configuration file, you can reload it during runtime. You do not need to restart the Grafana server for your changes to take effect.
**Before you begin:**
-5
View File
@@ -984,11 +984,6 @@ export interface FeatureToggles {
*/
recentlyViewedDashboards?: boolean;
/**
* A/A test for recently viewed dashboards feature
* @default false
*/
experimentRecentlyViewedDashboards?: boolean;
/**
* Enable configuration of alert enrichments in Grafana Cloud.
* @default false
*/
+3 -3
View File
@@ -170,7 +170,7 @@ func (hs *HTTPServer) getFrontendSettings(c *contextmodel.ReqContext) (*dtos.Fro
Signature: string(panel.Signature),
Sort: getPanelSort(panel.ID),
Angular: panel.Angular,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), panel),
LoadingStrategy: panel.LoadingStrategy,
Translations: panel.Translations,
}
}
@@ -531,7 +531,7 @@ func (hs *HTTPServer) getFSDataSources(c *contextmodel.ReqContext, availablePlug
BaseURL: plugin.BaseURL,
Angular: plugin.Angular,
MultiValueFilterOperators: plugin.MultiValueFilterOperators,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
LoadingStrategy: plugin.LoadingStrategy,
Translations: plugin.Translations,
}
@@ -638,7 +638,7 @@ func (hs *HTTPServer) newAppDTO(ctx context.Context, plugin pluginstore.Plugin,
Path: plugin.Module,
Preload: false,
Angular: plugin.Angular,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
LoadingStrategy: plugin.LoadingStrategy,
Extensions: plugin.Extensions,
Dependencies: plugin.Dependencies,
ModuleHash: plugin.ModuleHash,
+20 -41
View File
@@ -31,7 +31,6 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/licensing"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
"github.com/grafana/grafana/pkg/services/rendering"
@@ -44,7 +43,7 @@ import (
"github.com/grafana/grafana/pkg/web"
)
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service, passets *pluginassets.Service) (*web.Mux, *HTTPServer) {
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service) (*web.Mux, *HTTPServer) {
t.Helper()
db.InitTestDB(t)
// nolint:staticcheck
@@ -75,11 +74,6 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
pluginsSettings = &pluginsettings.FakePluginSettings{}
}
var pluginsAssets = passets
if pluginsAssets == nil {
pluginsAssets = pluginassets.ProvideService(pluginsCfg, pluginsCDN, pluginStore)
}
hs := &HTTPServer{
authnService: &authntest.FakeService{},
Cfg: cfg,
@@ -96,7 +90,6 @@ func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.F
AccessControl: accesscontrolmock.New(),
PluginSettings: pluginsSettings,
pluginsCDNService: pluginsCDN,
pluginAssets: pluginsAssets,
namespacer: request.GetNamespaceMapper(cfg),
SocialService: socialimpl.ProvideService(cfg, features, &usagestats.UsageStatsMock{}, supportbundlestest.NewFakeBundleService(), remotecache.NewFakeCacheStorage(), nil, ssosettingstests.NewFakeService()),
managedPluginsService: managedplugins.NewNoop(),
@@ -129,7 +122,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testi
cfg.BuildVersion = "7.8.9"
cfg.BuildCommit = "01234567"
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
@@ -221,7 +214,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.
if test.mutateCfg != nil {
test.mutateCfg(cfg)
}
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
recorder := httptest.NewRecorder()
@@ -246,7 +239,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
desc string
pluginStore func() pluginstore.Store
pluginSettings func() pluginsettings.Service
pluginAssets func() *pluginassets.Service
expected settings
}{
{
@@ -263,7 +255,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Type: plugins.TypeApp,
Preload: true,
},
FS: &pluginfakes.FakePluginFS{},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -273,7 +266,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Plugins: newAppSettings("test-app", false),
}
},
pluginAssets: newPluginAssets(),
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
@@ -301,7 +293,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Type: plugins.TypeApp,
Preload: true,
},
FS: &pluginfakes.FakePluginFS{},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -311,7 +304,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Plugins: newAppSettings("test-app", true),
}
},
pluginAssets: newPluginAssets(),
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
@@ -338,8 +330,9 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Type: plugins.TypeApp,
Preload: true,
},
Angular: plugins.AngularMeta{Detected: true},
FS: &pluginfakes.FakePluginFS{},
Angular: plugins.AngularMeta{Detected: true},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyFetch,
},
},
}
@@ -349,7 +342,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Plugins: newAppSettings("test-app", true),
}
},
pluginAssets: newPluginAssets(),
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
@@ -376,6 +368,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Type: plugins.TypeApp,
Preload: true,
},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -385,13 +378,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Plugins: newAppSettings("test-app", true),
}
},
pluginAssets: newPluginAssetsWithConfig(&config.PluginManagementCfg{
PluginSettings: map[string]map[string]string{
"test-app": {
pluginassets.CreatePluginVersionCfgKey: pluginassets.CreatePluginVersionScriptSupportEnabled,
},
},
}),
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
@@ -421,6 +407,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
FS: &pluginfakes.FakePluginFS{TypeFunc: func() plugins.FSType {
return plugins.FSTypeCDN
}},
LoadingStrategy: plugins.LoadingStrategyFetch,
},
},
}
@@ -430,7 +417,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
Plugins: newAppSettings("test-app", true),
}
},
pluginAssets: newPluginAssets(),
expected: settings{
Apps: map[string]*plugins.AppDTO{
"test-app": {
@@ -448,7 +434,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := setting.NewCfg()
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings(), test.pluginAssets())
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings())
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
recorder := httptest.NewRecorder()
@@ -549,7 +535,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
FS: &pluginfakes.FakePluginFS{},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -599,7 +586,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
FS: &pluginfakes.FakePluginFS{},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -639,7 +627,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
"en-US": "public/plugins/test-app/locales/en-US/test-app.json",
"pt-BR": "public/plugins/test-app/locales/pt-BR/test-app.json",
},
FS: &pluginfakes.FakePluginFS{},
FS: &pluginfakes.FakePluginFS{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
}
@@ -667,7 +656,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
for _, test := range tests {
t.Run(test.desc, func(t *testing.T) {
cfg := setting.NewCfg()
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), nil, nil)
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), nil)
// Create a request with the appropriate context
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
@@ -704,13 +693,3 @@ func TestIntegrationHTTPServer_GetFrontendSettings_translations(t *testing.T) {
})
}
}
func newPluginAssets() func() *pluginassets.Service {
return newPluginAssetsWithConfig(&config.PluginManagementCfg{})
}
func newPluginAssetsWithConfig(pCfg *config.PluginManagementCfg) func() *pluginassets.Service {
return func() *pluginassets.Service {
return pluginassets.ProvideService(pCfg, pluginscdn.ProvideService(pCfg), &pluginstore.FakePluginStore{})
}
}
+1 -4
View File
@@ -82,7 +82,6 @@ import (
"github.com/grafana/grafana/pkg/services/playlist"
"github.com/grafana/grafana/pkg/services/plugindashboards"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
@@ -151,7 +150,6 @@ type HTTPServer struct {
pluginDashboardService plugindashboards.Service
pluginStaticRouteResolver plugins.StaticRouteResolver
pluginErrorResolver plugins.ErrorResolver
pluginAssets *pluginassets.Service
pluginPreinstall pluginchecker.Preinstall
SearchService search.Service
ShortURLService shorturls.Service
@@ -255,7 +253,7 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
encryptionService encryption.Internal, grafanaUpdateChecker *updatemanager.GrafanaService,
pluginsUpdateChecker *updatemanager.PluginsService, searchUsersService searchusers.Service,
dataSourcesService datasources.DataSourceService, queryDataService query.Service, pluginFileStore plugins.FileStore,
serviceaccountsService serviceaccounts.Service, pluginAssets *pluginassets.Service,
serviceaccountsService serviceaccounts.Service,
authInfoService login.AuthInfoService, storageService store.StorageService,
notificationService notifications.Service, dashboardService dashboards.DashboardService,
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
@@ -294,7 +292,6 @@ func ProvideHTTPServer(opts ServerOptions, cfg *setting.Cfg, routeRegister routi
pluginStore: pluginStore,
pluginStaticRouteResolver: pluginStaticRouteResolver,
pluginDashboardService: pluginDashboardService,
pluginAssets: pluginAssets,
pluginErrorResolver: pluginErrorResolver,
pluginFileStore: pluginFileStore,
grafanaUpdateChecker: grafanaUpdateChecker,
+1 -1
View File
@@ -209,7 +209,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
SignatureOrg: plugin.SignatureOrg,
SecureJsonFields: map[string]bool{},
AngularDetected: plugin.Angular.Detected,
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
LoadingStrategy: plugin.LoadingStrategy,
Extensions: plugin.Extensions,
Translations: plugin.Translations,
}
+4 -7
View File
@@ -41,7 +41,6 @@ import (
"github.com/grafana/grafana/pkg/services/org/orgtest"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
@@ -675,9 +674,10 @@ func Test_PluginsList_AccessControl(t *testing.T) {
func createPlugin(jd plugins.JSONData, class plugins.Class, files plugins.FS) *plugins.Plugin {
return &plugins.Plugin{
JSONData: jd,
Class: class,
FS: files,
JSONData: jd,
Class: class,
FS: files,
LoadingStrategy: plugins.LoadingStrategyScript,
}
}
@@ -844,9 +844,6 @@ func Test_PluginsSettings(t *testing.T) {
ErrorCode: tc.errCode,
})
}
pCfg := &config.PluginManagementCfg{}
pluginCDN := pluginscdn.ProvideService(pCfg)
hs.pluginAssets = pluginassets.ProvideService(pCfg, pluginCDN, hs.pluginStore)
hs.pluginErrorResolver = pluginerrs.ProvideStore(errTracker)
hs.pluginsUpdateChecker, err = updatemanager.ProvidePluginsService(
hs.Cfg,
+1
View File
@@ -4,6 +4,7 @@ go 1.25.5
require (
github.com/Machiel/slugify v1.0.1
github.com/Masterminds/semver/v3 v3.4.0
github.com/ProtonMail/go-crypto v1.3.0
github.com/gobwas/glob v0.2.3
github.com/google/go-cmp v0.7.0
+2
View File
@@ -2,6 +2,8 @@ github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEK
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Machiel/slugify v1.0.1 h1:EfWSlRWstMadsgzmiV7d0yVd2IFlagWH68Q+DcYCm4E=
github.com/Machiel/slugify v1.0.1/go.mod h1:fTFGn5uWEynW4CUMG7sWkYXOf1UgDxyTM3DbR6Qfg3k=
github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
@@ -129,6 +129,7 @@ func TestLoader_Load(t *testing.T) {
Class: plugins.ClassCore,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -242,6 +243,7 @@ func TestLoader_Load(t *testing.T) {
SignatureOrg: "Grafana Labs",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -294,6 +296,7 @@ func TestLoader_Load(t *testing.T) {
Signature: "unsigned",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -353,6 +356,7 @@ func TestLoader_Load(t *testing.T) {
Signature: plugins.SignatureStatusUnsigned,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -451,6 +455,7 @@ func TestLoader_Load(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -36,6 +36,7 @@ func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Servi
AppChildDecorateFunc(),
SkipHostEnvVarsDecorateFunc(cfg),
ModuleHashDecorateFunc(cfg, cdn),
LoadingStrategyDecorateFunc(cfg, cdn),
}
}
@@ -166,3 +167,11 @@ func ModuleHashDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Ser
return p, nil
}
}
// LoadingStrategyDecorateFunc returns a DecorateFunc that calculates and sets the loading strategy for the plugin.
func LoadingStrategyDecorateFunc(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) DecorateFunc {
return func(_ context.Context, p *plugins.Plugin) (*plugins.Plugin, error) {
p.LoadingStrategy = pluginassets.CalculateLoadingStrategy(p, cfg, cdn)
return p, nil
}
}
@@ -0,0 +1,67 @@
package pluginassets
import (
"github.com/Masterminds/semver/v3"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
)
const (
CreatePluginVersionCfgKey = "create_plugin_version"
CreatePluginVersionScriptSupportEnabled = "4.15.0"
)
var (
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
)
// CalculateLoadingStrategy calculates the loading strategy for a plugin.
// If a plugin has plugin setting `create_plugin_version` >= 4.15.0, set loadingStrategy to "script".
// If a plugin is not loaded via the CDN and is not Angular, set loadingStrategy to "script".
// Otherwise, set loadingStrategy to "fetch".
func CalculateLoadingStrategy(p *plugins.Plugin, cfg *config.PluginManagementCfg, cdn *pluginscdn.Service) plugins.LoadingStrategy {
if cfg != nil && cfg.PluginSettings != nil {
if pCfg, ok := cfg.PluginSettings[p.ID]; ok {
if compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// If the plugin has a parent
if p.Parent != nil {
// Check the parent's create_plugin_version setting
if pCfg, ok := cfg.PluginSettings[p.Parent.ID]; ok {
if compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// Since the parent plugin is not explicitly configured as script loading compatible,
// If the plugin is either loaded from the CDN (via its parent) or contains Angular, we should use fetch
if cdnEnabled(p.Parent, cdn) || p.Angular.Detected {
return plugins.LoadingStrategyFetch
}
}
}
if !cdnEnabled(p, cdn) && !p.Angular.Detected {
return plugins.LoadingStrategyScript
}
return plugins.LoadingStrategyFetch
}
// compatibleCreatePluginVersion checks if the create_plugin_version setting is >= 4.15.0
func compatibleCreatePluginVersion(ps map[string]string) bool {
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
createPluginVer, err := semver.NewVersion(cpv)
if err != nil {
// Invalid semver, treat as incompatible
return false
}
return !createPluginVer.LessThan(scriptLoadingMinSupportedVersion)
}
return false
}
@@ -1,20 +1,25 @@
package pluginassets
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/manager/pluginfakes"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
func TestService_Calculate(t *testing.T) {
// cdnFS is a simple mock FS that returns CDN type
type cdnFS struct {
plugins.FS
}
func (f *cdnFS) Type() plugins.FSType {
return plugins.FSTypeCDN
}
func TestCalculateLoadingStrategy(t *testing.T) {
const pluginID = "grafana-test-datasource"
const (
@@ -26,7 +31,7 @@ func TestService_Calculate(t *testing.T) {
tcs := []struct {
name string
pluginSettings config.PluginSettings
plugin pluginstore.Plugin
plugin *plugins.Plugin
expected plugins.LoadingStrategy
}{
{
@@ -34,7 +39,7 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: compatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false)),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false)),
expected: plugins.LoadingStrategyScript,
},
{
@@ -42,9 +47,11 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
CreatePluginVersionCfgKey: compatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), func(p *plugins.Plugin) {
p.Parent = &plugins.Plugin{
JSONData: plugins.JSONData{ID: "parent-datasource"},
FS: plugins.NewFakeFS(),
}
}),
expected: plugins.LoadingStrategyScript,
},
@@ -53,24 +60,21 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: futureVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when create-plugin version is not provided, plugin is not angular and is not configured as CDN enabled",
pluginSettings: newPluginSettings(pluginID, map[string]string{
// NOTE: cdn key is not set
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
pluginSettings: newPluginSettings(pluginID, map[string]string{}),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
name: "Expected LoadingStrategyScript when create-plugin version is not compatible, plugin is not angular, is not configured as CDN enabled and does not have a CDN fs",
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
// NOTE: cdn key is not set
}),
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal), withFS(plugins.NewFakeFS())),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withClassForLoadingStrategy(plugins.ClassExternal), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
{
@@ -80,9 +84,11 @@ func TestService_Calculate(t *testing.T) {
"cdn": "true",
},
},
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), func(p *plugins.Plugin) {
p.Parent = &plugins.Plugin{
JSONData: plugins.JSONData{ID: "parent-datasource"},
FS: plugins.NewFakeFS(),
}
}),
expected: plugins.LoadingStrategyFetch,
},
@@ -93,18 +99,22 @@ func TestService_Calculate(t *testing.T) {
"cdn": "true",
},
},
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), func(p *plugins.Plugin) {
p.Parent = &plugins.Plugin{
JSONData: plugins.JSONData{ID: "parent-datasource"},
FS: plugins.NewFakeFS(),
}
}),
expected: plugins.LoadingStrategyFetch,
},
{
name: "Expected LoadingStrategyFetch when parent create-plugin version is not set, is not configured as CDN enabled and plugin is angular",
pluginSettings: config.PluginSettings{},
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS()), func(p pluginstore.Plugin) pluginstore.Plugin {
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
return p
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), withFSForLoadingStrategy(plugins.NewFakeFS()), func(p *plugins.Plugin) {
p.Parent = &plugins.Plugin{
JSONData: plugins.JSONData{ID: "parent-datasource"},
FS: plugins.NewFakeFS(),
}
}),
expected: plugins.LoadingStrategyFetch,
},
@@ -114,7 +124,7 @@ func TestService_Calculate(t *testing.T) {
"cdn": "true",
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal)),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withClassForLoadingStrategy(plugins.ClassExternal), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyFetch,
},
{
@@ -122,7 +132,7 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS())),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyFetch,
},
{
@@ -131,7 +141,7 @@ func TestService_Calculate(t *testing.T) {
"cdn": "true",
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false)),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyFetch,
},
{
@@ -139,12 +149,8 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: incompatVersion,
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(
&pluginfakes.FakePluginFS{
TypeFunc: func() plugins.FSType {
return plugins.FSTypeCDN
},
},
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(
&cdnFS{FS: plugins.NewFakeFS()},
)),
expected: plugins.LoadingStrategyFetch,
},
@@ -153,63 +159,53 @@ func TestService_Calculate(t *testing.T) {
pluginSettings: newPluginSettings(pluginID, map[string]string{
CreatePluginVersionCfgKey: "invalidSemver",
}),
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
expected: plugins.LoadingStrategyScript,
},
}
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
s := &Service{
cfg: newCfg(tc.pluginSettings),
cdn: pluginscdn.ProvideService(&config.PluginManagementCfg{
PluginsCDNURLTemplate: "http://cdn.example.com", // required for cdn.PluginSupported check
PluginSettings: tc.pluginSettings,
}),
log: log.NewNopLogger(),
cfg := &config.PluginManagementCfg{
PluginSettings: tc.pluginSettings,
}
cdn := pluginscdn.ProvideService(&config.PluginManagementCfg{
PluginsCDNURLTemplate: "http://cdn.example.com", // required for cdn.PluginSupported check
PluginSettings: tc.pluginSettings,
})
got := s.LoadingStrategy(context.Background(), tc.plugin)
got := CalculateLoadingStrategy(tc.plugin, cfg, cdn)
assert.Equal(t, tc.expected, got, "unexpected loading strategy")
})
}
}
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
p := pluginstore.Plugin{
func newPluginForLoadingStrategy(pluginID string, cbs ...func(*plugins.Plugin)) *plugins.Plugin {
p := &plugins.Plugin{
JSONData: plugins.JSONData{
ID: pluginID,
},
}
for _, cb := range cbs {
p = cb(p)
cb(p)
}
return p
}
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
p.FS = fs
return p
}
}
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
func withAngularForLoadingStrategy(angular bool) func(*plugins.Plugin) {
return func(p *plugins.Plugin) {
p.Angular = plugins.AngularMeta{Detected: angular}
return p
}
}
func withClass(class plugins.Class) func(p pluginstore.Plugin) pluginstore.Plugin {
return func(p pluginstore.Plugin) pluginstore.Plugin {
func withFSForLoadingStrategy(fs plugins.FS) func(*plugins.Plugin) {
return func(p *plugins.Plugin) {
p.FS = fs
}
}
func withClassForLoadingStrategy(class plugins.Class) func(*plugins.Plugin) {
return func(p *plugins.Plugin) {
p.Class = class
return p
}
}
func newCfg(ps config.PluginSettings) *config.PluginManagementCfg {
return &config.PluginManagementCfg{
PluginSettings: ps,
}
}
+4 -3
View File
@@ -49,9 +49,10 @@ type Plugin struct {
Error *Error
// SystemJS fields
Module string
ModuleHash string
BaseURL string
Module string
ModuleHash string
LoadingStrategy LoadingStrategy
BaseURL string
Angular AngularMeta
+1 -3
View File
@@ -14,7 +14,6 @@ import (
"github.com/grafana/grafana/pkg/services/apiserver/appinstaller"
grafanaauthorizer "github.com/grafana/grafana/pkg/services/apiserver/auth/authorizer"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
@@ -35,7 +34,6 @@ func ProvideAppInstaller(
cfgProvider configprovider.ConfigProvider,
restConfigProvider apiserver.RestConfigProvider,
pluginStore pluginstore.Store,
pluginAssetsService *pluginassets.Service,
accessControlService accesscontrol.Service, accessClient authlib.AccessClient,
features featuremgmt.FeatureToggles,
) (*AppInstaller, error) {
@@ -46,7 +44,7 @@ func ProvideAppInstaller(
}
}
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
localProvider := meta.NewLocalProvider(pluginStore)
metaProviderManager := meta.NewProviderManager(localProvider)
authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient)
i, err := pluginsapp.ProvideAppInstaller(authorizer, metaProviderManager)
+4 -7
View File
@@ -187,7 +187,6 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginaccesscontrol"
pluginassets2 "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -715,7 +714,6 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService)
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
prefService := prefimpl.ProvideService(sqlStore, cfg)
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
@@ -753,7 +751,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
}
idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer)
verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationService, idimplService)
httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, managedpluginsNoop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl)
httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, authinfoimplService, storageService, notificationService, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, managedpluginsNoop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokenService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl)
if err != nil {
return nil, err
}
@@ -786,7 +784,7 @@ func Initialize(ctx context.Context, cfg *setting.Cfg, opts Options, apiOpts api
if err != nil {
return nil, err
}
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient, featureToggles)
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, acimplService, accessClient, featureToggles)
if err != nil {
return nil, err
}
@@ -1383,7 +1381,6 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
pluginassetsService := pluginassets2.ProvideService(pluginManagementCfg, pluginscdnService, pluginstoreService)
avatarCacheServer := avatar.ProvideAvatarCacheServer(cfg)
prefService := prefimpl.ProvideService(sqlStore, cfg)
dashboardPermissionsService, err := ossaccesscontrol.ProvideDashboardPermissions(cfg, featureToggles, routeRegisterImpl, sqlStore, accessControl, ossLicensingService, dashboardService, folderimplService, acimplService, teamService, userService, actionSetService, dashboardServiceImpl, eventualRestConfigProvider)
@@ -1421,7 +1418,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
}
idimplService := idimpl.ProvideService(cfg, localSigner, remoteCache, authnService, registerer, tracer)
verifier := userimpl.ProvideVerifier(cfg, userService, tempuserService, notificationServiceMock, idimplService)
httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, pluginassetsService, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, managedpluginsNoop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl)
httpServer, err := api.ProvideHTTPServer(apiOpts, cfg, routeRegisterImpl, inProcBus, renderingService, ossLicensingService, hooksService, cacheService, sqlStore, ossDataSourceRequestValidator, pluginstoreService, service14, pluginstoreService, middlewareHandler, pluginerrsStore, pluginInstaller, ossImpl, cacheServiceImpl, userAuthTokenService, cleanUpService, shortURLService, queryHistoryService, correlationsService, remoteCache, provisioningServiceImpl, accessControl, dataSourceProxyService, searchSearchService, grafanaLive, gateway, plugincontextProvider, contexthandlerContextHandler, logger, featureToggles, alertNG, libraryPanelService, libraryElementService, quotaService, socialService, tracingService, serviceService, grafanaService, pluginsService, ossService, service15, queryServiceImpl, filestoreService, serviceAccountsProxy, authinfoimplService, storageService, notificationServiceMock, dashboardService, dashboardProvisioningService, folderimplService, ossProvider, serviceImpl, service13, avatarCacheServer, prefService, folderPermissionsService, dashboardPermissionsService, dashverService, starService, csrfCSRF, managedpluginsNoop, playlistService, apikeyService, kvStore, secretsMigrator, secretsService, secretMigrationProviderImpl, secretsKVStore, apiApi, userService, tempuserService, loginattemptimplService, orgService, deletionService, teamService, acimplService, navtreeService, repositoryImpl, tagimplService, searchHTTPService, oauthtokentestService, statsService, authnService, pluginscdnService, gatherer, apiAPI, registerer, eventualRestConfigProvider, anonDeviceService, verifier, preinstallImpl)
if err != nil {
return nil, err
}
@@ -1454,7 +1451,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
if err != nil {
return nil, err
}
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient, featureToggles)
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, acimplService, accessClient, featureToggles)
if err != nil {
return nil, err
}
-9
View File
@@ -1625,15 +1625,6 @@ var (
FrontendOnly: true,
Expression: "false",
},
{
Name: "experimentRecentlyViewedDashboards",
Description: "A/A test for recently viewed dashboards feature",
Stage: FeatureStageExperimental,
Owner: grafanaFrontendSearchNavOrganise,
FrontendOnly: true,
HideFromDocs: true,
Expression: "false",
},
{
Name: "alertEnrichment",
Description: "Enable configuration of alert enrichments in Grafana Cloud.",
-1
View File
@@ -223,7 +223,6 @@ kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,f
kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false
restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false
recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
experimentRecentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false
alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false
1 Name Stage Owner requiresDevMode RequiresRestart FrontendOnly
223 kubernetesExternalGroupMapping experimental @grafana/identity-access-team false false false
224 restoreDashboards experimental @grafana/grafana-search-navigate-organise false false false
225 recentlyViewedDashboards experimental @grafana/grafana-search-navigate-organise false false true
experimentRecentlyViewedDashboards experimental @grafana/grafana-search-navigate-organise false false true
226 alertEnrichment experimental @grafana/alerting-squad false false false
227 alertEnrichmentMultiStep experimental @grafana/alerting-squad false false false
228 alertEnrichmentConditional experimental @grafana/alerting-squad false false false
-15
View File
@@ -1365,21 +1365,6 @@
"hideFromDocs": true
}
},
{
"metadata": {
"name": "experimentRecentlyViewedDashboards",
"resourceVersion": "1768214542023",
"creationTimestamp": "2026-01-12T10:42:22Z"
},
"spec": {
"description": "A/A test for recently viewed dashboards feature",
"stage": "experimental",
"codeowner": "@grafana/grafana-search-navigate-organise",
"frontend": true,
"hideFromDocs": true,
"expression": "false"
}
},
{
"metadata": {
"name": "exploreLogsAggregatedMetrics",
@@ -127,6 +127,7 @@ func TestLoader_Load(t *testing.T) {
Signature: plugins.SignatureStatusInternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -240,6 +241,7 @@ func TestLoader_Load(t *testing.T) {
SignatureOrg: "Grafana Labs",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -292,6 +294,7 @@ func TestLoader_Load(t *testing.T) {
Signature: "unsigned",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -357,6 +360,7 @@ func TestLoader_Load(t *testing.T) {
Signature: plugins.SignatureStatusUnsigned,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -479,6 +483,7 @@ func TestLoader_Load(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -571,6 +576,7 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
},
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -688,6 +694,7 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
SignatureOrg: "Will Browne",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
pluginErrors: map[string]*plugins.Error{
@@ -822,6 +829,7 @@ func TestLoader_Load_RBACReady(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
},
},
@@ -906,6 +914,7 @@ func TestLoader_Load_Signature_RootURL(t *testing.T) {
Module: "public/plugins/test-datasource/module.js",
BaseURL: "public/plugins/test-datasource",
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1010,6 +1019,7 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1118,6 +1128,7 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
BaseURL: "public/plugins/test-app",
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
},
}
@@ -1295,6 +1306,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
child := &plugins.Plugin{
@@ -1354,6 +1366,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
parent.Children = []*plugins.Plugin{child}
@@ -1520,6 +1533,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
child := &plugins.Plugin{
@@ -1587,6 +1601,7 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
Class: plugins.ClassExternal,
SkipHostEnvVars: true,
Translations: map[string]string{},
LoadingStrategy: plugins.LoadingStrategyScript,
}
parent.Children = []*plugins.Plugin{child}
@@ -1,90 +0,0 @@
package pluginassets
import (
"context"
"github.com/Masterminds/semver/v3"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/config"
"github.com/grafana/grafana/pkg/plugins/pluginscdn"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
)
const (
CreatePluginVersionCfgKey = "create_plugin_version"
CreatePluginVersionScriptSupportEnabled = "4.15.0"
)
var (
scriptLoadingMinSupportedVersion = semver.MustParse(CreatePluginVersionScriptSupportEnabled)
)
func ProvideService(cfg *config.PluginManagementCfg, cdn *pluginscdn.Service, store pluginstore.Store) *Service {
return &Service{
cfg: cfg,
cdn: cdn,
store: store,
log: log.New("pluginassets"),
}
}
type Service struct {
cfg *config.PluginManagementCfg
cdn *pluginscdn.Service
store pluginstore.Store
log log.Logger
}
// LoadingStrategy calculates the loading strategy for a plugin.
// If a plugin has plugin setting `create_plugin_version` >= 4.15.0, set loadingStrategy to "script".
// If a plugin is not loaded via the CDN and is not Angular, set loadingStrategy to "script".
// Otherwise, set loadingStrategy to "fetch".
func (s *Service) LoadingStrategy(_ context.Context, p pluginstore.Plugin) plugins.LoadingStrategy {
if pCfg, ok := s.cfg.PluginSettings[p.ID]; ok {
if s.compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// If the plugin has a parent
if p.Parent != nil {
// Check the parent's create_plugin_version setting
if pCfg, ok := s.cfg.PluginSettings[p.Parent.ID]; ok {
if s.compatibleCreatePluginVersion(pCfg) {
return plugins.LoadingStrategyScript
}
}
// Since the parent plugin is not explicitly configured as script loading compatible,
// If the plugin is either loaded from the CDN (via its parent) or contains Angular, we should use fetch
if s.cdnEnabled(p.Parent.ID, p.FS) || p.Angular.Detected {
return plugins.LoadingStrategyFetch
}
}
if !s.cdnEnabled(p.ID, p.FS) && !p.Angular.Detected {
return plugins.LoadingStrategyScript
}
return plugins.LoadingStrategyFetch
}
func (s *Service) compatibleCreatePluginVersion(ps map[string]string) bool {
if cpv, ok := ps[CreatePluginVersionCfgKey]; ok {
createPluginVer, err := semver.NewVersion(cpv)
if err != nil {
s.log.Warn("Failed to parse create plugin version setting as semver", "version", cpv, "error", err)
} else {
if !createPluginVer.LessThan(scriptLoadingMinSupportedVersion) {
return true
}
}
}
return false
}
func (s *Service) cdnEnabled(pluginID string, fs plugins.FS) bool {
return s.cdn.PluginSupported(pluginID) || fs.Type().CDN()
}
@@ -46,7 +46,6 @@ import (
"github.com/grafana/grafana/pkg/services/pluginsintegration/loader"
"github.com/grafana/grafana/pkg/services/pluginsintegration/managedplugins"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pipeline"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginassets"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginchecker"
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
@@ -131,7 +130,6 @@ var WireSet = wire.NewSet(
plugincontext.ProvideBaseService,
wire.Bind(new(plugincontext.BasePluginContextProvider), new(*plugincontext.BaseProvider)),
plugininstaller.ProvideService,
pluginassets.ProvideService,
pluginchecker.ProvidePreinstall,
wire.Bind(new(pluginchecker.Preinstall), new(*pluginchecker.PreinstallImpl)),
advisor.ProvideService,
@@ -30,9 +30,10 @@ type Plugin struct {
Error *plugins.Error
// SystemJS fields
Module string
BaseURL string
ModuleHash string
Module string
LoadingStrategy plugins.LoadingStrategy
BaseURL string
ModuleHash string
Angular plugins.AngularMeta
@@ -77,6 +78,7 @@ func ToGrafanaDTO(p *plugins.Plugin) Plugin {
SignatureOrg: p.SignatureOrg,
Error: p.Error,
Module: p.Module,
LoadingStrategy: p.LoadingStrategy,
BaseURL: p.BaseURL,
ExternalService: p.ExternalService,
Angular: p.Angular,
@@ -1,12 +1,11 @@
import { css } from '@emotion/css';
import { memo, useEffect, useMemo, useRef } from 'react';
import { memo, useEffect, useMemo } from 'react';
import { useLocation, useParams } from 'react-router-dom-v5-compat';
import AutoSizer from 'react-virtualized-auto-sizer';
import { GrafanaTheme2 } from '@grafana/data';
import { Trans } from '@grafana/i18n';
import { config, reportInteraction } from '@grafana/runtime';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
import { Page } from 'app/core/components/Page/Page';
@@ -45,7 +44,6 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
const location = useLocation();
const search = useMemo(() => new URLSearchParams(location.search), [location.search]);
const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: folderUID });
const isRecentlyViewedEnabled = !folderUID && evaluateBooleanFlag('recentlyViewedDashboards', false);
useEffect(() => {
stateManager.initStateFromUrl(folderUID);
@@ -75,23 +73,6 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
}
}, [isSearching, searchState.result, stateManager]);
// Emit exposure event for A/A test once when page loads
const hasEmittedExposureEvent = useRef(false);
useEffect(() => {
if (!isRecentlyViewedEnabled || hasEmittedExposureEvent.current) {
return;
}
hasEmittedExposureEvent.current = true;
const isExperimentTreatment = evaluateBooleanFlag('experimentRecentlyViewedDashboards', false);
reportInteraction('dashboards_browse_list_viewed', {
experiment_dashboard_list_recently_viewed: isExperimentTreatment ? 'treatment' : 'control',
has_recently_viewed_component: isExperimentTreatment,
});
}, [isRecentlyViewedEnabled]);
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
const [saveFolder] = useUpdateFolder();
const navModel = useMemo(() => {
@@ -198,8 +179,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
>
<Page.Contents className={styles.pageContents}>
<ProvisionedFolderPreviewBanner queryParams={queryParams} />
{/* only show recently viewed dashboards when in root and flag is enabled */}
{isRecentlyViewedEnabled && <RecentlyViewedDashboards />}
{/* only show recently viewed dashboards when in root */}
{!folderUID && <RecentlyViewedDashboards />}
<div>
<FilterInput
placeholder={getSearchPlaceholder(searchState.includePanels)}
@@ -5,6 +5,7 @@ import { useAsyncRetry } from 'react-use';
import { GrafanaTheme2, store } from '@grafana/data';
import { t, Trans } from '@grafana/i18n';
import { reportInteraction } from '@grafana/runtime';
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui';
import { contextSrv } from 'app/core/services/context_srv';
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
@@ -27,6 +28,9 @@ export function RecentlyViewedDashboards() {
retry,
error,
} = useAsyncRetry(async () => {
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
return [];
}
return getRecentlyViewedDashboards(MAX_RECENT);
}, []);
const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0);
@@ -44,7 +48,7 @@ export function RecentlyViewedDashboards() {
setIsOpen(!isOpen);
};
if (recentDashboards.length === 0) {
if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
return null;
}
@@ -119,7 +123,6 @@ const getStyles = (theme: GrafanaTheme2) => {
color: 'transparent',
cursor: 'pointer',
},
padding: 0,
}),
content: css({
paddingTop: theme.spacing(0),
@@ -11,9 +11,9 @@ describe('isDashboardDatasource', () => {
{ refId: 'C', datasource: { uid: 'mysql-uid', type: 'mysql' } },
];
const nonDashboardQueries = queries.filter((q) => !isDashboardDatasource(q));
const backendQueries = queries.filter((q) => !isDashboardDatasource(q));
expect(nonDashboardQueries.map((q) => q.refId)).toEqual(['A', 'C']);
expect(backendQueries.map((q) => q.refId)).toEqual(['A', 'C']);
});
it('returns true when query has dashboard datasource uid', () => {
@@ -67,9 +67,9 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
try {
// Filter out Dashboard datasource queries - they are frontend-only and can't be processed by backend
const nonDashboardQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
const backendQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
if (nonDashboardQueries.length === 0) {
if (backendQueries.length === 0) {
setSchemas({ kind: 'SQLSchemaResponse', apiVersion: 'query.grafana.app/v0alpha1', sqlSchemas: {} });
setLoading(false);
return;
@@ -81,7 +81,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
const response = await getBackendSrv().post<SQLSchemasResponse>(
`/apis/query.grafana.app/v0alpha1/namespaces/${namespace}/sqlschemas/name`,
{
queries: nonDashboardQueries,
queries: backendQueries,
from: currentTimeRange.from.toISOString(),
to: currentTimeRange.to.toISOString(),
}
@@ -21,7 +21,7 @@ export function initSystemJSHooks() {
// This instructs SystemJS to load plugin assets using fetch and eval if it returns a truthy value, otherwise
// it will load the plugin using a script tag. The logic that sets loadingStrategy comes from the backend.
// See: pkg/services/pluginsintegration/pluginassets/pluginassets.go
// Loading strategy is calculated during bootstrap in pkg/plugins/pluginassets/loadingstrategy.go
systemJSPrototype.shouldFetch = function (url) {
const pluginInfo = getPluginInfoFromCache(url);
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
@@ -36,4 +36,9 @@ describe('ConfigEditor', () => {
expect(screen.getByTestId('url-auth-section')).toBeInTheDocument();
expect(screen.getByTestId('db-connection-section')).toBeInTheDocument();
});
it('shows the informational alert', () => {
render(<ConfigEditor {...defaultProps} />);
expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument();
});
});
@@ -2,12 +2,13 @@ import { css } from '@emotion/css';
import React from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { Box, Stack, Text, useStyles2 } from '@grafana/ui';
import { Alert, Box, Stack, TextLink, Text, useStyles2 } from '@grafana/ui';
import { DatabaseConnectionSection } from './DatabaseConnectionSection';
import { LeftSideBar } from './LeftSideBar';
import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection';
import { CONTAINER_MIN_WIDTH } from './constants';
import { trackInfluxDBConfigV2FeedbackButtonClicked } from './tracking';
import { Props } from './types';
export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Props) => {
@@ -21,6 +22,22 @@ export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Prop
</div>
<Box width="60%" flex="1 1 auto" minWidth={CONTAINER_MIN_WIDTH}>
<Stack direction="column">
<Alert
severity="info"
title="You are viewing a new design for the InfluxDB configuration settings."
className={styles.alertHeight}
>
<>
<TextLink
href="https://docs.google.com/forms/d/e/1FAIpQLSdi-zyX3c51vh937UKhNYYxhljUnFi6dQSlZv50mES9NrK-ig/viewform"
external
onClick={trackInfluxDBConfigV2FeedbackButtonClicked}
>
Share your thoughts
</TextLink>{' '}
to help us make it even better.
</>
</Alert>
<Text variant="bodySmall" color="secondary">
Fields marked with * are required
</Text>
@@ -1,5 +1,3 @@
import { truncate } from 'lodash';
import { reportInteraction } from '@grafana/runtime';
import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
import { LocationInfo } from 'app/features/search/service/types';
@@ -27,7 +25,6 @@ export function DashListItem({
onStarChange,
}: Props) {
const css = useStyles2(getStyles);
const shortTitle = truncate(dashboard.name, { length: 40, omission: '…' });
const onCardLinkClick = () => {
reportInteraction('grafana_recently_viewed_dashboards_click_card', {
@@ -57,35 +54,27 @@ export function DashListItem({
</div>
) : (
<Card className={css.dashlistCard} noMargin>
<Stack direction="column" justifyContent="space-between" height="100%">
<Stack justifyContent="space-between" alignItems="start">
<Link
className={css.dashlistCardLink}
href={url}
aria-label={dashboard.name}
title={dashboard.name}
onClick={onCardLinkClick}
>
{shortTitle}
</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</Stack>
{showFolderNames && locationInfo && (
<Stack alignItems="start" direction="row" gap={0.5} height="25%">
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
</Stack>
)}
<Stack justifyContent="space-between" alignItems="center">
<Link href={url} onClick={onCardLinkClick}>
{dashboard.name}
</Link>
<StarToolbarButton
title={dashboard.name}
group="dashboard.grafana.app"
kind="Dashboard"
id={dashboard.uid}
onStarChange={onStarChange}
/>
</Stack>
{showFolderNames && locationInfo && (
<Stack alignItems="center" direction="row" gap={0}>
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
<Text color="secondary" variant="bodySmall" element="p">
{locationInfo?.name}
</Text>
</Stack>
)}
</Card>
)}
</>
@@ -32,7 +32,6 @@ export const getStyles = (theme: GrafanaTheme2) => {
textDecoration: 'underline',
},
height: '100%',
paddingTop: theme.spacing(1.5),
'&:hover': {
backgroundImage: gradient,
@@ -42,8 +41,5 @@ export const getStyles = (theme: GrafanaTheme2) => {
dashlistCardIcon: css({
marginRight: theme.spacing(0.5),
}),
dashlistCardLink: css({
paddingTop: theme.spacing(0.5),
}),
};
};