Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8a01197f27 | |||
| 1d3f09d519 | |||
| ec1ace398e | |||
| fe5aa3e281 | |||
| a01777eafa |
@@ -30,7 +30,6 @@ 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
|
||||
|
||||
@@ -9,8 +9,6 @@ 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,6 +4,7 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/plugins"
|
||||
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
||||
)
|
||||
|
||||
@@ -11,16 +12,25 @@ 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
|
||||
store pluginstore.Store
|
||||
pluginAssets PluginAssetsCalculator
|
||||
}
|
||||
|
||||
// NewLocalProvider creates a new LocalProvider for locally installed plugins.
|
||||
func NewLocalProvider(pluginStore pluginstore.Store) *LocalProvider {
|
||||
// pluginAssets is required for calculating loading strategy.
|
||||
func NewLocalProvider(pluginStore pluginstore.Store, pluginAssets PluginAssetsCalculator) *LocalProvider {
|
||||
return &LocalProvider{
|
||||
store: pluginStore,
|
||||
store: pluginStore,
|
||||
pluginAssets: pluginAssets,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +41,10 @@ func (p *LocalProvider) GetMeta(ctx context.Context, pluginID, version string) (
|
||||
return nil, ErrMetaNotFound
|
||||
}
|
||||
|
||||
spec := pluginStorePluginToMeta(plugin, plugin.LoadingStrategy, plugin.ModuleHash)
|
||||
loadingStrategy := p.pluginAssets.LoadingStrategy(ctx, plugin)
|
||||
moduleHash := plugin.ModuleHash
|
||||
|
||||
spec := pluginStorePluginToMeta(plugin, loadingStrategy, 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 manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
|
||||
A Grafana server administrator (sometimes referred to as a **Grafana Admin**) manages server-wide settings and access to resources such as organizations, users, and licenses. Grafana includes a default server administrator that you can use to manage all of Grafana, or you can divide that responsibility among other server administrators that you create.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
The server administrator role does not mean that the user is also a Grafana [organization administrator](#organization-roles).
|
||||
{{< admonition type="caution" >}}
|
||||
The server administrator role is distinct from the [organization administrator](#organization-roles) role.
|
||||
{{< /admonition >}}
|
||||
|
||||
A server administrator can perform the following tasks:
|
||||
@@ -50,7 +50,7 @@ A server administrator can perform the following tasks:
|
||||
- Upgrade the server to Grafana Enterprise.
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
The server administrator role does not exist in Grafana Cloud.
|
||||
The server administrator (Grafana Admin) role does not exist in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
To assign or remove server administrator privileges, see [Server user management](../user-management/server-user-management/assign-remove-server-admin-privileges/).
|
||||
|
||||
+102
-59
@@ -53,6 +53,11 @@ refs:
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/custom-role-actions-scopes/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/custom-role-actions-scopes/
|
||||
rbac-terraform-provisioning:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-terraform-provisioning/
|
||||
- pattern: /docs/grafana-cloud/
|
||||
destination: /docs/grafana-cloud/account-management/authentication-and-permissions/access-control/rbac-terraform-provisioning/
|
||||
rbac-grafana-provisioning:
|
||||
- pattern: /docs/grafana/
|
||||
destination: /docs/grafana/<GRAFANA_VERSION>/administration/roles-and-permissions/access-control/rbac-grafana-provisioning/
|
||||
@@ -145,7 +150,13 @@ Refer to the [RBAC HTTP API](ref:api-rbac-get-a-role) for more details.
|
||||
|
||||
## Create custom roles
|
||||
|
||||
This section shows you how to create a custom RBAC role using Grafana provisioning and the HTTP API.
|
||||
This section shows you how to create a custom RBAC role using Grafana provisioning or the HTTP API.
|
||||
|
||||
Creating and editing custom roles is not currently possible in the Grafana UI. To manage custom roles, use one of the following methods:
|
||||
|
||||
- [Provisioning](ref:rbac-grafana-provisioning) (for self-managed instances)
|
||||
- [HTTP API](ref:api-rbac-create-a-new-custom-role)
|
||||
- [Terraform](ref:rbac-terraform-provisioning)
|
||||
|
||||
Create a custom role when basic roles and fixed roles do not meet your permissions requirements.
|
||||
|
||||
@@ -153,14 +164,101 @@ Create a custom role when basic roles and fixed roles do not meet your permissio
|
||||
|
||||
- [Plan your RBAC rollout strategy](ref:plan-rbac-rollout-strategy).
|
||||
- Determine which permissions you want to add to the custom role. To see a list of actions and scope, refer to [RBAC permissions, actions, and scopes](ref:custom-role-actions-scopes).
|
||||
- [Enable role provisioning](ref:rbac-grafana-provisioning).
|
||||
- Ensure that you have permissions to create a custom role.
|
||||
- By default, the Grafana Admin role has permission to create custom roles.
|
||||
- A Grafana Admin can delegate the custom role privilege to another user by creating a custom role with the relevant permissions and adding the `permissions:type:delegate` scope.
|
||||
|
||||
### Create custom roles using provisioning
|
||||
### Create custom roles using the HTTP API
|
||||
|
||||
[File-based provisioning](ref:rbac-grafana-provisioning) is one method you can use to create custom roles.
|
||||
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
When you create a custom role you can only give it the same permissions you already have. For example, if you only have `users:create` permissions, then you can't create a role that includes other permissions.
|
||||
{{< /admonition >}}
|
||||
|
||||
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
|
||||
|
||||
**Example request**
|
||||
|
||||
```
|
||||
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
|
||||
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"version": 1,
|
||||
"uid": "jZrmlLCkGksdka",
|
||||
"name": "custom:users:admin",
|
||||
"displayName": "custom users admin",
|
||||
"description": "My custom role which gives users permissions to create users",
|
||||
"global": true,
|
||||
"permissions": [
|
||||
{
|
||||
"action": "users:create"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Example response**
|
||||
|
||||
```
|
||||
{
|
||||
"version": 1,
|
||||
"uid": "jZrmlLCkGksdka",
|
||||
"name": "custom:users:admin",
|
||||
"displayName": "custom users admin",
|
||||
"description": "My custom role which gives users permissions to create users",
|
||||
"global": true,
|
||||
"permissions": [
|
||||
{
|
||||
"action": "users:create"
|
||||
"updated": "2021-05-17T22:07:31.569936+02:00",
|
||||
"created": "2021-05-17T22:07:31.569935+02:00"
|
||||
}
|
||||
],
|
||||
"updated": "2021-05-17T22:07:31.564403+02:00",
|
||||
"created": "2021-05-17T22:07:31.564403+02:00"
|
||||
}
|
||||
```
|
||||
|
||||
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
|
||||
|
||||
### Create custom roles using Terraform
|
||||
|
||||
You can use the [Grafana Terraform provider](https://registry.terraform.io/providers/grafana/grafana/latest/docs) to manage custom roles and their assignments. This is the recommended method for Grafana Cloud users who want to manage RBAC as code. For more information, refer to [Provisioning RBAC with Terraform](ref:rbac-terraform-provisioning).
|
||||
|
||||
The following example creates a custom role and assigns it to a team:
|
||||
|
||||
```terraform
|
||||
resource "grafana_role" "custom_folder_manager" {
|
||||
name = "custom:folders:manager"
|
||||
description = "Custom role for reading and creating folders"
|
||||
uid = "custom-folders-manager"
|
||||
version = 1
|
||||
global = true
|
||||
|
||||
permissions {
|
||||
action = "folders:read"
|
||||
scope = "folders:*"
|
||||
}
|
||||
|
||||
permissions {
|
||||
action = "folders:create"
|
||||
scope = "folders:uid:general" # Allows creating folders at the root level
|
||||
}
|
||||
}
|
||||
|
||||
resource "grafana_role_assignment" "custom_folder_manager_assignment" {
|
||||
role_uid = grafana_role.custom_folder_manager.uid
|
||||
teams = ["<TEAM_UID>"]
|
||||
}
|
||||
```
|
||||
|
||||
For more information, refer to the [`grafana_role`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role) and [`grafana_role_assignment`](https://registry.terraform.io/providers/grafana/grafana/latest/docs/resources/role_assignment) documentation in the Terraform Registry.
|
||||
|
||||
### Create custom roles using file-based provisioning
|
||||
|
||||
You can use [file-based provisioning](ref:rbac-grafana-provisioning) to create custom roles for self-managed instances.
|
||||
|
||||
1. Open the YAML configuration file and locate the `roles` section.
|
||||
|
||||
@@ -251,61 +349,6 @@ roles:
|
||||
state: 'absent'
|
||||
```
|
||||
|
||||
### Create custom roles using the HTTP API
|
||||
|
||||
The following examples show you how to create a custom role using the Grafana HTTP API. For more information about the HTTP API, refer to [Create a new custom role](ref:api-rbac-create-a-new-custom-role).
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
You cannot create a custom role with permissions that you do not have. For example, if you only have `users:create` permissions, then you cannot create a role that includes other permissions.
|
||||
{{< /admonition >}}
|
||||
|
||||
The following example creates a `custom:users:admin` role and assigns the `users:create` action to it.
|
||||
|
||||
**Example request**
|
||||
|
||||
```
|
||||
curl --location --request POST '<grafana_url>/api/access-control/roles/' \
|
||||
--header 'Authorization: Basic YWRtaW46cGFzc3dvcmQ=' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"version": 1,
|
||||
"uid": "jZrmlLCkGksdka",
|
||||
"name": "custom:users:admin",
|
||||
"displayName": "custom users admin",
|
||||
"description": "My custom role which gives users permissions to create users",
|
||||
"global": true,
|
||||
"permissions": [
|
||||
{
|
||||
"action": "users:create"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
**Example response**
|
||||
|
||||
```
|
||||
{
|
||||
"version": 1,
|
||||
"uid": "jZrmlLCkGksdka",
|
||||
"name": "custom:users:admin",
|
||||
"displayName": "custom users admin",
|
||||
"description": "My custom role which gives users permissions to create users",
|
||||
"global": true,
|
||||
"permissions": [
|
||||
{
|
||||
"action": "users:create"
|
||||
"updated": "2021-05-17T22:07:31.569936+02:00",
|
||||
"created": "2021-05-17T22:07:31.569935+02:00"
|
||||
}
|
||||
],
|
||||
"updated": "2021-05-17T22:07:31.564403+02:00",
|
||||
"created": "2021-05-17T22:07:31.564403+02:00"
|
||||
}
|
||||
```
|
||||
|
||||
Refer to the [RBAC HTTP API](ref:api-rbac-create-a-new-custom-role) for more details.
|
||||
|
||||
## Update basic role permissions
|
||||
|
||||
If the default basic role definitions do not meet your requirements, you can change their permissions.
|
||||
|
||||
+3
-2
@@ -6,7 +6,6 @@ description: Learn about RBAC Grafana provisioning and view an example YAML prov
|
||||
file that configures Grafana role assignments.
|
||||
labels:
|
||||
products:
|
||||
- cloud
|
||||
- enterprise
|
||||
menuTitle: Provisioning RBAC with Grafana
|
||||
title: Provisioning RBAC with Grafana
|
||||
@@ -52,11 +51,13 @@ refs:
|
||||
# Provisioning RBAC with Grafana
|
||||
|
||||
{{< admonition type="note" >}}
|
||||
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) and [Grafana Cloud](/docs/grafana-cloud).
|
||||
Available in [Grafana Enterprise](/docs/grafana/<GRAFANA_VERSION>/introduction/grafana-enterprise/) for self-managed instances. This feature is not available in Grafana Cloud.
|
||||
{{< /admonition >}}
|
||||
|
||||
You can create, change or remove [Custom roles](ref:manage-rbac-roles-create-custom-roles-using-provisioning) and create or remove [basic role assignments](ref:assign-rbac-roles-assign-a-fixed-role-to-a-basic-role-using-provisioning), by adding one or more YAML configuration files in the `provisioning/access-control/` directory.
|
||||
|
||||
Because this method requires access to the file system where Grafana is running, it's only available for self-managed Grafana instances. To provision RBAC in Grafana Cloud, use [Terraform](ref:rbac-terraform-provisioning) or the [HTTP API](ref:api-rbac-create-and-manage-custom-roles).
|
||||
|
||||
Grafana performs provisioning during startup. After you make a change to the configuration file, you can reload it during runtime. You do not need to restart the Grafana server for your changes to take effect.
|
||||
|
||||
**Before you begin:**
|
||||
|
||||
@@ -984,6 +984,11 @@ export interface FeatureToggles {
|
||||
*/
|
||||
recentlyViewedDashboards?: boolean;
|
||||
/**
|
||||
* A/A test for recently viewed dashboards feature
|
||||
* @default false
|
||||
*/
|
||||
experimentRecentlyViewedDashboards?: boolean;
|
||||
/**
|
||||
* Enable configuration of alert enrichments in Grafana Cloud.
|
||||
* @default false
|
||||
*/
|
||||
|
||||
@@ -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: panel.LoadingStrategy,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), panel),
|
||||
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: plugin.LoadingStrategy,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||
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: plugin.LoadingStrategy,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(ctx, plugin),
|
||||
Extensions: plugin.Extensions,
|
||||
Dependencies: plugin.Dependencies,
|
||||
ModuleHash: plugin.ModuleHash,
|
||||
|
||||
@@ -31,6 +31,7 @@ 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"
|
||||
@@ -43,7 +44,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) (*web.Mux, *HTTPServer) {
|
||||
func setupTestEnvironment(t *testing.T, cfg *setting.Cfg, features featuremgmt.FeatureToggles, pstore pluginstore.Store, psettings pluginsettings.Service, passets *pluginassets.Service) (*web.Mux, *HTTPServer) {
|
||||
t.Helper()
|
||||
db.InitTestDB(t)
|
||||
// nolint:staticcheck
|
||||
@@ -74,6 +75,11 @@ 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,
|
||||
@@ -90,6 +96,7 @@ 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(),
|
||||
@@ -122,7 +129,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_hideVersionAnonymous(t *testi
|
||||
cfg.BuildVersion = "7.8.9"
|
||||
cfg.BuildCommit = "01234567"
|
||||
|
||||
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
|
||||
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||
|
||||
@@ -214,7 +221,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_pluginsCDNBaseURL(t *testing.
|
||||
if test.mutateCfg != nil {
|
||||
test.mutateCfg(cfg)
|
||||
}
|
||||
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil)
|
||||
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), nil, nil, nil)
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -239,6 +246,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
desc string
|
||||
pluginStore func() pluginstore.Store
|
||||
pluginSettings func() pluginsettings.Service
|
||||
pluginAssets func() *pluginassets.Service
|
||||
expected settings
|
||||
}{
|
||||
{
|
||||
@@ -255,8 +263,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Type: plugins.TypeApp,
|
||||
Preload: true,
|
||||
},
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -266,6 +273,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", false),
|
||||
}
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@@ -293,8 +301,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Type: plugins.TypeApp,
|
||||
Preload: true,
|
||||
},
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -304,6 +311,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@@ -330,9 +338,8 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Type: plugins.TypeApp,
|
||||
Preload: true,
|
||||
},
|
||||
Angular: plugins.AngularMeta{Detected: true},
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
LoadingStrategy: plugins.LoadingStrategyFetch,
|
||||
Angular: plugins.AngularMeta{Detected: true},
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -342,6 +349,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@@ -368,7 +376,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Type: plugins.TypeApp,
|
||||
Preload: true,
|
||||
},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -378,6 +385,13 @@ 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": {
|
||||
@@ -407,7 +421,6 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
FS: &pluginfakes.FakePluginFS{TypeFunc: func() plugins.FSType {
|
||||
return plugins.FSTypeCDN
|
||||
}},
|
||||
LoadingStrategy: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -417,6 +430,7 @@ func TestIntegrationHTTPServer_GetFrontendSettings_apps(t *testing.T) {
|
||||
Plugins: newAppSettings("test-app", true),
|
||||
}
|
||||
},
|
||||
pluginAssets: newPluginAssets(),
|
||||
expected: settings{
|
||||
Apps: map[string]*plugins.AppDTO{
|
||||
"test-app": {
|
||||
@@ -434,7 +448,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())
|
||||
m, _ := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), test.pluginSettings(), test.pluginAssets())
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||
|
||||
recorder := httptest.NewRecorder()
|
||||
@@ -535,8 +549,7 @@ 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{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -586,8 +599,7 @@ 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{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -627,8 +639,7 @@ 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{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
FS: &pluginfakes.FakePluginFS{},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -656,7 +667,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)
|
||||
m, hs := setupTestEnvironment(t, cfg, featuremgmt.WithFeatures(), test.pluginStore(), nil, nil)
|
||||
|
||||
// Create a request with the appropriate context
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/frontend/settings", nil)
|
||||
@@ -693,3 +704,13 @@ 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{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +82,7 @@ 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"
|
||||
@@ -150,6 +151,7 @@ type HTTPServer struct {
|
||||
pluginDashboardService plugindashboards.Service
|
||||
pluginStaticRouteResolver plugins.StaticRouteResolver
|
||||
pluginErrorResolver plugins.ErrorResolver
|
||||
pluginAssets *pluginassets.Service
|
||||
pluginPreinstall pluginchecker.Preinstall
|
||||
SearchService search.Service
|
||||
ShortURLService shorturls.Service
|
||||
@@ -253,7 +255,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,
|
||||
serviceaccountsService serviceaccounts.Service, pluginAssets *pluginassets.Service,
|
||||
authInfoService login.AuthInfoService, storageService store.StorageService,
|
||||
notificationService notifications.Service, dashboardService dashboards.DashboardService,
|
||||
dashboardProvisioningService dashboards.DashboardProvisioningService, folderService folder.Service,
|
||||
@@ -292,6 +294,7 @@ 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
@@ -209,7 +209,7 @@ func (hs *HTTPServer) GetPluginSettingByID(c *contextmodel.ReqContext) response.
|
||||
SignatureOrg: plugin.SignatureOrg,
|
||||
SecureJsonFields: map[string]bool{},
|
||||
AngularDetected: plugin.Angular.Detected,
|
||||
LoadingStrategy: plugin.LoadingStrategy,
|
||||
LoadingStrategy: hs.pluginAssets.LoadingStrategy(c.Req.Context(), plugin),
|
||||
Extensions: plugin.Extensions,
|
||||
Translations: plugin.Translations,
|
||||
}
|
||||
|
||||
@@ -41,6 +41,7 @@ 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"
|
||||
@@ -674,10 +675,9 @@ 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,
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
JSONData: jd,
|
||||
Class: class,
|
||||
FS: files,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -844,6 +844,9 @@ 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,
|
||||
|
||||
@@ -4,7 +4,6 @@ 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,8 +2,6 @@ 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,7 +129,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Class: plugins.ClassCore,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -243,7 +242,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
SignatureOrg: "Grafana Labs",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -296,7 +294,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Signature: "unsigned",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -356,7 +353,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Signature: plugins.SignatureStatusUnsigned,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -455,7 +451,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
BaseURL: "public/plugins/test-app",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -36,7 +36,6 @@ func DefaultDecorateFuncs(cfg *config.PluginManagementCfg, cdn *pluginscdn.Servi
|
||||
AppChildDecorateFunc(),
|
||||
SkipHostEnvVarsDecorateFunc(cfg),
|
||||
ModuleHashDecorateFunc(cfg, cdn),
|
||||
LoadingStrategyDecorateFunc(cfg, cdn),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,11 +166,3 @@ 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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -49,10 +49,9 @@ type Plugin struct {
|
||||
Error *Error
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
ModuleHash string
|
||||
LoadingStrategy LoadingStrategy
|
||||
BaseURL string
|
||||
Module string
|
||||
ModuleHash string
|
||||
BaseURL string
|
||||
|
||||
Angular AngularMeta
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ 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"
|
||||
)
|
||||
|
||||
@@ -34,6 +35,7 @@ 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) {
|
||||
@@ -44,7 +46,7 @@ func ProvideAppInstaller(
|
||||
}
|
||||
}
|
||||
|
||||
localProvider := meta.NewLocalProvider(pluginStore)
|
||||
localProvider := meta.NewLocalProvider(pluginStore, pluginAssetsService)
|
||||
metaProviderManager := meta.NewProviderManager(localProvider)
|
||||
authorizer := grafanaauthorizer.NewResourceAuthorizer(accessClient)
|
||||
i, err := pluginsapp.ProvideAppInstaller(authorizer, metaProviderManager)
|
||||
|
||||
Generated
+7
-4
@@ -187,6 +187,7 @@ 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"
|
||||
@@ -714,6 +715,7 @@ 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)
|
||||
@@ -751,7 +753,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, 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, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -784,7 +786,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, acimplService, accessClient, featureToggles)
|
||||
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient, featureToggles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1381,6 +1383,7 @@ 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)
|
||||
@@ -1418,7 +1421,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, 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, 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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -1451,7 +1454,7 @@ func InitializeForTest(ctx context.Context, t sqlutil.ITestDB, testingT interfac
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, acimplService, accessClient, featureToggles)
|
||||
appInstaller, err := plugins.ProvideAppInstaller(configProvider, eventualRestConfigProvider, pluginstoreService, pluginassetsService, acimplService, accessClient, featureToggles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -1625,6 +1625,15 @@ var (
|
||||
FrontendOnly: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "experimentRecentlyViewedDashboards",
|
||||
Description: "A/A test for recently viewed dashboards feature",
|
||||
Stage: FeatureStageExperimental,
|
||||
Owner: grafanaFrontendSearchNavOrganise,
|
||||
FrontendOnly: true,
|
||||
HideFromDocs: true,
|
||||
Expression: "false",
|
||||
},
|
||||
{
|
||||
Name: "alertEnrichment",
|
||||
Description: "Enable configuration of alert enrichments in Grafana Cloud.",
|
||||
|
||||
Generated
+1
@@ -223,6 +223,7 @@ kubernetesAuthnMutation,experimental,@grafana/identity-access-team,false,false,f
|
||||
kubernetesExternalGroupMapping,experimental,@grafana/identity-access-team,false,false,false
|
||||
restoreDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,false
|
||||
recentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
|
||||
experimentRecentlyViewedDashboards,experimental,@grafana/grafana-search-navigate-organise,false,false,true
|
||||
alertEnrichment,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertEnrichmentMultiStep,experimental,@grafana/alerting-squad,false,false,false
|
||||
alertEnrichmentConditional,experimental,@grafana/alerting-squad,false,false,false
|
||||
|
||||
|
+15
@@ -1365,6 +1365,21 @@
|
||||
"hideFromDocs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "experimentRecentlyViewedDashboards",
|
||||
"resourceVersion": "1768214542023",
|
||||
"creationTimestamp": "2026-01-12T10:42:22Z"
|
||||
},
|
||||
"spec": {
|
||||
"description": "A/A test for recently viewed dashboards feature",
|
||||
"stage": "experimental",
|
||||
"codeowner": "@grafana/grafana-search-navigate-organise",
|
||||
"frontend": true,
|
||||
"hideFromDocs": true,
|
||||
"expression": "false"
|
||||
}
|
||||
},
|
||||
{
|
||||
"metadata": {
|
||||
"name": "exploreLogsAggregatedMetrics",
|
||||
|
||||
@@ -127,7 +127,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Signature: plugins.SignatureStatusInternal,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -241,7 +240,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
SignatureOrg: "Grafana Labs",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -294,7 +292,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Signature: "unsigned",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -360,7 +357,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
Signature: plugins.SignatureStatusUnsigned,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -483,7 +479,6 @@ func TestLoader_Load(t *testing.T) {
|
||||
BaseURL: "public/plugins/test-app",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -576,7 +571,6 @@ func TestLoader_Load_ExternalRegistration(t *testing.T) {
|
||||
},
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -694,7 +688,6 @@ func TestLoader_Load_MultiplePlugins(t *testing.T) {
|
||||
SignatureOrg: "Will Browne",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
pluginErrors: map[string]*plugins.Error{
|
||||
@@ -829,7 +822,6 @@ func TestLoader_Load_RBACReady(t *testing.T) {
|
||||
BaseURL: "public/plugins/test-app",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -914,7 +906,6 @@ 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,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1019,7 +1010,6 @@ func TestLoader_Load_DuplicatePlugins(t *testing.T) {
|
||||
BaseURL: "public/plugins/test-app",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1128,7 +1118,6 @@ func TestLoader_Load_SkipUninitializedPlugins(t *testing.T) {
|
||||
BaseURL: "public/plugins/test-app",
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1306,7 +1295,6 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Class: plugins.ClassExternal,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
@@ -1366,7 +1354,6 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Class: plugins.ClassExternal,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
}
|
||||
|
||||
parent.Children = []*plugins.Plugin{child}
|
||||
@@ -1533,7 +1520,6 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Class: plugins.ClassExternal,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
}
|
||||
|
||||
child := &plugins.Plugin{
|
||||
@@ -1601,7 +1587,6 @@ func TestLoader_Load_NestedPlugins(t *testing.T) {
|
||||
Class: plugins.ClassExternal,
|
||||
SkipHostEnvVars: true,
|
||||
Translations: map[string]string{},
|
||||
LoadingStrategy: plugins.LoadingStrategyScript,
|
||||
}
|
||||
|
||||
parent.Children = []*plugins.Plugin{child}
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
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()
|
||||
}
|
||||
+67
-63
@@ -1,25 +1,20 @@
|
||||
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"
|
||||
)
|
||||
|
||||
// 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) {
|
||||
func TestService_Calculate(t *testing.T) {
|
||||
const pluginID = "grafana-test-datasource"
|
||||
|
||||
const (
|
||||
@@ -31,7 +26,7 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
tcs := []struct {
|
||||
name string
|
||||
pluginSettings config.PluginSettings
|
||||
plugin *plugins.Plugin
|
||||
plugin pluginstore.Plugin
|
||||
expected plugins.LoadingStrategy
|
||||
}{
|
||||
{
|
||||
@@ -39,7 +34,7 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: compatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false)),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
{
|
||||
@@ -47,11 +42,9 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings("parent-datasource", map[string]string{
|
||||
CreatePluginVersionCfgKey: compatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), func(p *plugins.Plugin) {
|
||||
p.Parent = &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "parent-datasource"},
|
||||
FS: plugins.NewFakeFS(),
|
||||
}
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
@@ -60,21 +53,24 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: futureVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(false), withFS(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{}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
// NOTE: cdn key is not set
|
||||
}),
|
||||
plugin: newPlugin(pluginID, withAngular(false), withFS(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: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withClassForLoadingStrategy(plugins.ClassExternal), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal), withFS(plugins.NewFakeFS())),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
{
|
||||
@@ -84,11 +80,9 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
"cdn": "true",
|
||||
},
|
||||
},
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), func(p *plugins.Plugin) {
|
||||
p.Parent = &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "parent-datasource"},
|
||||
FS: plugins.NewFakeFS(),
|
||||
}
|
||||
plugin: newPlugin(pluginID, withAngular(false), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
@@ -99,22 +93,18 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
"cdn": "true",
|
||||
},
|
||||
},
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), func(p *plugins.Plugin) {
|
||||
p.Parent = &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "parent-datasource"},
|
||||
FS: plugins.NewFakeFS(),
|
||||
}
|
||||
plugin: newPlugin(pluginID, withAngular(true), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
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: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), withFSForLoadingStrategy(plugins.NewFakeFS()), func(p *plugins.Plugin) {
|
||||
p.Parent = &plugins.Plugin{
|
||||
JSONData: plugins.JSONData{ID: "parent-datasource"},
|
||||
FS: plugins.NewFakeFS(),
|
||||
}
|
||||
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS()), func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.Parent = &pluginstore.ParentPlugin{ID: "parent-datasource"}
|
||||
return p
|
||||
}),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
@@ -124,7 +114,7 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
"cdn": "true",
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withClassForLoadingStrategy(plugins.ClassExternal), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(false), withClass(plugins.ClassExternal)),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
{
|
||||
@@ -132,7 +122,7 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(true), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(true), withFS(plugins.NewFakeFS())),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
{
|
||||
@@ -141,7 +131,7 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
"cdn": "true",
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(false)),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
{
|
||||
@@ -149,8 +139,12 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: incompatVersion,
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(
|
||||
&cdnFS{FS: plugins.NewFakeFS()},
|
||||
plugin: newPlugin(pluginID, withAngular(false), withFS(
|
||||
&pluginfakes.FakePluginFS{
|
||||
TypeFunc: func() plugins.FSType {
|
||||
return plugins.FSTypeCDN
|
||||
},
|
||||
},
|
||||
)),
|
||||
expected: plugins.LoadingStrategyFetch,
|
||||
},
|
||||
@@ -159,53 +153,63 @@ func TestCalculateLoadingStrategy(t *testing.T) {
|
||||
pluginSettings: newPluginSettings(pluginID, map[string]string{
|
||||
CreatePluginVersionCfgKey: "invalidSemver",
|
||||
}),
|
||||
plugin: newPluginForLoadingStrategy(pluginID, withAngularForLoadingStrategy(false), withFSForLoadingStrategy(plugins.NewFakeFS())),
|
||||
plugin: newPlugin(pluginID, withAngular(false), withFS(plugins.NewFakeFS())),
|
||||
expected: plugins.LoadingStrategyScript,
|
||||
},
|
||||
}
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cfg := &config.PluginManagementCfg{
|
||||
PluginSettings: tc.pluginSettings,
|
||||
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(),
|
||||
}
|
||||
cdn := pluginscdn.ProvideService(&config.PluginManagementCfg{
|
||||
PluginsCDNURLTemplate: "http://cdn.example.com", // required for cdn.PluginSupported check
|
||||
PluginSettings: tc.pluginSettings,
|
||||
})
|
||||
|
||||
got := CalculateLoadingStrategy(tc.plugin, cfg, cdn)
|
||||
got := s.LoadingStrategy(context.Background(), tc.plugin)
|
||||
assert.Equal(t, tc.expected, got, "unexpected loading strategy")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newPluginForLoadingStrategy(pluginID string, cbs ...func(*plugins.Plugin)) *plugins.Plugin {
|
||||
p := &plugins.Plugin{
|
||||
func newPlugin(pluginID string, cbs ...func(p pluginstore.Plugin) pluginstore.Plugin) pluginstore.Plugin {
|
||||
p := pluginstore.Plugin{
|
||||
JSONData: plugins.JSONData{
|
||||
ID: pluginID,
|
||||
},
|
||||
}
|
||||
for _, cb := range cbs {
|
||||
cb(p)
|
||||
p = cb(p)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func withAngularForLoadingStrategy(angular bool) func(*plugins.Plugin) {
|
||||
return func(p *plugins.Plugin) {
|
||||
p.Angular = plugins.AngularMeta{Detected: angular}
|
||||
}
|
||||
}
|
||||
|
||||
func withFSForLoadingStrategy(fs plugins.FS) func(*plugins.Plugin) {
|
||||
return func(p *plugins.Plugin) {
|
||||
func withFS(fs plugins.FS) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
p.FS = fs
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func withClassForLoadingStrategy(class plugins.Class) func(*plugins.Plugin) {
|
||||
return func(p *plugins.Plugin) {
|
||||
func withAngular(angular bool) func(p pluginstore.Plugin) pluginstore.Plugin {
|
||||
return func(p pluginstore.Plugin) pluginstore.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 {
|
||||
p.Class = class
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func newCfg(ps config.PluginSettings) *config.PluginManagementCfg {
|
||||
return &config.PluginManagementCfg{
|
||||
PluginSettings: ps,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,6 +46,7 @@ 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"
|
||||
@@ -130,6 +131,7 @@ 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,10 +30,9 @@ type Plugin struct {
|
||||
Error *plugins.Error
|
||||
|
||||
// SystemJS fields
|
||||
Module string
|
||||
LoadingStrategy plugins.LoadingStrategy
|
||||
BaseURL string
|
||||
ModuleHash string
|
||||
Module string
|
||||
BaseURL string
|
||||
ModuleHash string
|
||||
|
||||
Angular plugins.AngularMeta
|
||||
|
||||
@@ -78,7 +77,6 @@ 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,11 +1,12 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { memo, useEffect, useMemo } from 'react';
|
||||
import { memo, useEffect, useMemo, useRef } from 'react';
|
||||
import { useLocation, useParams } from 'react-router-dom-v5-compat';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Trans } from '@grafana/i18n';
|
||||
import { config, reportInteraction } from '@grafana/runtime';
|
||||
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
|
||||
import { LinkButton, FilterInput, useStyles2, Text, Stack } from '@grafana/ui';
|
||||
import { useGetFolderQueryFacade, useUpdateFolder } from 'app/api/clients/folder/v1beta1/hooks';
|
||||
import { Page } from 'app/core/components/Page/Page';
|
||||
@@ -44,6 +45,7 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
const location = useLocation();
|
||||
const search = useMemo(() => new URLSearchParams(location.search), [location.search]);
|
||||
const { isReadOnlyRepo, repoType } = useGetResourceRepositoryView({ folderName: folderUID });
|
||||
const isRecentlyViewedEnabled = !folderUID && evaluateBooleanFlag('recentlyViewedDashboards', false);
|
||||
|
||||
useEffect(() => {
|
||||
stateManager.initStateFromUrl(folderUID);
|
||||
@@ -73,6 +75,23 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
}
|
||||
}, [isSearching, searchState.result, stateManager]);
|
||||
|
||||
// Emit exposure event for A/A test once when page loads
|
||||
const hasEmittedExposureEvent = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isRecentlyViewedEnabled || hasEmittedExposureEvent.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasEmittedExposureEvent.current = true;
|
||||
const isExperimentTreatment = evaluateBooleanFlag('experimentRecentlyViewedDashboards', false);
|
||||
|
||||
reportInteraction('dashboards_browse_list_viewed', {
|
||||
experiment_dashboard_list_recently_viewed: isExperimentTreatment ? 'treatment' : 'control',
|
||||
has_recently_viewed_component: isExperimentTreatment,
|
||||
});
|
||||
}, [isRecentlyViewedEnabled]);
|
||||
|
||||
const { data: folderDTO } = useGetFolderQueryFacade(folderUID);
|
||||
const [saveFolder] = useUpdateFolder();
|
||||
const navModel = useMemo(() => {
|
||||
@@ -179,8 +198,8 @@ const BrowseDashboardsPage = memo(({ queryParams }: { queryParams: Record<string
|
||||
>
|
||||
<Page.Contents className={styles.pageContents}>
|
||||
<ProvisionedFolderPreviewBanner queryParams={queryParams} />
|
||||
{/* only show recently viewed dashboards when in root */}
|
||||
{!folderUID && <RecentlyViewedDashboards />}
|
||||
{/* only show recently viewed dashboards when in root and flag is enabled */}
|
||||
{isRecentlyViewedEnabled && <RecentlyViewedDashboards />}
|
||||
<div>
|
||||
<FilterInput
|
||||
placeholder={getSearchPlaceholder(searchState.includePanels)}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useAsyncRetry } from 'react-use';
|
||||
import { GrafanaTheme2, store } from '@grafana/data';
|
||||
import { t, Trans } from '@grafana/i18n';
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { evaluateBooleanFlag } from '@grafana/runtime/internal';
|
||||
import { Button, CollapsableSection, Spinner, Stack, Text, useStyles2, Grid } from '@grafana/ui';
|
||||
import { contextSrv } from 'app/core/services/context_srv';
|
||||
import { useDashboardLocationInfo } from 'app/features/search/hooks/useDashboardLocationInfo';
|
||||
@@ -28,9 +27,6 @@ export function RecentlyViewedDashboards() {
|
||||
retry,
|
||||
error,
|
||||
} = useAsyncRetry(async () => {
|
||||
if (!evaluateBooleanFlag('recentlyViewedDashboards', false)) {
|
||||
return [];
|
||||
}
|
||||
return getRecentlyViewedDashboards(MAX_RECENT);
|
||||
}, []);
|
||||
const { foldersByUid } = useDashboardLocationInfo(recentDashboards.length > 0);
|
||||
@@ -48,7 +44,7 @@ export function RecentlyViewedDashboards() {
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
if (!evaluateBooleanFlag('recentlyViewedDashboards', false) || recentDashboards.length === 0) {
|
||||
if (recentDashboards.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -123,6 +119,7 @@ const getStyles = (theme: GrafanaTheme2) => {
|
||||
color: 'transparent',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
padding: 0,
|
||||
}),
|
||||
content: css({
|
||||
paddingTop: theme.spacing(0),
|
||||
|
||||
+2
-2
@@ -11,9 +11,9 @@ describe('isDashboardDatasource', () => {
|
||||
{ refId: 'C', datasource: { uid: 'mysql-uid', type: 'mysql' } },
|
||||
];
|
||||
|
||||
const backendQueries = queries.filter((q) => !isDashboardDatasource(q));
|
||||
const nonDashboardQueries = queries.filter((q) => !isDashboardDatasource(q));
|
||||
|
||||
expect(backendQueries.map((q) => q.refId)).toEqual(['A', 'C']);
|
||||
expect(nonDashboardQueries.map((q) => q.refId)).toEqual(['A', 'C']);
|
||||
});
|
||||
|
||||
it('returns true when query has dashboard datasource uid', () => {
|
||||
|
||||
@@ -67,9 +67,9 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
||||
|
||||
try {
|
||||
// Filter out Dashboard datasource queries - they are frontend-only and can't be processed by backend
|
||||
const backendQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
|
||||
const nonDashboardQueries = currentQueries.filter((q) => !isDashboardDatasource(q));
|
||||
|
||||
if (backendQueries.length === 0) {
|
||||
if (nonDashboardQueries.length === 0) {
|
||||
setSchemas({ kind: 'SQLSchemaResponse', apiVersion: 'query.grafana.app/v0alpha1', sqlSchemas: {} });
|
||||
setLoading(false);
|
||||
return;
|
||||
@@ -81,7 +81,7 @@ export function useSQLSchemas({ queries, enabled, timeRange }: UseSQLSchemasOpti
|
||||
const response = await getBackendSrv().post<SQLSchemasResponse>(
|
||||
`/apis/query.grafana.app/v0alpha1/namespaces/${namespace}/sqlschemas/name`,
|
||||
{
|
||||
queries: backendQueries,
|
||||
queries: nonDashboardQueries,
|
||||
from: currentTimeRange.from.toISOString(),
|
||||
to: currentTimeRange.to.toISOString(),
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
// Loading strategy is calculated during bootstrap in pkg/plugins/pluginassets/loadingstrategy.go
|
||||
// See: pkg/services/pluginsintegration/pluginassets/pluginassets.go
|
||||
systemJSPrototype.shouldFetch = function (url) {
|
||||
const pluginInfo = getPluginInfoFromCache(url);
|
||||
const jsTypeRegEx = /^[^#?]+\.(js)([?#].*)?$/;
|
||||
|
||||
-5
@@ -36,9 +36,4 @@ describe('ConfigEditor', () => {
|
||||
expect(screen.getByTestId('url-auth-section')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('db-connection-section')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows the informational alert', () => {
|
||||
render(<ConfigEditor {...defaultProps} />);
|
||||
expect(screen.getByText(/You are viewing a new design/i)).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
+1
-18
@@ -2,13 +2,12 @@ import { css } from '@emotion/css';
|
||||
import React from 'react';
|
||||
|
||||
import { GrafanaTheme2 } from '@grafana/data';
|
||||
import { Alert, Box, Stack, TextLink, Text, useStyles2 } from '@grafana/ui';
|
||||
import { Box, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
|
||||
import { DatabaseConnectionSection } from './DatabaseConnectionSection';
|
||||
import { LeftSideBar } from './LeftSideBar';
|
||||
import { UrlAndAuthenticationSection } from './UrlAndAuthenticationSection';
|
||||
import { CONTAINER_MIN_WIDTH } from './constants';
|
||||
import { trackInfluxDBConfigV2FeedbackButtonClicked } from './tracking';
|
||||
import { Props } from './types';
|
||||
|
||||
export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Props) => {
|
||||
@@ -22,22 +21,6 @@ export const ConfigEditor: React.FC<Props> = ({ onOptionsChange, options }: Prop
|
||||
</div>
|
||||
<Box width="60%" flex="1 1 auto" minWidth={CONTAINER_MIN_WIDTH}>
|
||||
<Stack direction="column">
|
||||
<Alert
|
||||
severity="info"
|
||||
title="You are viewing a new design for the InfluxDB configuration settings."
|
||||
className={styles.alertHeight}
|
||||
>
|
||||
<>
|
||||
<TextLink
|
||||
href="https://docs.google.com/forms/d/e/1FAIpQLSdi-zyX3c51vh937UKhNYYxhljUnFi6dQSlZv50mES9NrK-ig/viewform"
|
||||
external
|
||||
onClick={trackInfluxDBConfigV2FeedbackButtonClicked}
|
||||
>
|
||||
Share your thoughts
|
||||
</TextLink>{' '}
|
||||
to help us make it even better.
|
||||
</>
|
||||
</Alert>
|
||||
<Text variant="bodySmall" color="secondary">
|
||||
Fields marked with * are required
|
||||
</Text>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { truncate } from 'lodash';
|
||||
|
||||
import { reportInteraction } from '@grafana/runtime';
|
||||
import { Box, Card, Icon, Link, Stack, Text, useStyles2 } from '@grafana/ui';
|
||||
import { LocationInfo } from 'app/features/search/service/types';
|
||||
@@ -25,6 +27,7 @@ export function DashListItem({
|
||||
onStarChange,
|
||||
}: Props) {
|
||||
const css = useStyles2(getStyles);
|
||||
const shortTitle = truncate(dashboard.name, { length: 40, omission: '…' });
|
||||
|
||||
const onCardLinkClick = () => {
|
||||
reportInteraction('grafana_recently_viewed_dashboards_click_card', {
|
||||
@@ -54,27 +57,35 @@ export function DashListItem({
|
||||
</div>
|
||||
) : (
|
||||
<Card className={css.dashlistCard} noMargin>
|
||||
<Stack justifyContent="space-between" alignItems="center">
|
||||
<Link href={url} onClick={onCardLinkClick}>
|
||||
{dashboard.name}
|
||||
</Link>
|
||||
<StarToolbarButton
|
||||
title={dashboard.name}
|
||||
group="dashboard.grafana.app"
|
||||
kind="Dashboard"
|
||||
id={dashboard.uid}
|
||||
onStarChange={onStarChange}
|
||||
/>
|
||||
</Stack>
|
||||
|
||||
{showFolderNames && locationInfo && (
|
||||
<Stack alignItems="center" direction="row" gap={0}>
|
||||
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
|
||||
<Text color="secondary" variant="bodySmall" element="p">
|
||||
{locationInfo?.name}
|
||||
</Text>
|
||||
<Stack direction="column" justifyContent="space-between" height="100%">
|
||||
<Stack justifyContent="space-between" alignItems="start">
|
||||
<Link
|
||||
className={css.dashlistCardLink}
|
||||
href={url}
|
||||
aria-label={dashboard.name}
|
||||
title={dashboard.name}
|
||||
onClick={onCardLinkClick}
|
||||
>
|
||||
{shortTitle}
|
||||
</Link>
|
||||
<StarToolbarButton
|
||||
title={dashboard.name}
|
||||
group="dashboard.grafana.app"
|
||||
kind="Dashboard"
|
||||
id={dashboard.uid}
|
||||
onStarChange={onStarChange}
|
||||
/>
|
||||
</Stack>
|
||||
)}
|
||||
|
||||
{showFolderNames && locationInfo && (
|
||||
<Stack alignItems="start" direction="row" gap={0.5} height="25%">
|
||||
<Icon name="folder" size="sm" className={css.dashlistCardIcon} aria-hidden="true" />
|
||||
<Text color="secondary" variant="bodySmall" element="p">
|
||||
{locationInfo?.name}
|
||||
</Text>
|
||||
</Stack>
|
||||
)}
|
||||
</Stack>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -32,6 +32,7 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
textDecoration: 'underline',
|
||||
},
|
||||
height: '100%',
|
||||
paddingTop: theme.spacing(1.5),
|
||||
|
||||
'&:hover': {
|
||||
backgroundImage: gradient,
|
||||
@@ -41,5 +42,8 @@ export const getStyles = (theme: GrafanaTheme2) => {
|
||||
dashlistCardIcon: css({
|
||||
marginRight: theme.spacing(0.5),
|
||||
}),
|
||||
dashlistCardLink: css({
|
||||
paddingTop: theme.spacing(0.5),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user