Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3c5229986 | ||
|
|
7a2775b1a7 | ||
|
|
d25a8f5e72 | ||
|
|
48d032e5aa | ||
|
|
ae06690681 |
@@ -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
|
||||
|
||||
@@ -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,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/).
|
||||
|
||||
+59
-102
@@ -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.
|
||||
|
||||
+2
-3
@@ -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:**
|
||||
|
||||
@@ -177,16 +177,6 @@ Default Views are also available including:
|
||||
| South Asia | South-East Asia | East Asia | Australia | Oceania |
|
||||
<!-- prettier-ignore-end -->
|
||||
|
||||
#### Enable dashboard variable (Experimental)
|
||||
|
||||
When toggled, the Geomap panel updates the dashboard variable named `mapViewData` (if it exists) with the bounding box data of the current view.
|
||||
|
||||
The variable should be of the type `Custom` and can be hidden on the dashboard by setting its visibility to “Nothing” (Show on dashboard: Nothing).
|
||||
|
||||
You can use then the variable inside your query to receive on the server side a string like this:
|
||||
|
||||
4.102646520428928,44.489253792751214,12.793524298588558,46.4346026776777
|
||||
|
||||
#### Share view
|
||||
|
||||
The **Share view** option allows you to link the movement and zoom actions of multiple map visualizations within the same dashboard. The map visualizations that have this option enabled act in tandem when one of them is moved or zoomed, leaving the other ones independent.
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
-1
@@ -26,7 +26,6 @@ export const defaultOptions: Partial<Options> = {
|
||||
|
||||
export interface MapViewConfig {
|
||||
allLayers?: boolean;
|
||||
dashboardVariable?: boolean;
|
||||
id: string;
|
||||
lastOnly?: boolean;
|
||||
lat?: number;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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{})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,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
|
||||
}
|
||||
+63
-67
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
Generated
+4
-7
@@ -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
|
||||
}
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Generated
-1
@@ -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
|
||||
|
||||
|
-15
@@ -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),
|
||||
|
||||
@@ -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)([?#].*)?$/;
|
||||
|
||||
+5
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
+18
-1
@@ -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),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,6 @@ import Attribution from 'ol/control/Attribution';
|
||||
import ScaleLine from 'ol/control/ScaleLine';
|
||||
import Zoom from 'ol/control/Zoom';
|
||||
import { Coordinate } from 'ol/coordinate';
|
||||
import { EventsKey } from 'ol/events';
|
||||
import { isEmpty } from 'ol/extent';
|
||||
import MouseWheelZoom from 'ol/interaction/MouseWheelZoom';
|
||||
import { fromLonLat, transformExtent } from 'ol/proj';
|
||||
@@ -17,7 +16,7 @@ import { Subscription } from 'rxjs';
|
||||
|
||||
import { DataHoverEvent, PanelData, PanelProps } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { config, locationService } from '@grafana/runtime';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { PanelContext, PanelContextRoot } from '@grafana/ui';
|
||||
import { appEvents } from 'app/core/app_events';
|
||||
import { VariablesChanged } from 'app/features/variables/types';
|
||||
@@ -74,8 +73,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
layers: MapLayerState[] = [];
|
||||
readonly byName = new Map<string, MapLayerState>();
|
||||
|
||||
mapViewData?: string;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { ttipOpen: false, legends: [] };
|
||||
@@ -128,12 +125,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
// Check for resize
|
||||
if (this.props.height !== nextProps.height || this.props.width !== nextProps.width) {
|
||||
this.map.updateSize();
|
||||
// update dashboard variable if enabled
|
||||
const options = this.props.options;
|
||||
if (options.view.dashboardVariable) {
|
||||
const view = this.map.getView();
|
||||
this.updateGeoVariables(view, options);
|
||||
}
|
||||
}
|
||||
|
||||
// External data changed
|
||||
@@ -236,28 +227,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
this.setState({ legends: this.getLegends() });
|
||||
}
|
||||
|
||||
// view listerner handler, used to unregister when view changes
|
||||
private viewListernerKey: EventsKey | null = null;
|
||||
|
||||
// updateGeoVariables debounce timeout
|
||||
private timeoutId: NodeJS.Timeout | null = null; // for debounce
|
||||
|
||||
// Updates the dashboard variable `mapViewData` with the view extent value.
|
||||
// Use a debounce strategy to wait the user stop dragging or zooming the map.
|
||||
updateGeoVariables = (view: View, options: Options) => {
|
||||
const bounds = view.calculateExtent();
|
||||
const bounds4326 = transformExtent(bounds, 'EPSG:3857', 'EPSG:4326');
|
||||
if (this.timeoutId) {
|
||||
clearTimeout(this.timeoutId);
|
||||
}
|
||||
this.timeoutId = setTimeout(() => {
|
||||
if (options.controls.showDebug) {
|
||||
console.log('GeomapPanel.updateGeoVariables', bounds4326);
|
||||
}
|
||||
locationService.partial({ 'var-mapViewData': `${bounds4326}` }, true);
|
||||
}, 500);
|
||||
};
|
||||
|
||||
initMapAsync = async (div: HTMLDivElement | null) => {
|
||||
if (!div) {
|
||||
// Do not initialize new map or dispose old map
|
||||
@@ -299,9 +268,7 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
}
|
||||
this.layers = layers;
|
||||
this.map = map; // redundant
|
||||
const view = map.getView();
|
||||
const viewConfig = options.view;
|
||||
this.initViewExtent(view, viewConfig);
|
||||
this.initViewExtent(map.getView(), options.view);
|
||||
|
||||
this.mouseWheelZoom = new MouseWheelZoom();
|
||||
this.map?.addInteraction(this.mouseWheelZoom);
|
||||
@@ -311,20 +278,6 @@ export class GeomapPanel extends Component<Props, State> {
|
||||
notifyPanelEditor(this, layers, layers.length - 1);
|
||||
|
||||
this.setState({ legends: this.getLegends() });
|
||||
|
||||
// register view listener to update dashboard variable if enabled
|
||||
if (viewConfig.dashboardVariable) {
|
||||
if (options.controls.showDebug) {
|
||||
console.log('Geomap.initMapAsync: register view listener', view);
|
||||
}
|
||||
if (this.viewListernerKey != null) {
|
||||
view.un('change', this.viewListernerKey.listener);
|
||||
}
|
||||
this.viewListernerKey = view.on('change', () => {
|
||||
this.updateGeoVariables(view, options);
|
||||
});
|
||||
this.updateGeoVariables(view, options);
|
||||
}
|
||||
};
|
||||
|
||||
clearTooltip = () => {
|
||||
|
||||
@@ -32,14 +32,6 @@ export const plugin = new PanelPlugin<Options>(GeomapPanel)
|
||||
defaultValue: defaultMapViewConfig,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
category,
|
||||
path: 'view.dashboardVariable',
|
||||
description: 'Store view bounds into the "mapViewData" dashboard variable.',
|
||||
name: 'Enable dashboard variable (Experimental)',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
||||
builder.addBooleanSwitch({
|
||||
category,
|
||||
path: 'view.shared',
|
||||
|
||||
@@ -34,19 +34,18 @@ composableKinds: PanelCfg: {
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
MapViewConfig: {
|
||||
id: string | *"zero"
|
||||
lat?: int64 | *0
|
||||
lon?: int64 | *0
|
||||
zoom?: int64 | *1
|
||||
minZoom?: int64
|
||||
maxZoom?: int64
|
||||
padding?: int64
|
||||
allLayers?: bool | *true
|
||||
lastOnly?: bool
|
||||
layer?: string
|
||||
shared?: bool
|
||||
noRepeat?: bool | *false
|
||||
dashboardVariable?: bool
|
||||
id: string | *"zero"
|
||||
lat?: int64 | *0
|
||||
lon?: int64 | *0
|
||||
zoom?: int64 | *1
|
||||
minZoom?: int64
|
||||
maxZoom?: int64
|
||||
padding?: int64
|
||||
allLayers?: bool | *true
|
||||
lastOnly?: bool
|
||||
layer?: string
|
||||
shared?: bool
|
||||
noRepeat?: bool | *false
|
||||
} @cuetsy(kind="interface")
|
||||
|
||||
ControlsOptions: {
|
||||
|
||||
-1
@@ -24,7 +24,6 @@ export const defaultOptions: Partial<Options> = {
|
||||
|
||||
export interface MapViewConfig {
|
||||
allLayers?: boolean;
|
||||
dashboardVariable?: boolean;
|
||||
id: string;
|
||||
lastOnly?: boolean;
|
||||
lat?: number;
|
||||
|
||||
Reference in New Issue
Block a user